From 46fb64c3d768fce59e513e5af0f66647871baf2f Mon Sep 17 00:00:00 2001 From: Oleksandr Fediashov Date: Tue, 6 Sep 2022 15:27:14 +0200 Subject: [PATCH 1/6] feat: add makeResetStyles to @griffel/core --- ...-40e0578a-53de-40b8-9ed2-958d4fe70437.json | 7 + e2e/typescript/src/assets/fixture-reset.ts | 321 ++++++++++++++++++ e2e/typescript/src/test.mjs | 1 + packages/core/src/constants.ts | 3 + packages/core/src/index.ts | 10 +- packages/core/src/makeResetStyles.test.ts | 36 ++ packages/core/src/makeResetStyles.ts | 35 ++ .../renderer/getStyleSheetForBucket.test.ts | 1 + .../src/renderer/getStyleSheetForBucket.ts | 2 + .../runtime/resolveResetStyleRules.test.ts | 188 ++++++++++ .../src/runtime/resolveResetStyleRules.ts | 122 +++++++ packages/core/src/types.ts | 64 +++- 12 files changed, 783 insertions(+), 7 deletions(-) create mode 100644 change/@griffel-core-40e0578a-53de-40b8-9ed2-958d4fe70437.json create mode 100644 e2e/typescript/src/assets/fixture-reset.ts create mode 100644 packages/core/src/makeResetStyles.test.ts create mode 100644 packages/core/src/makeResetStyles.ts create mode 100644 packages/core/src/runtime/resolveResetStyleRules.test.ts create mode 100644 packages/core/src/runtime/resolveResetStyleRules.ts diff --git a/change/@griffel-core-40e0578a-53de-40b8-9ed2-958d4fe70437.json b/change/@griffel-core-40e0578a-53de-40b8-9ed2-958d4fe70437.json new file mode 100644 index 000000000..ac37f9620 --- /dev/null +++ b/change/@griffel-core-40e0578a-53de-40b8-9ed2-958d4fe70437.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "feat: add makeResetStyles to @griffel/core", + "packageName": "@griffel/core", + "email": "olfedias@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/e2e/typescript/src/assets/fixture-reset.ts b/e2e/typescript/src/assets/fixture-reset.ts new file mode 100644 index 000000000..7f9beaba7 --- /dev/null +++ b/e2e/typescript/src/assets/fixture-reset.ts @@ -0,0 +1,321 @@ +import { GriffelResetStyle } from '@griffel/core'; + +function assertType(style: GriffelResetStyle): GriffelResetStyle { + return style; +} + +// Animation +assertType({ animationName: 'foo' }); + +// Basic styles +// + +assertType({ flex: 0 }); +assertType({ flex: 1 }); +assertType({ zIndex: 0 }); +assertType({ zIndex: 1 }); + +assertType({ padding: '5px' }); +assertType({ paddingLeft: '5px' }); +assertType({ color: 'beige' }); + +// CSS variables +// + +assertType({ fontWeight: 'var(--foo)' }); +assertType({ flexShrink: 'var(--bar)' }); +assertType({ opacity: 'var(--baz)' }); +assertType({ zIndex: 'var(--qux)' }); + +assertType({ '--color': 'red' }); + +// Mixins +// + +assertType({ + ...{ padding: '5px' }, + ...{ paddingLeft: '5px' }, + ...{ color: 'red' }, +}); + +// Strict selector defined via "CSS.Pseudos" +// + +assertType({ + ':hover': { flexShrink: 0 }, + ':focus': { flexShrink: 'initial' }, + ':active': { zIndex: 0 }, + ':visited': { zIndex: 1 }, +}); + +assertType({ + ':hover': { fontWeight: 'var(--foo)' }, + ':focus': { flexShrink: 'var(--bar)' }, + ':active': { opacity: 'var(--bar)' }, + ':visited': { zIndex: 'var(--qux)' }, +}); + +assertType({ + ':hover': { color: 'beige' }, + ':active': { padding: '5px' }, + ':focus': { paddingLeft: '5px' }, +}); + +assertType({ + ':hover': { '--color': 'red' }, +}); + +// Custom selectors +// + +assertType({ + ':hover:focus': { flexShrink: 0 }, + ':hover:active': { flexShrink: 'initial' }, + ':hover:visited': { zIndex: 0 }, + ':hover:focus-visible': { zIndex: 1 }, +}); + +assertType({ + ':link:hover': { fontWeight: 'var(--foo)' }, + ':link:focus': { flexShrink: 'var(--bar)' }, + ':link:active': { opacity: 'var(--bar)' }, + ':link:visited': { zIndex: 'var(--qux)' }, +}); + +assertType({ + '.bar': { color: 'beige' }, + '.foo': { padding: '5px' }, + '.qux': { paddingLeft: '5px' }, +}); + +assertType({ + '.bar': { '--color': 'red' }, +}); + +// Nested custom selectors +// + +assertType({ + '.foo': { + '.bar': { flexShrink: 0 }, + '.baz': { flexShrink: 'initial' }, + '.qux': { opacity: 0 }, + '.fred': { zIndex: 0 }, + '.thud': { zIndex: 1 }, + }, + '.bar': { + '.baz': { color: 'beige' }, + '.qux': { paddingLeft: '5px' }, + }, + '.baz': { + '.qux': { + '--color': 'red', + }, + }, + '.qux': { + '.bar': { flexShrink: 'var(--bar)' }, + '.baz': { opacity: 'var(--baz)' }, + }, +}); + +// Invalid values +// + +assertType({ + // @ts-expect-error "outline-box" is an invalid value for "box-sizing" + boxSizing: 'outline-box', +}); +assertType({ + // @ts-expect-error "1" is invalid value for "overflowX" + overflowX: '1', +}); +assertType({ + // @ts-expect-error "paddingLeft" cannot be numeric value + paddingLeft: 5, +}); +assertType({ + // @ts-expect-error "0" is invalid value for "color" + color: 0, +}); + +assertType({ + // @ts-expect-error type check still fails on outline-box, not on any other line + boxSizing: 'outline-box', + zIndex: 1, +}); + +assertType({ + // @ts-expect-error Object is not assignable to CSS property + zIndex: { color: 'red' }, + // @ts-expect-error Object is not assignable to CSS property + opacity: { color: 'red' }, +}); + +// Mixins with invalid values +// + +// @ts-expect-error Object in "zIndex" is not assignable to CSS property +assertType({ + ...{ zIndex: { color: 'red' } }, + ...{ color: 'red' }, +}); +// @ts-expect-error "outline-box" in "boxSizing" is an invalid value for "box-sizing" +assertType({ + ...{ boxSizing: 'outline-box' }, + ...{ color: 'red' }, +}); + +// Just a type check, deep objects are not expected to be used as style mixins? +const typedMixin: GriffelResetStyle = { + marginLeft: '5px', + ':hover': { + marginLeft: '6px', + '--customColor': 'blue', + }, + '--customColor': 'silver', +}; + +assertType({ + ...typedMixin, + color: 'var(--customColor)', +}); + +assertType({ + '@media screen and (max-width: 992px)': { + ...typedMixin, + }, +}); + +// Strict selectors +// + +assertType({ + ':hover': { + // @ts-expect-error "1" is invalid value for "overflowX" + overflowX: '1', + // @ts-expect-error "paddingLeft" cannot be numeric value + paddingLeft: 5, + }, +}); +assertType({ + ':hover': { + // @ts-expect-error outline-box is an invalid value for box-sizing + boxSizing: 'outline-box', + zIndex: 1, + }, +}); + +// Custom selectors +// + +assertType({ + // @ts-expect-error "1" is invalid value for "overflowX" + ':hover:focus': { + overflowX: '1', + }, +}); + +assertType({ + // @ts-expect-error "paddingLeft" cannot be numeric value + ':hover:focus': { + paddingLeft: 5, + }, +}); + +assertType({ + // @ts-expect-error "1" is invalid value for "overflowX", padding is banned, paddingLeft cannot be a numeric value + ':hover:focus': { + overflowX: 'scroll', + padding: 0, + paddingLeft: 5, + }, +}); + +assertType({ + ':hover:focus': { + // @ts-expect-error Object is not assignable to CSS property + zIndex: { color: 'red' }, + opacity: { color: 'red' }, // < no error here, TS only reports the first error in this case + }, + ':hover:active': { + // @ts-expect-error Object is not assignable to CSS property + opacity: { color: 'red' }, + zIndex: { color: 'red' }, // < no error here, TS only reports the first error in this case + }, +}); + +// Nested custom selectors +// + +assertType({ + // @ts-expect-error "1" is invalid value for "overflowX", padding is banned, paddingLeft cannot be a numeric value + '.foo': { + '.baz': { + overflowX: '1', + padding: 0, + paddingLeft: 5, + }, + }, +}); +assertType({ + // @ts-expect-error outline-box is an invalid value for box-sizing + '.foo': { + boxSizing: 'outline-box', + + '.bar': { + boxSizing: 'outline-box', // < no error here, TS only reports the error for the whole object + }, + }, +}); +assertType({ + // @ts-expect-error outline-box is an invalid value for box-sizing + '.foo': { + boxSizing: 'outline-box', + zIndex: 1, + + '.bar': { + boxSizing: 'outline-box', // < no error here, TS only reports the error for the whole object + zIndex: 1, + }, + }, +}); +assertType({ + '.foo': { + // @ts-expect-error Object is not assignable to CSS property + zIndex: { color: 'red' }, + opacity: { color: 'red' }, // < no error here, TS only reports the first error in this case + + '.bar': { + zIndex: { color: 'red' }, // < no error here, TS only reports the first error in this case + opacity: { color: 'red' }, // < no error here, TS only reports the first error in this case + }, + }, +}); + +// Fallback values +assertType({ + color: ['red', 'blue'], + padding: [0, '2px'], + paddingLeft: [0, '2px'], + zIndex: [10, 20], + ':hover': { + color: ['red', 'blue'], + }, + ':hover:active': { + zIndex: [2], + color: ['red', 'blue'], + paddingLeft: [0, '2px'], + }, +}); + +assertType({ + paddingLeft: [ + // @ts-expect-error "paddingLeft" cannot be numeric value + 2, + '2px', + ], + // @ts-expect-error "paddingLeft" cannot be numeric value + ':hover:active': { + paddingLeft: ['2px', 2], + }, +}); diff --git a/e2e/typescript/src/test.mjs b/e2e/typescript/src/test.mjs index dc0769151..4bc793356 100644 --- a/e2e/typescript/src/test.mjs +++ b/e2e/typescript/src/test.mjs @@ -77,6 +77,7 @@ async function performTest(tsVersion) { console.log(logSymbols.success, 'Package files were packed'); await fs.promises.copyFile(path.resolve(assetsPath, 'fixture.ts'), path.join(tempDir, 'fixture.ts')); + await fs.promises.copyFile(path.resolve(assetsPath, 'fixture-reset.ts'), path.join(tempDir, 'fixture-reset.ts')); await fs.promises.copyFile(path.resolve(assetsPath, 'tsconfig.fixture.json'), path.join(tempDir, 'tsconfig.json')); // Reuse the same cache directory to speed up install and avoid network requests diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index 21414f56e..0fc7f6274 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -6,6 +6,9 @@ export const DATA_BUCKET_ATTR = 'data-make-styles-bucket'; /** @internal */ export const HASH_PREFIX = 'f'; +/** @internal */ +export const RESET_HASH_PREFIX = 'rf'; + /** @internal */ export const SEQUENCE_HASH_LENGTH = 7; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index aa817dbf9..447328f51 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -47,19 +47,23 @@ export type { CreateDOMRendererOptions } from './renderer/createDOMRenderer'; export { rehydrateRendererCache } from './renderer/rehydrateRendererCache'; export { mergeClasses } from './mergeClasses'; -export { makeStaticStyles } from './makeStaticStyles'; export { makeStyles } from './makeStyles'; +export { makeStaticStyles } from './makeStaticStyles'; +export { makeResetStyles } from './makeResetStyles'; + export { resolveStyleRulesForSlots } from './resolveStyleRulesForSlots'; // Private exports, are used by build time transforms or other tools export { __css } from './__css'; +export { __styles } from './__styles'; + export { normalizeCSSBucketEntry } from './runtime/utils/normalizeCSSBucketEntry'; export { styleBucketOrdering } from './renderer/getStyleSheetForBucket'; export { defaultCompareMediaQueries } from './renderer/createDOMRenderer'; export { getStyleBucketName } from './runtime/getStyleBucketName'; export { reduceToClassNameForSlots } from './runtime/reduceToClassNameForSlots'; export { resolveStyleRules } from './runtime/resolveStyleRules'; -export { __styles } from './__styles'; +export { resolveResetStyleRules } from './runtime/resolveResetStyleRules'; export * from './constants'; export type { @@ -69,6 +73,8 @@ export type { // Styles GriffelAnimation, GriffelStyle, + // Reset styles + GriffelResetStyle, // Internal types CSSClasses, CSSClassesMapBySlot, diff --git a/packages/core/src/makeResetStyles.test.ts b/packages/core/src/makeResetStyles.test.ts new file mode 100644 index 000000000..8d91f4cd9 --- /dev/null +++ b/packages/core/src/makeResetStyles.test.ts @@ -0,0 +1,36 @@ +import { griffelRendererSerializer } from './common/snapshotSerializers'; +import { createDOMRenderer } from './renderer/createDOMRenderer'; +import { makeResetStyles } from './makeResetStyles'; +import { GriffelRenderer } from './types'; + +expect.addSnapshotSerializer(griffelRendererSerializer); + +describe('makeResetStyles', () => { + let renderer: GriffelRenderer; + + beforeEach(() => { + process.env.NODE_ENV = 'production'; + renderer = createDOMRenderer(document); + }); + + afterEach(() => { + document.head.innerHTML = ''; + }); + + it('returns a single classname for a single style', () => { + const computeClassName = makeResetStyles({ + color: 'red', + flexDirection: 'row', + }); + + expect(computeClassName({ dir: 'ltr', renderer })).toEqual('rf7lmmpp'); + expect(renderer).toMatchInlineSnapshot(` + .rf7lmmpp { + color: red; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + } + `); + }); +}); diff --git a/packages/core/src/makeResetStyles.ts b/packages/core/src/makeResetStyles.ts new file mode 100644 index 000000000..717b28f35 --- /dev/null +++ b/packages/core/src/makeResetStyles.ts @@ -0,0 +1,35 @@ +import { resolveResetStyleRules } from './runtime/resolveResetStyleRules'; +import type { GriffelResetStyle, MakeStylesOptions } from './types'; + +/** + * @internal + */ +export function makeResetStyles(styles: GriffelResetStyle) { + const insertionCache: Record = {}; + + let ltrClassName: string | null = null; + let rtlClassName: string | null = null; + + let cssRules: string[] | null = null; + + function computeClassName(options: MakeStylesOptions): string { + const { dir, renderer } = options; + + if (ltrClassName === null) { + [ltrClassName, rtlClassName, cssRules] = resolveResetStyleRules(styles); + } + + const isLTR = dir === 'ltr'; + // As RTL classes are different they should have a different cache key for insertion + const rendererId = isLTR ? renderer.id : renderer.id + 'r'; + + if (insertionCache[rendererId] === undefined) { + renderer.insertCSSRules({ r: cssRules! }); + insertionCache[rendererId] = true; + } + + return isLTR ? ltrClassName : rtlClassName || ltrClassName; + } + + return computeClassName; +} diff --git a/packages/core/src/renderer/getStyleSheetForBucket.test.ts b/packages/core/src/renderer/getStyleSheetForBucket.test.ts index 187611189..d71a14dc5 100644 --- a/packages/core/src/renderer/getStyleSheetForBucket.test.ts +++ b/packages/core/src/renderer/getStyleSheetForBucket.test.ts @@ -14,6 +14,7 @@ describe('getStyleSheetForBucket', () => { const target = createFakeDocument(); const renderer = createDOMRenderer(); + getStyleSheetForBucket('r', target, renderer); getStyleSheetForBucket('l', target, renderer); getStyleSheetForBucket('d', target, renderer); getStyleSheetForBucket('v', target, renderer); diff --git a/packages/core/src/renderer/getStyleSheetForBucket.ts b/packages/core/src/renderer/getStyleSheetForBucket.ts index 3abc69c8a..d93789c9d 100644 --- a/packages/core/src/renderer/getStyleSheetForBucket.ts +++ b/packages/core/src/renderer/getStyleSheetForBucket.ts @@ -8,6 +8,8 @@ import { createIsomorphicStyleSheet } from './createIsomorphicStyleSheet'; * @internal */ export const styleBucketOrdering: StyleBucketName[] = [ + // reset styles + 'r', // catch-all 'd', // link diff --git a/packages/core/src/runtime/resolveResetStyleRules.test.ts b/packages/core/src/runtime/resolveResetStyleRules.test.ts new file mode 100644 index 000000000..7b6a7a4f0 --- /dev/null +++ b/packages/core/src/runtime/resolveResetStyleRules.test.ts @@ -0,0 +1,188 @@ +import { resolveResetStyleRules } from './resolveResetStyleRules'; + +describe('resolveResetStyleRules', () => { + it('handles base rules', () => { + const result = resolveResetStyleRules({ + color: 'red', + overflowX: 'hidden', + }); + + expect(result).toMatchInlineSnapshot(` + Array [ + "rf11y0rml", + null, + Array [ + ".rf11y0rml{color:red;overflow-x:hidden;}", + ], + ] + `); + }); + + it('handles RTL', () => { + const result = resolveResetStyleRules({ marginLeft: '15px' }); + + expect(result).toMatchInlineSnapshot(` + Array [ + "rfovwgyn", + "rfj5b9iu", + Array [ + ".rfovwgyn{margin-left:15px;}", + ".rfj5b9iu{margin-right:15px;}", + ], + ] + `); + }); + + it('handles fallback values', () => { + const result = resolveResetStyleRules({ + color: ['red', 'blue'], + }); + + expect(result).toMatchInlineSnapshot(` + Array [ + "rfj1urkn", + null, + Array [ + ".rfj1urkn{color:red;color:blue;}", + ], + ] + `); + }); + + it('handles :global() selector', () => { + expect( + resolveResetStyleRules({ + color: 'red', + ':global(body)': { color: 'magenta' }, + }), + ).toMatchInlineSnapshot(` + Array [ + "rfzlpwqs", + null, + Array [ + ".rfzlpwqs{color:red;}", + "body .rfzlpwqs{color:magenta;}", + ], + ] + `); + + expect( + resolveResetStyleRules({ + ':global(body)': { + color: 'magenta', + ':focus': { color: 'pink' }, + }, + }), + ).toMatchInlineSnapshot(` + Array [ + "rf1i1zh9k", + null, + Array [ + "body .rf1i1zh9k{color:magenta;}", + "body .rf1i1zh9k:focus{color:pink;}", + ], + ] + `); + + expect( + resolveResetStyleRules({ + ':global(.fui-FluentProvider)': { + '& .foo': { color: 'orange' }, + }, + }), + ).toMatchInlineSnapshot(` + Array [ + "rfmi35r5", + null, + Array [ + ".fui-FluentProvider .rfmi35r5 .foo{color:orange;}", + ], + ] + `); + }); + + it('handles media queries', () => { + const result = resolveResetStyleRules({ + color: 'red', + '@media (forced-colors: active)': { + color: 'orange', + ':focus': { + color: 'yellow', + }, + }, + }); + + expect(result).toMatchInlineSnapshot(` + Array [ + "rfpycl1b", + null, + Array [ + ".rfpycl1b{color:red;}", + "@media (forced-colors: active){.rfpycl1b{color:orange;}.rfpycl1b:focus{color:yellow;}}", + ], + ] + `); + }); + + it('handles layer queries', () => { + const result = resolveResetStyleRules({ + '@layer utilities': { + color: 'orange', + ':focus': { + color: 'yellow', + }, + }, + }); + + expect(result).toMatchInlineSnapshot(` + Array [ + "rfvhnavh", + null, + Array [ + "@layer utilities{color:orange;:focus{color:yellow;}}", + ], + ] + `); + }); + + it('handles support queries', () => { + const result = resolveResetStyleRules({ + '@supports (display: flex)': { + color: 'orange', + ':focus': { + color: 'yellow', + }, + }, + }); + + expect(result).toMatchInlineSnapshot(` + Array [ + "rfxf8lon", + null, + Array [ + "@supports (display: flex){.rfxf8lon{color:orange;}.rfxf8lon:focus{color:yellow;}}", + ], + ] + `); + }); + + it('handles nested selectors', () => { + const result = resolveResetStyleRules({ + ':hover': { color: 'red' }, + '& :focus': { color: 'red' }, + '&.foo': { color: 'red' }, + }); + + expect(result).toMatchInlineSnapshot(` + Array [ + "rf1s1f2pl", + null, + Array [ + ".rf1s1f2pl:hover{color:red;}", + ".rf1s1f2pl :focus{color:red;}", + ".rf1s1f2pl.foo{color:red;}", + ], + ] + `); + }); +}); diff --git a/packages/core/src/runtime/resolveResetStyleRules.ts b/packages/core/src/runtime/resolveResetStyleRules.ts new file mode 100644 index 000000000..eccd37dbf --- /dev/null +++ b/packages/core/src/runtime/resolveResetStyleRules.ts @@ -0,0 +1,122 @@ +import hashString from '@emotion/hash'; +import { convertProperty } from 'rtl-css-js/core'; + +import { RESET_HASH_PREFIX } from '../constants'; +import { GriffelStyle, GriffelResetStyle } from '../types'; +import { compileCSSRules, normalizePseudoSelector } from './compileCSS'; +import { isMediaQuerySelector } from './utils/isMediaQuerySelector'; +import { isLayerSelector } from './utils/isLayerSelector'; +import { isNestedSelector } from './utils/isNestedSelector'; +import { isSupportQuerySelector } from './utils/isSupportQuerySelector'; +import { isObject } from './utils/isObject'; +import { hyphenateProperty } from './utils/hyphenateProperty'; + +/** + * @internal + */ +export function createStringFromStyles(styles: GriffelResetStyle) { + let ltrCSS = ''; + let rtlCSS = ''; + + // eslint-disable-next-line guard-for-in + for (const property in styles) { + const value = styles[property as keyof GriffelStyle]; + + // eslint-disable-next-line eqeqeq + if (value == null) { + continue; + } + + if (typeof value === 'string' || typeof value === 'number') { + const { key: rtlProperty, value: rtlValue } = convertProperty(property, value); + + ltrCSS += `${hyphenateProperty(property)}:${value};`; + rtlCSS += `${hyphenateProperty(rtlProperty)}:${rtlValue};`; + + continue; + } + + if (property === 'animationName') { + // TODO: handle animations + throw new Error(); + } + + if (Array.isArray(value)) { + // not animationName property but array in the value => fallback values + if (value.length === 0) { + if (process.env.NODE_ENV !== 'production') { + console.warn( + `makeResetStyles(): An empty array was passed as input to "${property}", the property will be omitted in the styles.`, + ); + } + continue; + } + + const rtlDefinitions = value.map(v => convertProperty(property, v!)); + const rtlPropertyConsistent = !rtlDefinitions.some(v => v.key !== rtlDefinitions[0].key); + + if (!rtlPropertyConsistent) { + if (process.env.NODE_ENV !== 'production') { + console.error( + 'makeStyles(): mixing CSS fallback values which result in multiple CSS properties in RTL is not supported.', + ); + } + continue; + } + + const rtlProperty = rtlDefinitions[0].key; + + ltrCSS += value.map(v => `${hyphenateProperty(property)}:${v};`).join(''); + rtlCSS += rtlDefinitions.map(definition => `${hyphenateProperty(rtlProperty)}:${definition.value};`).join(''); + + continue; + } + + if (isObject(value)) { + if (isNestedSelector(property)) { + const nestedSelector = normalizePseudoSelector(property); + const [ltrNested, rtlNested] = createStringFromStyles(value); + + ltrCSS += `${nestedSelector}{${ltrNested}}`; + rtlCSS += `${nestedSelector}{${rtlNested}}`; + + continue; + } + + if (isMediaQuerySelector(property) || isLayerSelector(property) || isSupportQuerySelector(property)) { + const [ltrNested, rtlNested] = createStringFromStyles(value); + + ltrCSS += `${property}{${ltrNested}}`; + rtlCSS += `${property}{${rtlNested}}`; + + continue; + } + } + + if (process.env.NODE_ENV !== 'production') { + // eslint-disable-next-line no-console + console.error(`Please fix the unresolved style rule: \n ${property} \n ${JSON.stringify(value, null, 2)}"`); + } + } + + return [ltrCSS, rtlCSS]; +} + +/** + * @internal + */ +export function resolveResetStyleRules(styles: GriffelResetStyle): [string, string | null, string[]] { + const [ltrRule, rtlRule] = createStringFromStyles(styles); + + const ltrClassName = RESET_HASH_PREFIX + hashString(ltrRule); + const ltrCSS = compileCSSRules(`.${ltrClassName}{${ltrRule}}`); + + if (ltrRule === rtlRule) { + return [ltrClassName, null, ltrCSS]; + } + + const rtlClassName = RESET_HASH_PREFIX + hashString(rtlRule); + const rtlCSS = compileCSSRules(`.${rtlClassName}{${rtlRule}}`); + + return [ltrClassName, rtlClassName, ltrCSS.concat(rtlCSS)]; +} diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index ccff5fc44..ff9260fbb 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -151,13 +151,10 @@ type GriffelStylesCSSObjectCustomL5 = { } & GriffelStylesStrictCSSObject; export type GriffelStyle = GriffelStylesStrictCSSObject | GriffelStylesCSSObjectCustomL1; - export type GriffelAnimation = Record<'from' | 'to' | string, GriffelStylesCSSObjectCustomL1>; -export interface MakeStylesOptions { - dir: 'ltr' | 'rtl'; - renderer: GriffelRenderer; -} +// Types for makeStaticStyles() +// --- export type GriffelStaticStyle = { [key: string]: CSS.Properties & @@ -180,6 +177,61 @@ export type GriffelStaticStyle = { }; export type GriffelStaticStyles = GriffelStaticStyle | string; +// Types for makeResetStyles() +// --- + +type GriffelResetStylesCSSProperties = Omit< + CSS.PropertiesFallback>, + // We have custom definition for "animationName" + 'animationName' +>; + +type GriffelResetStylesStrictCSSObject = GriffelResetStylesCSSProperties & + GriffelResetStylesCSSPseudos & { + animationName?: GriffelAnimation | GriffelAnimation[] | CSS.Property.Animation; + }; + +type GriffelResetStylesCSSPseudos = { + [Property in CSS.Pseudos]?: + | (GriffelResetStylesStrictCSSObject & { content?: string | string[] }) + | (GriffelResetStylesCSSObjectCustomL1 & { content?: string | string[] }); +}; + +// +// "GriffelStylesCSSObjectCustom*" is a workaround to avoid circular references in types that are breaking TS <4. +// Once we will support "typesVersions" (types downleleving) or update our requirements for TS this should be +// updated or removed. +// + +type GriffelResetStylesCSSObjectCustomL1 = { + [Property: string]: string | number | (string | number)[] | undefined | GriffelResetStylesCSSObjectCustomL2; +} & GriffelResetStylesStrictCSSObject; + +type GriffelResetStylesCSSObjectCustomL2 = { + [Property: string]: string | number | (string | number)[] | undefined | GriffelResetStylesCSSObjectCustomL3; +} & GriffelResetStylesStrictCSSObject; + +type GriffelResetStylesCSSObjectCustomL3 = { + [Property: string]: string | number | (string | number)[] | undefined | GriffelResetStylesCSSObjectCustomL4; +} & GriffelResetStylesStrictCSSObject; + +type GriffelResetStylesCSSObjectCustomL4 = { + [Property: string]: string | number | (string | number)[] | undefined | GriffelResetStylesCSSObjectCustomL5; +} & GriffelResetStylesStrictCSSObject; + +type GriffelResetStylesCSSObjectCustomL5 = { + [Property: string]: string | number | (string | number)[] | undefined | GriffelResetStylesStrictCSSObject; +} & GriffelResetStylesStrictCSSObject; + +export type GriffelResetStyle = GriffelResetStylesStrictCSSObject | GriffelResetStylesCSSObjectCustomL1; + +// --- + +export interface MakeStylesOptions { + dir: 'ltr' | 'rtl'; + renderer: GriffelRenderer; +} + export interface MakeStaticStylesOptions { renderer: GriffelRenderer; } @@ -238,6 +290,8 @@ export type CSSClassesMap = Record; export type CSSClassesMapBySlot = Record; export type CSSRulesByBucket = { + // reset + r?: CSSBucketEntry[]; // default d?: CSSBucketEntry[]; // link From 259ce171e1575c23674cff8c619be18d6fdb93ff Mon Sep 17 00:00:00 2001 From: Oleksandr Fediashov Date: Tue, 4 Oct 2022 10:03:24 +0200 Subject: [PATCH 2/6] Update change/@griffel-core-40e0578a-53de-40b8-9ed2-958d4fe70437.json Co-authored-by: Bernardo Sunderhus --- change/@griffel-core-40e0578a-53de-40b8-9ed2-958d4fe70437.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/change/@griffel-core-40e0578a-53de-40b8-9ed2-958d4fe70437.json b/change/@griffel-core-40e0578a-53de-40b8-9ed2-958d4fe70437.json index ac37f9620..ba5a2fe52 100644 --- a/change/@griffel-core-40e0578a-53de-40b8-9ed2-958d4fe70437.json +++ b/change/@griffel-core-40e0578a-53de-40b8-9ed2-958d4fe70437.json @@ -1,5 +1,5 @@ { - "type": "patch", + "type": "minor", "comment": "feat: add makeResetStyles to @griffel/core", "packageName": "@griffel/core", "email": "olfedias@microsoft.com", From d12d11f8793e7502dfda6949ffbfebec2b23ff08 Mon Sep 17 00:00:00 2001 From: Oleksandr Fediashov Date: Tue, 4 Oct 2022 10:12:24 +0200 Subject: [PATCH 3/6] add a test case for nested queries --- .../runtime/resolveResetStyleRules.test.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/core/src/runtime/resolveResetStyleRules.test.ts b/packages/core/src/runtime/resolveResetStyleRules.test.ts index 7b6a7a4f0..0a767390c 100644 --- a/packages/core/src/runtime/resolveResetStyleRules.test.ts +++ b/packages/core/src/runtime/resolveResetStyleRules.test.ts @@ -166,6 +166,28 @@ describe('resolveResetStyleRules', () => { `); }); + it('handles nested queries queries', () => { + const result = resolveResetStyleRules({ + '@supports (display: flex)': { + color: 'pink', + + '@media (forced-colors: active)': { + color: 'orange', + }, + }, + }); + + expect(result).toMatchInlineSnapshot(` + Array [ + "rfhd25ja", + null, + Array [ + "@supports (display: flex){.rfhd25ja{color:pink;}@media (forced-colors: active){.rfhd25ja{color:orange;}}}", + ], + ] + `); + }); + it('handles nested selectors', () => { const result = resolveResetStyleRules({ ':hover': { color: 'red' }, From a487682caf7e14414190a2d75c6724500e78b6fd Mon Sep 17 00:00:00 2001 From: Oleksandr Fediashov Date: Tue, 4 Oct 2022 11:37:47 +0200 Subject: [PATCH 4/6] add a fixture for an object --- e2e/typescript/src/assets/fixture-reset.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/e2e/typescript/src/assets/fixture-reset.ts b/e2e/typescript/src/assets/fixture-reset.ts index 7f9beaba7..1ba44ff00 100644 --- a/e2e/typescript/src/assets/fixture-reset.ts +++ b/e2e/typescript/src/assets/fixture-reset.ts @@ -143,6 +143,11 @@ assertType({ boxSizing: 'outline-box', zIndex: 1, }); +assertType({ + zIndex: 1, + // @ts-expect-error type check still fails on outline-box, not on any other line + boxSizing: 'outline-box', +}); assertType({ // @ts-expect-error Object is not assignable to CSS property From 8676f41c09f8f2f9f788abbd9170d0f7f365835b Mon Sep 17 00:00:00 2001 From: Oleksandr Fediashov Date: Tue, 4 Oct 2022 12:03:58 +0200 Subject: [PATCH 5/6] use a single char --- packages/core/src/constants.ts | 2 +- packages/core/src/makeResetStyles.test.ts | 4 +- .../runtime/resolveResetStyleRules.test.ts | 56 +++++++++---------- 3 files changed, 31 insertions(+), 31 deletions(-) diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index 0fc7f6274..77f71ea3c 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -7,7 +7,7 @@ export const DATA_BUCKET_ATTR = 'data-make-styles-bucket'; export const HASH_PREFIX = 'f'; /** @internal */ -export const RESET_HASH_PREFIX = 'rf'; +export const RESET_HASH_PREFIX = 'r'; /** @internal */ export const SEQUENCE_HASH_LENGTH = 7; diff --git a/packages/core/src/makeResetStyles.test.ts b/packages/core/src/makeResetStyles.test.ts index 8d91f4cd9..65c463d28 100644 --- a/packages/core/src/makeResetStyles.test.ts +++ b/packages/core/src/makeResetStyles.test.ts @@ -23,9 +23,9 @@ describe('makeResetStyles', () => { flexDirection: 'row', }); - expect(computeClassName({ dir: 'ltr', renderer })).toEqual('rf7lmmpp'); + expect(computeClassName({ dir: 'ltr', renderer })).toEqual('r7lmmpp'); expect(renderer).toMatchInlineSnapshot(` - .rf7lmmpp { + .r7lmmpp { color: red; -webkit-flex-direction: row; -ms-flex-direction: row; diff --git a/packages/core/src/runtime/resolveResetStyleRules.test.ts b/packages/core/src/runtime/resolveResetStyleRules.test.ts index 0a767390c..8d313c302 100644 --- a/packages/core/src/runtime/resolveResetStyleRules.test.ts +++ b/packages/core/src/runtime/resolveResetStyleRules.test.ts @@ -9,10 +9,10 @@ describe('resolveResetStyleRules', () => { expect(result).toMatchInlineSnapshot(` Array [ - "rf11y0rml", + "r11y0rml", null, Array [ - ".rf11y0rml{color:red;overflow-x:hidden;}", + ".r11y0rml{color:red;overflow-x:hidden;}", ], ] `); @@ -23,11 +23,11 @@ describe('resolveResetStyleRules', () => { expect(result).toMatchInlineSnapshot(` Array [ - "rfovwgyn", - "rfj5b9iu", + "rovwgyn", + "rj5b9iu", Array [ - ".rfovwgyn{margin-left:15px;}", - ".rfj5b9iu{margin-right:15px;}", + ".rovwgyn{margin-left:15px;}", + ".rj5b9iu{margin-right:15px;}", ], ] `); @@ -40,10 +40,10 @@ describe('resolveResetStyleRules', () => { expect(result).toMatchInlineSnapshot(` Array [ - "rfj1urkn", + "rj1urkn", null, Array [ - ".rfj1urkn{color:red;color:blue;}", + ".rj1urkn{color:red;color:blue;}", ], ] `); @@ -57,11 +57,11 @@ describe('resolveResetStyleRules', () => { }), ).toMatchInlineSnapshot(` Array [ - "rfzlpwqs", + "rzlpwqs", null, Array [ - ".rfzlpwqs{color:red;}", - "body .rfzlpwqs{color:magenta;}", + ".rzlpwqs{color:red;}", + "body .rzlpwqs{color:magenta;}", ], ] `); @@ -75,11 +75,11 @@ describe('resolveResetStyleRules', () => { }), ).toMatchInlineSnapshot(` Array [ - "rf1i1zh9k", + "r1i1zh9k", null, Array [ - "body .rf1i1zh9k{color:magenta;}", - "body .rf1i1zh9k:focus{color:pink;}", + "body .r1i1zh9k{color:magenta;}", + "body .r1i1zh9k:focus{color:pink;}", ], ] `); @@ -92,10 +92,10 @@ describe('resolveResetStyleRules', () => { }), ).toMatchInlineSnapshot(` Array [ - "rfmi35r5", + "rmi35r5", null, Array [ - ".fui-FluentProvider .rfmi35r5 .foo{color:orange;}", + ".fui-FluentProvider .rmi35r5 .foo{color:orange;}", ], ] `); @@ -114,11 +114,11 @@ describe('resolveResetStyleRules', () => { expect(result).toMatchInlineSnapshot(` Array [ - "rfpycl1b", + "rpycl1b", null, Array [ - ".rfpycl1b{color:red;}", - "@media (forced-colors: active){.rfpycl1b{color:orange;}.rfpycl1b:focus{color:yellow;}}", + ".rpycl1b{color:red;}", + "@media (forced-colors: active){.rpycl1b{color:orange;}.rpycl1b:focus{color:yellow;}}", ], ] `); @@ -136,7 +136,7 @@ describe('resolveResetStyleRules', () => { expect(result).toMatchInlineSnapshot(` Array [ - "rfvhnavh", + "rvhnavh", null, Array [ "@layer utilities{color:orange;:focus{color:yellow;}}", @@ -157,10 +157,10 @@ describe('resolveResetStyleRules', () => { expect(result).toMatchInlineSnapshot(` Array [ - "rfxf8lon", + "rxf8lon", null, Array [ - "@supports (display: flex){.rfxf8lon{color:orange;}.rfxf8lon:focus{color:yellow;}}", + "@supports (display: flex){.rxf8lon{color:orange;}.rxf8lon:focus{color:yellow;}}", ], ] `); @@ -179,10 +179,10 @@ describe('resolveResetStyleRules', () => { expect(result).toMatchInlineSnapshot(` Array [ - "rfhd25ja", + "rhd25ja", null, Array [ - "@supports (display: flex){.rfhd25ja{color:pink;}@media (forced-colors: active){.rfhd25ja{color:orange;}}}", + "@supports (display: flex){.rhd25ja{color:pink;}@media (forced-colors: active){.rhd25ja{color:orange;}}}", ], ] `); @@ -197,12 +197,12 @@ describe('resolveResetStyleRules', () => { expect(result).toMatchInlineSnapshot(` Array [ - "rf1s1f2pl", + "r1s1f2pl", null, Array [ - ".rf1s1f2pl:hover{color:red;}", - ".rf1s1f2pl :focus{color:red;}", - ".rf1s1f2pl.foo{color:red;}", + ".r1s1f2pl:hover{color:red;}", + ".r1s1f2pl :focus{color:red;}", + ".r1s1f2pl.foo{color:red;}", ], ] `); From f74c276200bfd394743c7856a9ae3011fbef3547 Mon Sep 17 00:00:00 2001 From: Oleksandr Fediashov Date: Tue, 4 Oct 2022 12:21:52 +0200 Subject: [PATCH 6/6] remove an export --- packages/core/src/runtime/resolveResetStyleRules.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/runtime/resolveResetStyleRules.ts b/packages/core/src/runtime/resolveResetStyleRules.ts index eccd37dbf..ee3173c42 100644 --- a/packages/core/src/runtime/resolveResetStyleRules.ts +++ b/packages/core/src/runtime/resolveResetStyleRules.ts @@ -14,7 +14,7 @@ import { hyphenateProperty } from './utils/hyphenateProperty'; /** * @internal */ -export function createStringFromStyles(styles: GriffelResetStyle) { +function createStringFromStyles(styles: GriffelResetStyle) { let ltrCSS = ''; let rtlCSS = '';