Skip to content

Commit f22457d

Browse files
asynclizcopybara-github
authored andcommitted
feat(labs): add expressive system stylesheets
PiperOrigin-RevId: 895367246
1 parent a589695 commit f22457d

File tree

14 files changed

+531
-2
lines changed

14 files changed

+531
-2
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ node_modules
55
*.css
66
*.cssresult.ts
77
*-styles.ts
8+
!adopt-styles.ts
89
tokens/versions/**/*-meta.scss
910
*.map
1011
*.d.ts

labs/gb/styles/adopt-styles.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import {type CSSResult, type CSSResultOrNative} from 'lit';
8+
9+
/**
10+
* Owner types that can adopt stylesheets using `adoptStyles()`.
11+
*/
12+
export type AdoptStylesOwner = DocumentOrShadowRoot | Element;
13+
14+
/**
15+
* Adopts the given stylesheets to the provided document or shadow root owner.
16+
*
17+
* @example
18+
* ```ts
19+
* import globalStylesheet from './stylesheet.css' with {type: 'css'};
20+
*
21+
* adoptStyles(document, globalStylesheet);
22+
* ```
23+
*
24+
* If an element is provided, the styles are adopted to the element's owner
25+
* document. If the element is within a shadow root, the styles are also adopted
26+
* to the host shadow root.
27+
*
28+
* @example
29+
* ```ts
30+
* import hostClasses from './stylesheet.css' with {type: 'css'};
31+
*
32+
* class LightDomElement extends HTMLElement {
33+
* connectedCallback() {
34+
* adoptStyles(this, hostClasses);
35+
* this.classList.add('host-class');
36+
* }
37+
* }
38+
* ```
39+
*
40+
* @param owner The owner document, shadow root, or element to adopt the
41+
* styles to.
42+
* @param styles The styles to adopt.
43+
*/
44+
export function adoptStyles(
45+
owner: AdoptStylesOwner | null | undefined,
46+
styles: CSSResultOrNative | CSSResultOrNative[],
47+
): void {
48+
if (!owner) return;
49+
50+
styles = Array.isArray(styles) ? styles : [styles];
51+
const isCSSResult = (style: CSSResultOrNative): style is CSSResult =>
52+
'styleSheet' in style;
53+
const stylesheets: CSSStyleSheet[] = styles.map((cssResultOrNative) =>
54+
isCSSResult(cssResultOrNative)
55+
? cssResultOrNative.styleSheet!
56+
: cssResultOrNative,
57+
);
58+
59+
const adopt = (
60+
node: DocumentOrShadowRoot | Node | null,
61+
stylesheets: CSSStyleSheet[],
62+
): node is DocumentOrShadowRoot => {
63+
if (node && 'adoptedStyleSheets' in node) {
64+
node.adoptedStyleSheets = Array.from(
65+
new Set([...node.adoptedStyleSheets, ...stylesheets]),
66+
);
67+
return true;
68+
}
69+
return false;
70+
};
71+
72+
if (adopt(owner, stylesheets)) {
73+
// Styles adopted directly on the owner document or shadow root.
74+
return;
75+
}
76+
77+
// When provided an element, adopt styles to the element's document and host
78+
// shadow root, if present.
79+
adopt(owner.ownerDocument, stylesheets);
80+
adopt(owner.getRootNode(), stylesheets);
81+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
// import 'jasmine'; (google3-only)
8+
9+
import {adoptStyles} from './adopt-styles.js';
10+
11+
describe('adoptStyles()', () => {
12+
let sheet: CSSStyleSheet;
13+
14+
beforeEach(() => {
15+
document.adoptedStyleSheets = [];
16+
sheet = new CSSStyleSheet();
17+
});
18+
19+
it('should adopt to a ShadowRoot', () => {
20+
const host = document.createElement('div');
21+
const shadowRoot = host.attachShadow({mode: 'open'});
22+
adoptStyles(shadowRoot, sheet);
23+
24+
expect(shadowRoot.adoptedStyleSheets)
25+
.withContext('shadowRoot.adoptedStyleSheets after adopt')
26+
.toContain(sheet);
27+
});
28+
29+
it('should adopt to a Document', () => {
30+
adoptStyles(document, sheet);
31+
32+
expect(document.adoptedStyleSheets)
33+
.withContext('document.adoptedStyleSheets after adopt')
34+
.toContain(sheet);
35+
});
36+
37+
it("should adopt to an Element's document", () => {
38+
const element = document.createElement('div');
39+
adoptStyles(element, sheet);
40+
41+
expect(document.adoptedStyleSheets)
42+
.withContext(
43+
'document.adoptedStyleSheets after adopt (element in light dom)',
44+
)
45+
.toContain(sheet);
46+
});
47+
48+
it("should adopt to an Element's document and host ShadowRoot", () => {
49+
const host = document.createElement('div');
50+
const shadowRoot = host.attachShadow({mode: 'open'});
51+
const element = document.createElement('div');
52+
shadowRoot.appendChild(element);
53+
54+
adoptStyles(element, sheet);
55+
56+
expect(document.adoptedStyleSheets)
57+
.withContext(
58+
'document.adoptedStyleSheets after adopt (element in shadow dom)',
59+
)
60+
.toContain(sheet);
61+
expect(shadowRoot.adoptedStyleSheets)
62+
.withContext(
63+
'shadowRoot.adoptedStyleSheets after adopt (element in shadow dom)',
64+
)
65+
.toContain(sheet);
66+
});
67+
});
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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 'sass:meta';
8+
@use '../../../../../sass/ext/map_ext';
9+
@use '../../../../../tokens/versions/latest/sass/md-ref-palette';
10+
@use '../../../../../tokens/versions/latest/sass/md-sys-color';
11+
@use '../../../../../tokens/versions/latest/sass/md-sys-color-meta';
12+
@use '../../../../../tokens/versions/latest/sass/md-sys-color__dark';
13+
@use '../../../../../tokens/versions/latest/sass/md-sys-color__dark-meta';
14+
// go/keep-sorted end
15+
16+
@mixin emit-custom-properties(
17+
$light-vars: meta.module-variables(md-sys-color),
18+
$light-meta: meta.module-variables(md-sys-color-meta),
19+
$dark-vars: meta.module-variables(md-sys-color__dark),
20+
$dark-meta: meta.module-variables(md-sys-color__dark-meta),
21+
$include-palette: false,
22+
$palette-vars: meta.module-variables(md-ref-palette),
23+
) {
24+
$color-tokens: (
25+
'primary',
26+
'on-primary',
27+
'primary-container',
28+
'on-primary-container',
29+
'primary-fixed',
30+
'primary-fixed-dim',
31+
'on-primary-fixed',
32+
'on-primary-fixed-variant',
33+
'secondary',
34+
'on-secondary',
35+
'secondary-container',
36+
'on-secondary-container',
37+
'secondary-fixed',
38+
'secondary-fixed-dim',
39+
'on-secondary-fixed',
40+
'on-secondary-fixed-variant',
41+
'tertiary',
42+
'on-tertiary',
43+
'tertiary-container',
44+
'on-tertiary-container',
45+
'tertiary-fixed',
46+
'tertiary-fixed-dim',
47+
'on-tertiary-fixed',
48+
'on-tertiary-fixed-variant',
49+
'error',
50+
'on-error',
51+
'error-container',
52+
'on-error-container',
53+
'surface',
54+
'surface-dim',
55+
'surface-bright',
56+
'surface-container-lowest',
57+
'surface-container-low',
58+
'surface-container',
59+
'surface-container-high',
60+
'surface-container-highest',
61+
'on-surface',
62+
'on-surface-variant',
63+
'outline',
64+
'outline-variant',
65+
'inverse-surface',
66+
'inverse-on-surface',
67+
'inverse-primary',
68+
'scrim',
69+
'shadow',
70+
);
71+
72+
@each $name in $color-tokens {
73+
$light: map_ext.get-strict($light-vars, $name);
74+
$dark: map_ext.get-strict($dark-vars, $name);
75+
76+
@if $include-palette {
77+
// Change value to reference palette variables.
78+
$light: map_ext.get-strict($light-meta, '#{$name}--resolved');
79+
$dark: map_ext.get-strict($dark-meta, '#{$name}--resolved');
80+
}
81+
82+
$value: $light;
83+
@if $light != $dark {
84+
$value: light-dark($light, $dark);
85+
}
86+
87+
--md-sys-color-#{$name}: #{$value};
88+
}
89+
90+
@if $include-palette {
91+
@each $name, $value in $palette-vars {
92+
--md-ref-palette-#{$name}: #{$value};
93+
}
94+
}
95+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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 'internal/color-tokens';
8+
// go/keep-sorted end
9+
10+
@layer md.sys.color {
11+
:root {
12+
color-scheme: light dark;
13+
@include color-tokens.emit-custom-properties(
14+
$include-palette: true,
15+
);
16+
}
17+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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 'internal/color-tokens';
8+
// go/keep-sorted end
9+
10+
@layer md.sys.color {
11+
:root {
12+
color-scheme: light dark;
13+
@include color-tokens.emit-custom-properties;
14+
}
15+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*!
2+
* Copyright 2026 Google LLC
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
@layer md.sys.elevation {
7+
:root {
8+
--md-sys-elevation-shadow-0: none;
9+
--md-sys-elevation-shadow-1:
10+
0 1px 2px 0 hsl(from var(--md-sys-color-shadow) h s l / 0.3),
11+
0 1px 3px 1px hsl(from var(--md-sys-color-shadow) h s l / 0.15);
12+
--md-sys-elevation-shadow-2:
13+
0 1px 2px 0 hsl(from var(--md-sys-color-shadow) h s l / 0.3),
14+
0 2px 6px 2px hsl(from var(--md-sys-color-shadow) h s l / 0.15);
15+
--md-sys-elevation-shadow-3:
16+
0 1px 3px 0 hsl(from var(--md-sys-color-shadow) h s l / 0.3),
17+
0 4px 8px 3px hsl(from var(--md-sys-color-shadow) h s l / 0.15);
18+
--md-sys-elevation-shadow-4:
19+
0 2px 3px 0 hsl(from var(--md-sys-color-shadow) h s l / 0.3),
20+
0 6px 10px 4px hsl(from var(--md-sys-color-shadow) h s l / 0.15);
21+
--md-sys-elevation-shadow-5:
22+
0 4px 4px 0 hsl(from var(--md-sys-color-shadow) h s l / 0.3),
23+
0 8px 12px 6px hsl(from var(--md-sys-color-shadow) h s l / 0.15);
24+
}
25+
}

labs/gb/styles/icon/md-icon.scss

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*!
2+
* Copyright 2026 Google LLC
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
@property --md-icon-font {
7+
syntax: '<string>#';
8+
inherits: true;
9+
initial-value: 'Material Symbols';
10+
}
11+
@property --md-icon-color {
12+
syntax: '<color>';
13+
inherits: true;
14+
initial-value: currentColor;
15+
}
16+
@property --md-icon-size {
17+
syntax: '<length-percentage>';
18+
inherits: true;
19+
initial-value: 24px;
20+
}
21+
@property --md-icon-opsz {
22+
syntax: '<number>';
23+
inherits: true;
24+
initial-value: 24;
25+
}
26+
@property --md-icon-wght {
27+
syntax: '<number>';
28+
inherits: true;
29+
initial-value: 400;
30+
}
31+
@property --md-icon-fill {
32+
syntax: '<number>';
33+
inherits: true;
34+
initial-value: 0;
35+
}
36+
@property --md-icon-grad {
37+
syntax: '<number>';
38+
inherits: true;
39+
initial-value: 0;
40+
}
41+
42+
@layer md.sys.icon {
43+
.md-icon {
44+
display: inline-flex;
45+
place-items: center;
46+
place-content: center;
47+
overflow: hidden;
48+
color: var(--md-icon-color);
49+
fill: var(--md-icon-color);
50+
aspect-ratio: 1;
51+
min-width: var(--md-icon-size);
52+
max-width: var(--md-icon-size);
53+
font-size: var(--md-icon-size);
54+
font-family: var(--md-icon-font);
55+
font-variation-settings:
56+
'opsz' var(--md-icon-opsz),
57+
'wght' var(--md-icon-wght),
58+
'FILL' var(--md-icon-fill),
59+
'GRAD' var(--md-icon-grad);
60+
-webkit-font-smoothing: antialiased;
61+
}
62+
}

labs/gb/styles/m3.scss

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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+
@import 'color/md-color-tokens';
8+
@import 'elevation/md-elevation-tokens';
9+
@import 'icon/md-icon';
10+
@import 'motion/md-motion-tokens-easing';
11+
@import 'shape/md-shape-tokens';
12+
@import 'typography/md-typography-tokens';
13+
// go/keep-sorted end

0 commit comments

Comments
 (0)