Skip to content

Commit

Permalink
feat: Support theme selector in HTML
Browse files Browse the repository at this point in the history
  • Loading branch information
YueyingLu committed Aug 27, 2023
1 parent 20ef6bf commit 5d38d0c
Show file tree
Hide file tree
Showing 10 changed files with 106 additions and 65 deletions.
20 changes: 10 additions & 10 deletions src/browser/__tests__/__snapshots__/index.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ exports[`with baseThemeId attaches one style node containing overrides with the
--containerShadowBase-css:2px 3px orange, -1px 0 8px olive;
--modalShadowContainer-css:2px 3px orange, -1px 0 8px olive;
}
.compact.compact.secondary-theme:not(#\\\\9){
.compact.compact.secondary-theme:not(#\\\\9), html.secondary-theme.secondary-theme .compact:not(#\\\\9){
--fontFamilyBase-css:\\"Helvetica Neue\\", Arial, sans-serif;
--fontFamilyBody-css:\\"Helvetica Neue\\", Arial, sans-serif;
--black-css:purple;
Expand All @@ -30,7 +30,7 @@ exports[`with baseThemeId attaches one style node containing overrides with the
--containerShadowBase-css:2px 3px orange, -1px 0 8px olive;
--modalShadowContainer-css:2px 3px orange, -1px 0 8px olive;
}
@media not print {.dark.dark.secondary-theme:not(#\\\\9){
@media not print {.dark.dark.secondary-theme:not(#\\\\9), html.secondary-theme.secondary-theme .dark:not(#\\\\9){
--fontFamilyBase-css:\\"Helvetica Neue\\", Arial, sans-serif;
--fontFamilyBody-css:\\"Helvetica Neue\\", Arial, sans-serif;
--black-css:purple;
Expand All @@ -45,7 +45,7 @@ exports[`with baseThemeId attaches one style node containing overrides with the
--containerShadowBase-css:2px 3px orange, -1px 0 8px olive;
--modalShadowContainer-css:2px 3px orange, -1px 0 8px olive;
}}
.disabled-motion.disabled-motion.secondary-theme:not(#\\\\9){
.disabled-motion.disabled-motion.secondary-theme:not(#\\\\9), html.secondary-theme.secondary-theme .disabled-motion:not(#\\\\9){
--fontFamilyBase-css:\\"Helvetica Neue\\", Arial, sans-serif;
--fontFamilyBody-css:\\"Helvetica Neue\\", Arial, sans-serif;
--black-css:purple;
Expand Down Expand Up @@ -74,7 +74,7 @@ exports[`with baseThemeId attaches one style node containing overrides with the
--containerShadowBase-css:2px 3px orange, -1px 0 8px olive;
--modalShadowContainer-css:2px 3px orange, -1px 0 8px olive;
}
.navigation.navigation.secondary-theme:not(#\\\\9){
.navigation.navigation.secondary-theme:not(#\\\\9), html.secondary-theme.secondary-theme .navigation:not(#\\\\9){
--fontFamilyBase-css:\\"Helvetica Neue\\", Arial, sans-serif;
--fontFamilyBody-css:\\"Helvetica Neue\\", Arial, sans-serif;
--black-css:purple;
Expand All @@ -91,7 +91,7 @@ exports[`with baseThemeId attaches one style node containing overrides with the
--containerShadowBase-css:2px 3px orange, -1px 0 8px olive;
--modalShadowContainer-css:2px 3px orange, -1px 0 8px olive;
}
.compact.compact.secondary-theme .navigation:not(#\\\\9){
.compact.compact.secondary-theme .navigation:not(#\\\\9), html.secondary-theme.secondary-theme .compact .navigation:not(#\\\\9){
--fontFamilyBase-css:\\"Helvetica Neue\\", Arial, sans-serif;
--fontFamilyBody-css:\\"Helvetica Neue\\", Arial, sans-serif;
--black-css:purple;
Expand All @@ -103,7 +103,7 @@ exports[`with baseThemeId attaches one style node containing overrides with the
--containerShadowBase-css:2px 3px orange, -1px 0 8px olive;
--modalShadowContainer-css:2px 3px orange, -1px 0 8px olive;
}
.compact.compact.navigation.secondary-theme:not(#\\\\9){
.compact.compact.navigation.secondary-theme:not(#\\\\9), html.secondary-theme.secondary-theme .compact.navigation:not(#\\\\9){
--fontFamilyBase-css:\\"Helvetica Neue\\", Arial, sans-serif;
--fontFamilyBody-css:\\"Helvetica Neue\\", Arial, sans-serif;
--black-css:purple;
Expand All @@ -115,7 +115,7 @@ exports[`with baseThemeId attaches one style node containing overrides with the
--containerShadowBase-css:2px 3px orange, -1px 0 8px olive;
--modalShadowContainer-css:2px 3px orange, -1px 0 8px olive;
}
@media not print {.dark.dark.secondary-theme .navigation:not(#\\\\9){
@media not print {.dark.dark.secondary-theme .navigation:not(#\\\\9), html.secondary-theme.secondary-theme .dark .navigation:not(#\\\\9){
--fontFamilyBase-css:\\"Helvetica Neue\\", Arial, sans-serif;
--fontFamilyBody-css:\\"Helvetica Neue\\", Arial, sans-serif;
--black-css:purple;
Expand All @@ -130,7 +130,7 @@ exports[`with baseThemeId attaches one style node containing overrides with the
--containerShadowBase-css:2px 3px orange, -1px 0 8px olive;
--modalShadowContainer-css:2px 3px orange, -1px 0 8px olive;
}}
@media not print {.dark.dark.navigation.secondary-theme:not(#\\\\9){
@media not print {.dark.dark.navigation.secondary-theme:not(#\\\\9), html.secondary-theme.secondary-theme .dark.navigation:not(#\\\\9){
--fontFamilyBase-css:\\"Helvetica Neue\\", Arial, sans-serif;
--fontFamilyBody-css:\\"Helvetica Neue\\", Arial, sans-serif;
--black-css:purple;
Expand All @@ -145,7 +145,7 @@ exports[`with baseThemeId attaches one style node containing overrides with the
--containerShadowBase-css:2px 3px orange, -1px 0 8px olive;
--modalShadowContainer-css:2px 3px orange, -1px 0 8px olive;
}}
.disabled-motion.disabled-motion.secondary-theme .navigation:not(#\\\\9){
.disabled-motion.disabled-motion.secondary-theme .navigation:not(#\\\\9), html.secondary-theme.secondary-theme .disabled-motion .navigation:not(#\\\\9){
--fontFamilyBase-css:\\"Helvetica Neue\\", Arial, sans-serif;
--fontFamilyBody-css:\\"Helvetica Neue\\", Arial, sans-serif;
--black-css:purple;
Expand All @@ -157,7 +157,7 @@ exports[`with baseThemeId attaches one style node containing overrides with the
--containerShadowBase-css:2px 3px orange, -1px 0 8px olive;
--modalShadowContainer-css:2px 3px orange, -1px 0 8px olive;
}
.disabled-motion.disabled-motion.navigation.secondary-theme:not(#\\\\9){
.disabled-motion.disabled-motion.navigation.secondary-theme:not(#\\\\9), html.secondary-theme.secondary-theme .disabled-motion.navigation:not(#\\\\9){
--fontFamilyBase-css:\\"Helvetica Neue\\", Arial, sans-serif;
--fontFamilyBody-css:\\"Helvetica Neue\\", Arial, sans-serif;
--black-css:purple;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ exports[`Build-time theming of main theme with matching baseThemeId generates th
--color-background-button-primary-active-h8she0:#dd6baa;
}
.dark-mode.secondary:not(#\\\\9) {
.dark-mode.secondary:not(#\\\\9), html.secondary .dark-mode:not(#\\\\9) {
--color-background-button-primary-default-cqohzr:#dd6baa;
--color-background-button-primary-active-h8she0:#ec72aa;
}"
Expand All @@ -49,7 +49,7 @@ exports[`Build-time theming of secondary theme generates the correct css files 1
--color-background-button-primary-active-h8she0:#dd6baa;
}
.dark-mode.secondary:not(#\\\\9) {
.dark-mode.secondary:not(#\\\\9), html.secondary .dark-mode:not(#\\\\9) {
--color-background-button-primary-default-l567s3:#bbbbbb;
--color-background-button-primary-active-h8she0:#ec72aa;
}"
Expand Down
5 changes: 4 additions & 1 deletion src/build/tasks/postcss/modules.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
export function markGlobal(selector: string): string {
return `:global(${selector})`;
const split = selector.split(',').map((singleSelector) => {
return `:global(${singleSelector})`;
});
return split.join(',');
}
20 changes: 10 additions & 10 deletions src/shared/declaration/__tests__/__snapshots__/index.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ exports[`renderDeclarations includes secondary theme 1`] = `
--black-css:purple;
--brown-css:black;
}
@media not print {.dark.secondary-theme{
@media not print {.dark.secondary-theme, html.secondary-theme .dark{
--shadow-css:purple;
--buttonShadow-css:purple;
--boxShadow-css:black;
Expand All @@ -67,18 +67,18 @@ exports[`renderDeclarations includes secondary theme 1`] = `
--buttonShadow-css:purple;
--lineShadow-css:purple;
}
.navigation.secondary-theme{
.navigation.secondary-theme, html.secondary-theme .navigation{
--shadow-css:purple;
--buttonShadow-css:purple;
--lineShadow-css:purple;
}
@media not print {.dark.secondary-theme .navigation{
@media not print {.dark.secondary-theme .navigation, html.secondary-theme .dark .navigation{
--shadow-css:grey;
--buttonShadow-css:grey;
--boxShadow-css:black;
--lineShadow-css:black;
}}
@media not print {.dark.navigation.secondary-theme{
@media not print {.dark.navigation.secondary-theme, html.secondary-theme .dark.navigation{
--shadow-css:grey;
--buttonShadow-css:grey;
--lineShadow-css:black;
Expand Down Expand Up @@ -151,16 +151,16 @@ exports[`renderDeclarations renders declarations for theme with non :root select
--containerShadowBase-css:2px 3px orange, -1px 0 8px olive;
--modalShadowContainer-css:2px 3px orange, -1px 0 8px olive;
}
.compact.secondary-theme{
.compact.secondary-theme, html.secondary-theme .compact{
--scaledSize-css:1px;
}
@media not print {.dark.secondary-theme{
@media not print {.dark.secondary-theme, html.secondary-theme .dark{
--shadow-css:purple;
--buttonShadow-css:purple;
--boxShadow-css:black;
--lineShadow-css:black;
}}
.disabled-motion.secondary-theme{
.disabled-motion.secondary-theme, html.secondary-theme .disabled-motion{
--appear-css:0;
}
.secondary-theme .navigation{
Expand All @@ -169,19 +169,19 @@ exports[`renderDeclarations renders declarations for theme with non :root select
--boxShadow-css:purple;
--lineShadow-css:purple;
}
.navigation.secondary-theme{
.navigation.secondary-theme, html.secondary-theme .navigation{
--shadow-css:purple;
--buttonShadow-css:purple;
--boxShadow-css:purple;
--lineShadow-css:purple;
}
@media not print {.dark.secondary-theme .navigation{
@media not print {.dark.secondary-theme .navigation, html.secondary-theme .dark .navigation{
--shadow-css:grey;
--buttonShadow-css:grey;
--boxShadow-css:black;
--lineShadow-css:black;
}}
@media not print {.dark.navigation.secondary-theme{
@media not print {.dark.navigation.secondary-theme, html.secondary-theme .dark.navigation{
--shadow-css:grey;
--buttonShadow-css:grey;
--boxShadow-css:black;
Expand Down
20 changes: 13 additions & 7 deletions src/shared/declaration/__tests__/selector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,34 @@ describe('Selector', () => {
});

test('creates selector for theme', () => {
expect(selector.for({ global: ['.theme'] })).toEqual('.theme');
expect(selector.for({ theme: ['.theme'] })).toEqual('.theme');
});

test('creates selector for mode', () => {
expect(selector.for({ global: ['.theme', '.mode'] })).toEqual('.mode.theme');
expect(selector.for({ theme: ['.theme'], modeAndContext: ['.mode'] })).toEqual('.mode.theme, html.theme .mode');
});

test('creates selector for context', () => {
expect(selector.for({ global: ['.theme'], local: ['.context'] })).toEqual('.theme .context');
expect(selector.for({ global: [':root'], local: ['.context'] })).toEqual('.context');
expect(selector.for({ theme: ['.theme'], local: ['.context'] })).toEqual('.theme .context');
expect(selector.for({ theme: [':root'], local: ['.context'] })).toEqual('.context');
});

test('creates selector for context within mode', () => {
expect(selector.for({ global: ['.theme', '.mode'], local: ['.context'] })).toEqual('.mode.theme .context');
expect(selector.for({ theme: ['.theme'], modeAndContext: ['.mode'], local: ['.context'] })).toEqual(
'.mode.theme .context, html.theme .mode .context'
);
});

test('creates selector for mode with root selector', () => {
expect(selector.for({ global: [':root', '.mode'], local: ['.context'] })).toEqual('.mode .context');
expect(selector.for({ theme: [':root'], modeAndContext: ['.mode'], local: ['.context'] })).toEqual(
'.mode .context'
);
});

test('customizes each selector when multiple', () => {
const selector = new Selector((sel) => `${sel}:not(.theme)`);
expect(selector.for({ global: [':root', '.mode'], local: ['.context'] })).toEqual('.mode .context:not(.theme)');
expect(selector.for({ theme: [':root'], modeAndContext: ['.mode'], local: ['.context'] })).toEqual(
'.mode .context:not(.theme)'
);
});
});
36 changes: 21 additions & 15 deletions src/shared/declaration/multi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,20 +60,21 @@ export class MultiThemeCreator extends AbstractCreator implements StylesheetCrea
appendRulesForSecondary(stylesheet: Stylesheet, primary: Theme, secondary: Theme) {
const secondaryResolution = resolveTheme(secondary);
const defaults = reduce(secondaryResolution, secondary, defaultsReducer());
const rootRule = this.ruleCreator.create({ global: [secondary.selector] }, defaults);
const parentRule = this.findRule(stylesheet, { global: [primary.selector] });
const rootRule = this.ruleCreator.create({ theme: [secondary.selector] }, defaults);
const parentRule = this.findRule(stylesheet, { theme: [primary.selector] });
MultiThemeCreator.appendRuleToStylesheet(stylesheet, rootRule, compact([parentRule]));

MultiThemeCreator.forEachOptionalModeState(secondary, (mode, state) => {
const optionalState = mode.states[state] as OptionalState;
const modeResolution = reduce(secondaryResolution, secondary, modeReducer(mode, state));
const modeRule = this.ruleCreator.create(
{ global: [secondary.selector, optionalState.selector], media: optionalState.media },
{ theme: [secondary.selector], modeAndContext: [optionalState.selector], media: optionalState.media },
modeResolution
);
const parentModeRule = stylesheet.findRule(
this.ruleCreator.selectorFor({
global: [primary.selector, optionalState.selector],
modeAndContext: [optionalState.selector],
theme: [primary.selector],
})
);
MultiThemeCreator.appendRuleToStylesheet(stylesheet, modeRule, compact([rootRule, parentModeRule, parentRule]));
Expand All @@ -82,12 +83,12 @@ export class MultiThemeCreator extends AbstractCreator implements StylesheetCrea
MultiThemeCreator.forEachContext(secondary, (context) => {
const contextResolution = reduce(resolveContext(secondary, context), secondary, defaultsReducer());
const contextRule = this.ruleCreator.create(
{ global: [secondary.selector], local: [context.selector] },
{ theme: [secondary.selector], local: [context.selector] },
contextResolution
);
const parentContextRule = stylesheet.findRule(
this.ruleCreator.selectorFor({
global: [primary.selector],
theme: [primary.selector],
local: [context.selector],
})
);
Expand All @@ -98,7 +99,7 @@ export class MultiThemeCreator extends AbstractCreator implements StylesheetCrea
);

const contextRuleGlobal = this.ruleCreator.create(
{ global: [secondary.selector, context.selector] },
{ modeAndContext: [context.selector], theme: [secondary.selector] },
contextResolution
);
MultiThemeCreator.appendRuleToStylesheet(
Expand All @@ -111,30 +112,34 @@ export class MultiThemeCreator extends AbstractCreator implements StylesheetCrea
MultiThemeCreator.forEachContextWithinOptionalModeState(secondary, (context, mode, state) => {
const optionalState = mode.states[state] as OptionalState;
const contextResolution = reduce(resolveContext(secondary, context), secondary, modeReducer(mode, state));
const contextRule = this.findRule(stylesheet, { global: [secondary.selector], local: [context.selector] });
const contextRule = this.findRule(stylesheet, { theme: [secondary.selector], local: [context.selector] });
const modeRule = this.findRule(stylesheet, {
global: [secondary.selector, optionalState.selector],
theme: [secondary.selector],
modeAndContext: [optionalState.selector],
});
const contextAndModeRule = this.ruleCreator.create(
{
global: [secondary.selector, optionalState.selector],
modeAndContext: [optionalState.selector],
theme: [secondary.selector],
local: [context.selector],
media: optionalState.media,
},
contextResolution
);
const parentContextRule = stylesheet.findRule(
this.ruleCreator.selectorFor({ global: [primary.selector], local: [context.selector] })
this.ruleCreator.selectorFor({ theme: [primary.selector], local: [context.selector] })
);

const parentModeRule = stylesheet.findRule(
this.ruleCreator.selectorFor({
global: [primary.selector, optionalState.selector],
modeAndContext: [optionalState.selector],
theme: [primary.selector],
})
);
const parentContextAndModeRule = stylesheet.findRule(
this.ruleCreator.selectorFor({
global: [primary.selector, optionalState.selector],
modeAndContext: [optionalState.selector],
theme: [primary.selector],
local: [context.selector],
})
);
Expand All @@ -154,12 +159,13 @@ export class MultiThemeCreator extends AbstractCreator implements StylesheetCrea
);

const parentContextAndModeRuleGlobal = stylesheet.findRule(
this.ruleCreator.selectorFor({ global: [secondary.selector, context.selector] })
this.ruleCreator.selectorFor({ theme: [secondary.selector], modeAndContext: [context.selector] })
);

const contextAndModeRuleGlobal = this.ruleCreator.create(
{
global: [secondary.selector, optionalState.selector, context.selector],
modeAndContext: [optionalState.selector, context.selector],
theme: [secondary.selector],
media: optionalState.media,
},
contextResolution
Expand Down
3 changes: 2 additions & 1 deletion src/shared/declaration/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { entries } from '../utils';
import { SpecificResolution } from '../theme';

export interface SelectorConfig {
global: string[];
theme: string[];
modeAndContext?: string[];
local?: string[];
media?: string;
}
Expand Down
26 changes: 19 additions & 7 deletions src/shared/declaration/selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import type { SelectorCustomizer } from './interfaces';

interface SelectorParams {
global: string[];
theme: string[];
modeAndContext?: string[];
local?: string[];
}

Expand All @@ -14,16 +15,27 @@ export class Selector {
this.customizer = customizer;
}

for({ global, local }: SelectorParams): string {
if (global.length === 1 && !local?.length && global[0] === ':root') {
// Function to generate .theme -> .mode/context -> .local seletor, it returns:
// ".themeORmode/context .local" OR ".theme.mode/context .local, html.theme .mode/context .local"
for({ theme, modeAndContext = [], local }: SelectorParams): string {
if ([...theme, ...modeAndContext].length === 1 && !local?.length && theme[0] === ':root') {
// :root is only applied alone
return this.customizer(':root');
}
const globalWithoutRoot = global.filter((f) => f !== ':root');
const themeWithoutRoot = theme.filter((f) => f !== ':root');

let selector = this.toSelector(globalWithoutRoot);
if (local?.length) {
selector += ` ${this.toSelector(local)}`;
let selector = this.toSelector([...themeWithoutRoot, ...modeAndContext]);
const localSelector = local?.length ? ` ${this.toSelector(local)}` : '';
selector += localSelector;

// Only when .theme and mode/context both exist, we need additional "html.theme .modeORcontext .local" selector
// Because .theme can be in <html> or <body> while .mode/context can be only in <body>
if (themeWithoutRoot.length && modeAndContext.length) {
selector = [
selector,
`html${this.toSelector(themeWithoutRoot)} ${this.toSelector(modeAndContext)}` + localSelector,
].join(', ');
return this.customizer(selector.trim());
}

return this.customizer(selector.trim());
Expand Down
Loading

0 comments on commit 5d38d0c

Please sign in to comment.