Skip to content

Commit c86e3a2

Browse files
asynclizcopybara-github
authored andcommitted
feat(labs): add expressive fab utility class component
PiperOrigin-RevId: 897859708
1 parent d37988b commit c86e3a2

File tree

6 files changed

+560
-0
lines changed

6 files changed

+560
-0
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
//
2+
// Copyright 2026 Google LLC
3+
// SPDX-License-Identifier: Apache-2.0
4+
//
5+
6+
@mixin root {
7+
--container-color: transparent;
8+
--container-elevation: var(--md-sys-elevation-shadow-3);
9+
--container-height: 56px;
10+
--container-shape: var(--md-sys-shape-corner-lg);
11+
--icon-color: currentColor;
12+
--icon-label-space: 12px;
13+
--icon-size: 24px;
14+
--label-text: var(--md-sys-typescale-label-lg);
15+
--label-text-color: currentColor;
16+
--label-text-tracking: var(--md-sys-typescale-label-lg-tracking);
17+
--leading-space: 16px;
18+
--trailing-space: 16px;
19+
}
20+
21+
@mixin hovered {
22+
--container-elevation: var(--md-sys-elevation-shadow-4);
23+
}
24+
25+
@mixin pressed {
26+
--container-elevation: var(--md-sys-elevation-shadow-3);
27+
}
28+
29+
@mixin primary-container {
30+
--container-color: var(--md-sys-color-primary-container);
31+
--icon-color: var(--md-sys-color-on-primary-container);
32+
--label-text-color: var(--md-sys-color-on-primary-container);
33+
}
34+
35+
@mixin primary {
36+
--container-color: var(--md-sys-color-primary);
37+
--icon-color: var(--md-sys-color-on-primary);
38+
--label-text-color: var(--md-sys-color-on-primary);
39+
}
40+
41+
@mixin secondary-container {
42+
--container-color: var(--md-sys-color-secondary-container);
43+
--icon-color: var(--md-sys-color-on-secondary-container);
44+
--label-text-color: var(--md-sys-color-on-secondary-container);
45+
}
46+
47+
@mixin secondary {
48+
--container-color: var(--md-sys-color-secondary);
49+
--icon-color: var(--md-sys-color-on-secondary);
50+
--label-text-color: var(--md-sys-color-on-secondary);
51+
}
52+
53+
@mixin tertiary-container {
54+
--container-color: var(--md-sys-color-tertiary-container);
55+
--icon-color: var(--md-sys-color-on-tertiary-container);
56+
--label-text-color: var(--md-sys-color-on-tertiary-container);
57+
}
58+
59+
@mixin tertiary {
60+
--container-color: var(--md-sys-color-tertiary);
61+
--icon-color: var(--md-sys-color-on-tertiary);
62+
--label-text-color: var(--md-sys-color-on-tertiary);
63+
}
64+
65+
@mixin md {
66+
--container-height: 80px;
67+
--container-shape: var(--md-sys-shape-corner-lg-increased);
68+
--icon-size: 28px;
69+
--label-text: var(--md-sys-typescale-title-lg);
70+
--label-text-tracking: var(--md-sys-typescale-title-lg-tracking);
71+
--leading-space: 26px;
72+
--trailing-space: 26px;
73+
}
74+
75+
@mixin lg {
76+
--container-height: 96px;
77+
--container-shape: var(--md-sys-shape-corner-xl);
78+
--icon-label-space: 16px;
79+
--icon-size: 36px;
80+
--label-text: var(--md-sys-typescale-headline-sm);
81+
--label-text-tracking: var(--md-sys-typescale-headline-sm-tracking);
82+
--leading-space: 28px;
83+
--trailing-space: 28px;
84+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import './material-collection.js';
8+
import './index.js';
9+
10+
import {
11+
KnobTypesToKnobs,
12+
MaterialCollection,
13+
materialInitsToStoryInits,
14+
setUpDemo,
15+
} from './material-collection.js';
16+
import {Knob, selectDropdown, textInput} from './index.js';
17+
18+
import {type FabColor, type FabSize} from '../fab.js';
19+
import {stories, StoryKnobs} from './stories.js';
20+
21+
const collection = new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>('Fab', [
22+
new Knob('icon', {
23+
ui: textInput(),
24+
defaultValue: 'add',
25+
}),
26+
new Knob('label', {
27+
ui: textInput(),
28+
}),
29+
new Knob('color', {
30+
ui: selectDropdown<FabColor>({
31+
options: [
32+
{value: 'primary', label: 'Primary'},
33+
{value: 'primary-container', label: 'Primary Container'},
34+
{value: 'secondary', label: 'Secondary'},
35+
{value: 'secondary-container', label: 'Secondary Container'},
36+
{value: 'tertiary', label: 'Tertiary'},
37+
{value: 'tertiary-container', label: 'Tertiary Container'},
38+
],
39+
}),
40+
}),
41+
new Knob('size', {
42+
ui: selectDropdown<FabSize>({
43+
options: [
44+
{value: 'default', label: 'Default'},
45+
{value: 'md', label: 'Medium'},
46+
{value: 'lg', label: 'Large'},
47+
],
48+
}),
49+
}),
50+
]);
51+
52+
collection.addStories(...materialInitsToStoryInits(stories));
53+
54+
setUpDemo(collection, {fonts: 'roboto', icons: 'material-symbols'});
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import '@material/web/icon/icon.js';
8+
import '@material/web/labs/gb/components/fab/md-fab.js';
9+
10+
import {MaterialStoryInit} from './material-collection.js';
11+
import {FabColor, FabSize} from '@material/web/labs/gb/components/fab/fab.js';
12+
import {adoptStyles} from '@material/web/labs/gb/styles/adopt-styles.js';
13+
import {styles as m3Styles} from '@material/web/labs/gb/styles/m3.cssresult.js';
14+
import {css, html, nothing} from 'lit';
15+
16+
/** Knob types for fab stories. */
17+
export interface StoryKnobs {
18+
icon: string;
19+
label: string;
20+
color?: FabColor;
21+
size?: FabSize;
22+
}
23+
24+
adoptStyles(document, [
25+
m3Styles,
26+
css`
27+
:root {
28+
--md-icon-font: 'Material Symbols Outlined';
29+
}
30+
`,
31+
]);
32+
33+
const playground: MaterialStoryInit<StoryKnobs> = {
34+
name: 'Playground',
35+
render(knobs) {
36+
return html`
37+
<md-fab color="${knobs.color || nothing}" size="${knobs.size || nothing}">
38+
<md-icon>${knobs.icon}</md-icon>
39+
${knobs.label}
40+
</md-fab>
41+
`;
42+
},
43+
};
44+
45+
const extendedFab: MaterialStoryInit<StoryKnobs> = {
46+
name: 'Extended FAB',
47+
render(knobs) {
48+
return html`
49+
<md-fab color="${knobs.color || nothing}" size="${knobs.size || nothing}">
50+
<md-icon>${knobs.icon || 'add'}</md-icon>
51+
${knobs.label || 'Add'}
52+
</md-fab>
53+
`;
54+
},
55+
};
56+
57+
const colors: MaterialStoryInit<StoryKnobs> = {
58+
name: 'Colors',
59+
render(knobs) {
60+
return html`
61+
<md-fab color="primary-container" size="${knobs.size || nothing}">
62+
<md-icon>${knobs.icon}</md-icon>
63+
${knobs.label}
64+
</md-fab>
65+
<md-fab color="secondary-container" size="${knobs.size || nothing}">
66+
<md-icon>${knobs.icon}</md-icon>
67+
${knobs.label}
68+
</md-fab>
69+
<md-fab color="tertiary-container" size="${knobs.size || nothing}">
70+
<md-icon>${knobs.icon}</md-icon>
71+
${knobs.label}
72+
</md-fab>
73+
<md-fab color="primary" size="${knobs.size || nothing}">
74+
<md-icon>${knobs.icon}</md-icon>
75+
${knobs.label}
76+
</md-fab>
77+
<md-fab color="secondary" size="${knobs.size || nothing}">
78+
<md-icon>${knobs.icon}</md-icon>
79+
${knobs.label}
80+
</md-fab>
81+
<md-fab color="tertiary" size="${knobs.size || nothing}">
82+
<md-icon>${knobs.icon}</md-icon>
83+
${knobs.label}
84+
</md-fab>
85+
`;
86+
},
87+
};
88+
89+
const sizes: MaterialStoryInit<StoryKnobs> = {
90+
name: 'Sizes',
91+
render(knobs) {
92+
return html`
93+
<md-fab color="${knobs.color || nothing}">
94+
<md-icon>${knobs.icon}</md-icon>
95+
${knobs.label}
96+
</md-fab>
97+
<md-fab color="${knobs.color || nothing}" size="md">
98+
<md-icon>${knobs.icon}</md-icon>
99+
${knobs.label}
100+
</md-fab>
101+
<md-fab color="${knobs.color || nothing}" size="lg">
102+
<md-icon>${knobs.icon}</md-icon>
103+
${knobs.label}
104+
</md-fab>
105+
`;
106+
},
107+
};
108+
109+
/** Fab stories. */
110+
export const stories = [playground, extendedFab, colors, sizes];

labs/gb/components/fab/fab.scss

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*!
2+
* Copyright 2026 Google LLC
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
// go/keep-sorted start by_regex='(.+) prefix_order=sass:
7+
@use 'fab-tokens';
8+
// go/keep-sorted end
9+
10+
// TODO: add any md.comp dependencies (e.g. md.comp.ripple)
11+
@layer md.sys, md.comp.dependencies;
12+
@layer md.comp.fab {
13+
.fab {
14+
& {
15+
@include fab-tokens.root;
16+
}
17+
18+
&:is(:hover, .hover) {
19+
@include fab-tokens.hovered;
20+
}
21+
22+
&:is(:active, .active) {
23+
@include fab-tokens.pressed;
24+
}
25+
26+
&.fab-primary-container {
27+
@include fab-tokens.primary-container;
28+
}
29+
30+
&.fab-primary {
31+
@include fab-tokens.primary;
32+
}
33+
34+
&.fab-secondary-container {
35+
@include fab-tokens.secondary-container;
36+
}
37+
38+
&.fab-secondary {
39+
@include fab-tokens.secondary;
40+
}
41+
42+
&.fab-tertiary-container {
43+
@include fab-tokens.tertiary-container;
44+
}
45+
46+
&.fab-tertiary {
47+
@include fab-tokens.tertiary;
48+
}
49+
50+
&.fab-md {
51+
@include fab-tokens.md;
52+
}
53+
54+
&.fab-lg {
55+
@include fab-tokens.lg;
56+
}
57+
58+
& {
59+
appearance: none;
60+
border: none;
61+
display: inline-flex;
62+
align-items: center;
63+
gap: var(--icon-label-space);
64+
height: var(--container-height);
65+
background-color: var(--container-color);
66+
border-radius: var(--container-shape);
67+
box-shadow: var(--container-elevation);
68+
color: var(--label-text-color);
69+
font: var(--label-text);
70+
letter-spacing: var(--label-text-tracking);
71+
padding-inline: var(--leading-space) var(--trailing-space);
72+
animation: var(--ripple-animation), var(--focus-ring-animation);
73+
--md-icon-color: var(--icon-color);
74+
--md-icon-size: var(--icon-size);
75+
}
76+
}
77+
}

0 commit comments

Comments
 (0)