From 396c63daf07fd8f837335dd5d9eb1a6332a1c917 Mon Sep 17 00:00:00 2001 From: Sebastian Sebald Date: Tue, 5 Apr 2022 11:38:57 +0200 Subject: [PATCH 01/24] starting --- packages/system/src/component.tsx | 15 +++++++++++++++ packages/system/src/components.test.ts | 11 +++++++++++ 2 files changed, 26 insertions(+) create mode 100644 packages/system/src/component.tsx create mode 100644 packages/system/src/components.test.ts diff --git a/packages/system/src/component.tsx b/packages/system/src/component.tsx new file mode 100644 index 0000000000..1664ca1c06 --- /dev/null +++ b/packages/system/src/component.tsx @@ -0,0 +1,15 @@ +/** + * 1. transform object to "variant string" + * 2. pass to Box' variant prop + */ + +// const Button = ({}) => { +// const styles = f({ component: 'Button', size: 'small', variant: 'primary' }); // base + size + variant + +// return ; +// }; + +export const useStyleConfig = ( + componentName: string, + { size, variant, state } +) => {}; diff --git a/packages/system/src/components.test.ts b/packages/system/src/components.test.ts new file mode 100644 index 0000000000..cd357408cd --- /dev/null +++ b/packages/system/src/components.test.ts @@ -0,0 +1,11 @@ +test('get base styles for a component'); +test('get variant styles for a component'); +test('get size styles for a component'); +test('get state styles for a component'); + +test('override order: base < variant < size < state'); + +// example 'Button.state.hover' => 'Button:hover' +test('allow to transform styles'); + +test('usage with '); From 36c452fee5ae074f3cd26f47fb42a727c09bfa40 Mon Sep 17 00:00:00 2001 From: Sebastian Sebald Date: Tue, 5 Apr 2022 13:09:29 +0200 Subject: [PATCH 02/24] update fn signature --- .../src/{component.tsx => component.ts} | 19 ++++++++++++++++++- ...components.test.ts => components.test.tsx} | 10 +++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) rename packages/system/src/{component.tsx => component.ts} (52%) rename packages/system/src/{components.test.ts => components.test.tsx} (64%) diff --git a/packages/system/src/component.tsx b/packages/system/src/component.ts similarity index 52% rename from packages/system/src/component.tsx rename to packages/system/src/component.ts index 1664ca1c06..88db567773 100644 --- a/packages/system/src/component.tsx +++ b/packages/system/src/component.ts @@ -9,7 +9,24 @@ // return ; // }; +export type ComponentState = + | 'hover' + | 'focus' + | 'active' + | 'visited' + | 'disabled' + | 'readOnly' + | 'error' + | 'checked' + | 'indeterminate'; + +export interface UseStyleConfigProps { + variant?: string; + size?: string; + state?: ComponentState; +} + export const useStyleConfig = ( componentName: string, - { size, variant, state } + props?: UseStyleConfigProps = {} ) => {}; diff --git a/packages/system/src/components.test.ts b/packages/system/src/components.test.tsx similarity index 64% rename from packages/system/src/components.test.ts rename to packages/system/src/components.test.tsx index cd357408cd..78aaaa6880 100644 --- a/packages/system/src/components.test.ts +++ b/packages/system/src/components.test.tsx @@ -1,3 +1,5 @@ +import { useStyleConfig } from './component'; + test('get base styles for a component'); test('get variant styles for a component'); test('get size styles for a component'); @@ -6,6 +8,12 @@ test('get state styles for a component'); test('override order: base < variant < size < state'); // example 'Button.state.hover' => 'Button:hover' -test('allow to transform styles'); +test('transform state styles'); + +test('support multiple parts'); test('usage with '); + +const Component = () => { + useStyleConfig('name'); +}; From e746c7146e971feefb2fa8a77c07328c95ed8f88 Mon Sep 17 00:00:00 2001 From: Sebastian Sebald Date: Tue, 5 Apr 2022 13:55:50 +0200 Subject: [PATCH 03/24] with parts --- packages/system/src/component.ts | 13 ++++++++++--- packages/system/src/components.test.tsx | 4 ++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/system/src/component.ts b/packages/system/src/component.ts index 88db567773..c46dc8d4d0 100644 --- a/packages/system/src/component.ts +++ b/packages/system/src/component.ts @@ -20,13 +20,20 @@ export type ComponentState = | 'checked' | 'indeterminate'; -export interface UseStyleConfigProps { +export interface ComponentStylesProps { variant?: string; size?: string; state?: ComponentState; } -export const useStyleConfig = ( +export interface ComponentStylesOptions { + parts?: string[]; +} + +export const useComponentStyles = ( componentName: string, - props?: UseStyleConfigProps = {} + props?: ComponentStylesProps = {}, + options: ComponentStylesOptions = {} ) => {}; + +// useRef for perf diff --git a/packages/system/src/components.test.tsx b/packages/system/src/components.test.tsx index 78aaaa6880..8c5323b95b 100644 --- a/packages/system/src/components.test.tsx +++ b/packages/system/src/components.test.tsx @@ -1,4 +1,4 @@ -import { useStyleConfig } from './component'; +import { useComponentStyles } from './component'; test('get base styles for a component'); test('get variant styles for a component'); @@ -15,5 +15,5 @@ test('support multiple parts'); test('usage with '); const Component = () => { - useStyleConfig('name'); + useComponentStyles('name'); }; From f76093c92a40a4cf4cea8c5fca7d03715ec946a4 Mon Sep 17 00:00:00 2001 From: Sebastian Sebald Date: Tue, 5 Apr 2022 21:07:57 +0200 Subject: [PATCH 04/24] hmm --- packages/system/src/component.ts | 39 +++++++++++++++++++++++-- packages/system/src/components.test.tsx | 4 ++- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/packages/system/src/component.ts b/packages/system/src/component.ts index c46dc8d4d0..bd6d9805b5 100644 --- a/packages/system/src/component.ts +++ b/packages/system/src/component.ts @@ -30,10 +30,43 @@ export interface ComponentStylesOptions { parts?: string[]; } -export const useComponentStyles = ( +export type ComponentStyles = { + [P in T[number]]: string[]; +}; + +export function useComponentStyles( + componentName: string, + props?: ComponentStylesProps, + options?: { + parts?: never; + } +): string[]; + +export function useComponentStyles( + componentName: string, + props?: ComponentStylesProps, + options?: { + parts: Parts; + } +): ComponentStyles; + +export function useComponentStyles( componentName: string, props?: ComponentStylesProps = {}, - options: ComponentStylesOptions = {} -) => {}; + options?: any = {} +) { + // Just some PoC that the overloads work + if (options.parts) { + return { + [options.parts[0]]: ['asd'], + [options.parts[1]]: ['asd'], + }; + } + + return ['foo']; +} // useRef for perf + +// Q: if we get styles from the theme and deep merge directly, +// can we avoid using `variant` and instead pass it to `__baseCSS`/`css` directly? diff --git a/packages/system/src/components.test.tsx b/packages/system/src/components.test.tsx index 8c5323b95b..ddab664436 100644 --- a/packages/system/src/components.test.tsx +++ b/packages/system/src/components.test.tsx @@ -15,5 +15,7 @@ test('support multiple parts'); test('usage with '); const Component = () => { - useComponentStyles('name'); + const r = useComponentStyles('name'); + const f = useComponentStyles('name', {}, { parts: ['wrapper', 'icon'] }); + console.log(f.wrapper, f.icon, f.name); }; From 0273e4ca2e74d05755a0e6a8090c0e79c47b06f4 Mon Sep 17 00:00:00 2001 From: Sebastian Sebald Date: Wed, 6 Apr 2022 09:33:13 +0200 Subject: [PATCH 05/24] yay types work! --- packages/system/src/component.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/system/src/component.ts b/packages/system/src/component.ts index bd6d9805b5..0927104ab8 100644 --- a/packages/system/src/component.ts +++ b/packages/system/src/component.ts @@ -26,11 +26,7 @@ export interface ComponentStylesProps { state?: ComponentState; } -export interface ComponentStylesOptions { - parts?: string[]; -} - -export type ComponentStyles = { +export type ComponentStyles> = { [P in T[number]]: string[]; }; @@ -38,17 +34,22 @@ export function useComponentStyles( componentName: string, props?: ComponentStylesProps, options?: { - parts?: never; + parts: never; } ): string[]; -export function useComponentStyles( +export function useComponentStyles< + Part extends string, + Parts extends ReadonlyArray +>( componentName: string, props?: ComponentStylesProps, options?: { parts: Parts; } -): ComponentStyles; +): { + [P in Parts[number]]: string[]; +}; export function useComponentStyles( componentName: string, From f780408e531e974a1c304903da0200fd399f5b95 Mon Sep 17 00:00:00 2001 From: Sebastian Sebald Date: Wed, 6 Apr 2022 09:41:35 +0200 Subject: [PATCH 06/24] return css object? --- packages/system/src/component.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/system/src/component.ts b/packages/system/src/component.ts index 0927104ab8..ea63d840eb 100644 --- a/packages/system/src/component.ts +++ b/packages/system/src/component.ts @@ -1,3 +1,4 @@ +import { CSSObject } from './types'; /** * 1. transform object to "variant string" * 2. pass to Box' variant prop @@ -26,17 +27,13 @@ export interface ComponentStylesProps { state?: ComponentState; } -export type ComponentStyles> = { - [P in T[number]]: string[]; -}; - export function useComponentStyles( componentName: string, props?: ComponentStylesProps, options?: { parts: never; } -): string[]; +): CSSObject; export function useComponentStyles< Part extends string, @@ -48,7 +45,7 @@ export function useComponentStyles< parts: Parts; } ): { - [P in Parts[number]]: string[]; + [P in Parts[number]]: CSSObject; }; export function useComponentStyles( @@ -59,12 +56,12 @@ export function useComponentStyles( // Just some PoC that the overloads work if (options.parts) { return { - [options.parts[0]]: ['asd'], - [options.parts[1]]: ['asd'], + [options.parts[0]]: {}, + [options.parts[1]]: {}, }; } - return ['foo']; + return {}; } // useRef for perf From 64611a89acaa05297ca2a2e7bb32cf189b6438be Mon Sep 17 00:00:00 2001 From: Sebastian Sebald Date: Wed, 6 Apr 2022 09:42:44 +0200 Subject: [PATCH 07/24] update --- packages/system/src/component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/system/src/component.ts b/packages/system/src/component.ts index ea63d840eb..fa0300b64f 100644 --- a/packages/system/src/component.ts +++ b/packages/system/src/component.ts @@ -68,3 +68,4 @@ export function useComponentStyles( // Q: if we get styles from the theme and deep merge directly, // can we avoid using `variant` and instead pass it to `__baseCSS`/`css` directly? +// -> Make we can remove variant from `` altogether then? (since this hook is the better appraoch) From 89f30f76a49dbad2ec7bdb3403dd35bc3a0c8f9b Mon Sep 17 00:00:00 2001 From: Sebastian Sebald Date: Wed, 6 Apr 2022 09:48:31 +0200 Subject: [PATCH 08/24] add test cases --- packages/system/src/components.test.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/system/src/components.test.tsx b/packages/system/src/components.test.tsx index ddab664436..bd07d7711a 100644 --- a/packages/system/src/components.test.tsx +++ b/packages/system/src/components.test.tsx @@ -1,18 +1,23 @@ import { useComponentStyles } from './component'; test('get base styles for a component'); +test('get base styles for a component (with parts)'); test('get variant styles for a component'); +test('get variant styles for a component (with parts)'); test('get size styles for a component'); +test('get size styles for a component (with parts)'); test('get state styles for a component'); +test('get state styles for a component (with parts)'); test('override order: base < variant < size < state'); +test('override order: base < variant < size < state (with parts)'); // example 'Button.state.hover' => 'Button:hover' test('transform state styles'); - -test('support multiple parts'); +test('transform state styles (with parts)'); test('usage with '); +test('usage with (with parts)'); const Component = () => { const r = useComponentStyles('name'); From e04bc05084a45b2e1e614d5446d33b7cf14fe1e9 Mon Sep 17 00:00:00 2001 From: Sebastian Sebald Date: Wed, 6 Apr 2022 14:46:06 +0200 Subject: [PATCH 09/24] rename --- .../system/src/useComponentStyles.test.tsx | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 packages/system/src/useComponentStyles.test.tsx diff --git a/packages/system/src/useComponentStyles.test.tsx b/packages/system/src/useComponentStyles.test.tsx new file mode 100644 index 0000000000..1d921239c4 --- /dev/null +++ b/packages/system/src/useComponentStyles.test.tsx @@ -0,0 +1,26 @@ +import { useComponentStyles } from './useComponentStyles'; + +test('get base styles for a component'); +test('get base styles for a component (with parts)'); +test('get variant styles for a component'); +test('get variant styles for a component (with parts)'); +test('get size styles for a component'); +test('get size styles for a component (with parts)'); +test('get state styles for a component'); +test('get state styles for a component (with parts)'); + +test('override order: base < variant < size < state'); +test('override order: base < variant < size < state (with parts)'); + +// example 'Button.state.hover' => 'Button:hover' +test('transform state styles'); +test('transform state styles (with parts)'); + +test('usage with '); +test('usage with (with parts)'); + +const Component = () => { + const r = useComponentStyles('name'); + const f = useComponentStyles('name', {}, { parts: ['wrapper', 'icon'] }); + console.log(f.wrapper, f.icon, f.name); +}; From 567eefea7699853a392bf093f8c170b07e64b96d Mon Sep 17 00:00:00 2001 From: Sebastian Sebald Date: Wed, 6 Apr 2022 14:48:04 +0200 Subject: [PATCH 10/24] ups --- packages/system/src/components.test.tsx | 26 ------------------- .../{component.ts => useComponentStyles.ts} | 0 2 files changed, 26 deletions(-) delete mode 100644 packages/system/src/components.test.tsx rename packages/system/src/{component.ts => useComponentStyles.ts} (100%) diff --git a/packages/system/src/components.test.tsx b/packages/system/src/components.test.tsx deleted file mode 100644 index bd07d7711a..0000000000 --- a/packages/system/src/components.test.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { useComponentStyles } from './component'; - -test('get base styles for a component'); -test('get base styles for a component (with parts)'); -test('get variant styles for a component'); -test('get variant styles for a component (with parts)'); -test('get size styles for a component'); -test('get size styles for a component (with parts)'); -test('get state styles for a component'); -test('get state styles for a component (with parts)'); - -test('override order: base < variant < size < state'); -test('override order: base < variant < size < state (with parts)'); - -// example 'Button.state.hover' => 'Button:hover' -test('transform state styles'); -test('transform state styles (with parts)'); - -test('usage with '); -test('usage with (with parts)'); - -const Component = () => { - const r = useComponentStyles('name'); - const f = useComponentStyles('name', {}, { parts: ['wrapper', 'icon'] }); - console.log(f.wrapper, f.icon, f.name); -}; diff --git a/packages/system/src/component.ts b/packages/system/src/useComponentStyles.ts similarity index 100% rename from packages/system/src/component.ts rename to packages/system/src/useComponentStyles.ts From 387cbebb51f8835bad56bfe7063a829cac00e8b9 Mon Sep 17 00:00:00 2001 From: Sebastian Sebald Date: Wed, 6 Apr 2022 15:29:53 +0200 Subject: [PATCH 11/24] woot woot --- packages/system/package.json | 3 +- .../system/src/useComponentStyles.test.tsx | 182 +++++++++++++++--- packages/system/src/useComponentStyles.ts | 25 ++- pnpm-lock.yaml | 2 + 4 files changed, 185 insertions(+), 27 deletions(-) diff --git a/packages/system/package.json b/packages/system/package.json index b7a4d1c062..3fb35d77e3 100644 --- a/packages/system/package.json +++ b/packages/system/package.json @@ -29,7 +29,8 @@ "@emotion/react": "11.8.2", "@marigold/types": "workspace:*", "@theme-ui/css": "0.14.2", - "csstype": "3.0.11" + "csstype": "3.0.11", + "deepmerge": "^4.2.2" }, "peerDependencies": { "react": "^16.x || ^17.0.0", diff --git a/packages/system/src/useComponentStyles.test.tsx b/packages/system/src/useComponentStyles.test.tsx index 1d921239c4..4f7736818f 100644 --- a/packages/system/src/useComponentStyles.test.tsx +++ b/packages/system/src/useComponentStyles.test.tsx @@ -1,26 +1,162 @@ +import React from 'react'; +import { renderHook } from '@testing-library/react-hooks'; +import { ThemeProvider } from './useTheme'; + import { useComponentStyles } from './useComponentStyles'; -test('get base styles for a component'); -test('get base styles for a component (with parts)'); -test('get variant styles for a component'); -test('get variant styles for a component (with parts)'); -test('get size styles for a component'); -test('get size styles for a component (with parts)'); -test('get state styles for a component'); -test('get state styles for a component (with parts)'); - -test('override order: base < variant < size < state'); -test('override order: base < variant < size < state (with parts)'); - -// example 'Button.state.hover' => 'Button:hover' -test('transform state styles'); -test('transform state styles (with parts)'); - -test('usage with '); -test('usage with (with parts)'); - -const Component = () => { - const r = useComponentStyles('name'); - const f = useComponentStyles('name', {}, { parts: ['wrapper', 'icon'] }); - console.log(f.wrapper, f.icon, f.name); +// Setup +// --------------- +const theme = { + colors: { + primary: '#0070f3', + secondary: '#ff4081', + white: '#fff', + black: '#000', + }, + space: { + none: 0, + 'small-1': 4, + 'medium-1': 8, + 'large-1': 16, + }, + sizes: { + none: 0, + 'small-1': 16, + 'medium-1': 32, + 'large-1': 48, + }, + components: { + // Component without parts + Button: { + base: { + appearance: 'none', + bg: 'white', + }, + size: { + small: { + height: 'small-1', + }, + medium: { + height: 'medium-1', + }, + large: { + height: 'large-1', + }, + }, + variant: { + primary: { + color: 'primary', + }, + secondary: { + color: 'secondary', + }, + }, + }, + // Component with multiple parts + Checkbox: { + base: { + container: {}, + icon: {}, + label: {}, + }, + state: { + checked: { + container: {}, + icon: {}, + label: {}, + }, + unchecked: { + container: {}, + icon: {}, + label: {}, + }, + indeterminate: { + container: {}, + icon: {}, + label: {}, + }, + error: { + container: {}, + icon: {}, + label: {}, + }, + }, + size: { + small: { + container: {}, + icon: {}, + label: {}, + }, + medium: { + container: {}, + icon: {}, + label: {}, + }, + large: { + container: {}, + icon: {}, + label: {}, + }, + }, + }, + }, }; + +const wrapper: React.FC = ({ children }) => ( + {children} +); + +// Tests +// --------------- +test('get base styles for a component', () => { + const { result } = renderHook(() => useComponentStyles('Button'), { + wrapper, + }); + expect(result.current).toMatchInlineSnapshot(` + { + "appearance": "none", + "bg": "white", + } + `); + expect(result.current).toMatchInlineSnapshot(` + { + "appearance": "none", + "bg": "white", + } + `); +}); + +test('get variant styles for a component', () => { + const { result } = renderHook( + () => useComponentStyles('Button', { variant: 'primary' }), + { + wrapper, + } + ); +}); + +// test('works if variant does not exist'); + +// test('get size styles for a component'); +// test('get state styles for a component'); + +// test('get base styles for a component (with parts)'); +// test('get variant styles for a component (with parts)'); +// test('get size styles for a component (with parts)'); +// test('get state styles for a component (with parts)'); + +// test('override order: base < variant < size < state'); +// test('override order: base < variant < size < state (with parts)'); + +// // example 'Button.state.hover' => 'Button:hover' +// test('transform state styles'); +// test('transform state styles (with parts)'); + +// test('usage with '); +// test('usage with (with parts)'); + +// const Component = () => { +// const r = useComponentStyles('name'); +// const f = useComponentStyles('name', {}, { parts: ['wrapper', 'icon'] }); +// console.log(f.wrapper, f.icon, f.name); +// }; diff --git a/packages/system/src/useComponentStyles.ts b/packages/system/src/useComponentStyles.ts index fa0300b64f..9ab54a8d96 100644 --- a/packages/system/src/useComponentStyles.ts +++ b/packages/system/src/useComponentStyles.ts @@ -1,4 +1,8 @@ +import merge from 'deepmerge'; + import { CSSObject } from './types'; +import { useTheme } from './useTheme'; + /** * 1. transform object to "variant string" * 2. pass to Box' variant prop @@ -10,6 +14,15 @@ import { CSSObject } from './types'; // return ; // }; +// Helper +// --------------- +const get = (obj: object, path: string | string[]): any => { + const keys = typeof path === 'string' ? path.split('.') : path; + return keys.reduce((acc, key) => acc && (acc as any)[key], obj); +}; + +// Types +// --------------- export type ComponentState = | 'hover' | 'focus' @@ -50,9 +63,12 @@ export function useComponentStyles< export function useComponentStyles( componentName: string, - props?: ComponentStylesProps = {}, - options?: any = {} + props: ComponentStylesProps = {}, + options: any = {} ) { + const { theme } = useTheme(); + const styles = get(theme, `components.${componentName}`); + // Just some PoC that the overloads work if (options.parts) { return { @@ -61,7 +77,10 @@ export function useComponentStyles( }; } - return {}; + return merge( + styles.base, + props.variant ? styles?.variant?.[props.variant] ?? {} : {} + ); } // useRef for perf diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d6062675ed..c85be5f3a1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -335,12 +335,14 @@ importers: '@marigold/types': workspace:* '@theme-ui/css': 0.14.2 csstype: 3.0.11 + deepmerge: ^4.2.2 tsup: 5.12.4 dependencies: '@emotion/react': 11.8.2_react@17.0.2 '@marigold/types': link:../types '@theme-ui/css': 0.14.2_@emotion+react@11.8.2 csstype: 3.0.11 + deepmerge: 4.2.2 devDependencies: '@marigold/tsconfig': link:../../config/tsconfig tsup: 5.12.4_typescript@4.6.3 From 843854043624485fc804d85d6705f044040dedb8 Mon Sep 17 00:00:00 2001 From: Sebastian Sebald Date: Wed, 6 Apr 2022 21:38:48 +0200 Subject: [PATCH 12/24] better --- packages/system/package.json | 3 +- .../system/src/useComponentStyles.test.tsx | 26 ++++++-- packages/system/src/useComponentStyles.ts | 63 +++++++++---------- pnpm-lock.yaml | 2 + 4 files changed, 55 insertions(+), 39 deletions(-) diff --git a/packages/system/package.json b/packages/system/package.json index 3fb35d77e3..f5128befbe 100644 --- a/packages/system/package.json +++ b/packages/system/package.json @@ -30,7 +30,8 @@ "@marigold/types": "workspace:*", "@theme-ui/css": "0.14.2", "csstype": "3.0.11", - "deepmerge": "^4.2.2" + "deepmerge": "^4.2.2", + "react-fast-compare": "^3.2.0" }, "peerDependencies": { "react": "^16.x || ^17.0.0", diff --git a/packages/system/src/useComponentStyles.test.tsx b/packages/system/src/useComponentStyles.test.tsx index 4f7736818f..5356a4df2b 100644 --- a/packages/system/src/useComponentStyles.test.tsx +++ b/packages/system/src/useComponentStyles.test.tsx @@ -118,21 +118,36 @@ test('get base styles for a component', () => { "bg": "white", } `); - expect(result.current).toMatchInlineSnapshot(` +}); + +test('get variant styles for a component', () => { + let view = renderHook( + () => useComponentStyles('Button', { variant: 'primary' }), + { + wrapper, + } + ); + expect(view.result.current).toMatchInlineSnapshot(` { "appearance": "none", "bg": "white", + "color": "primary", } `); -}); -test('get variant styles for a component', () => { - const { result } = renderHook( - () => useComponentStyles('Button', { variant: 'primary' }), + view = renderHook( + () => useComponentStyles('Button', { variant: 'secondary' }), { wrapper, } ); + expect(view.result.current).toMatchInlineSnapshot(` + { + "appearance": "none", + "bg": "white", + "color": "secondary", + } + `); }); // test('works if variant does not exist'); @@ -145,6 +160,7 @@ test('get variant styles for a component', () => { // test('get size styles for a component (with parts)'); // test('get state styles for a component (with parts)'); +// test('base styles are always added'); // test('override order: base < variant < size < state'); // test('override order: base < variant < size < state (with parts)'); diff --git a/packages/system/src/useComponentStyles.ts b/packages/system/src/useComponentStyles.ts index 9ab54a8d96..1f5c52f970 100644 --- a/packages/system/src/useComponentStyles.ts +++ b/packages/system/src/useComponentStyles.ts @@ -1,25 +1,23 @@ import merge from 'deepmerge'; +import { useRef } from 'react'; +import isEqual from 'react-fast-compare'; import { CSSObject } from './types'; import { useTheme } from './useTheme'; -/** - * 1. transform object to "variant string" - * 2. pass to Box' variant prop - */ - -// const Button = ({}) => { -// const styles = f({ component: 'Button', size: 'small', variant: 'primary' }); // base + size + variant - -// return ; -// }; - // Helper // --------------- -const get = (obj: object, path: string | string[]): any => { - const keys = typeof path === 'string' ? path.split('.') : path; - return keys.reduce((acc, key) => acc && (acc as any)[key], obj); -}; +export function get(obj: object, path: string, fallback?: any): any { + const key = typeof path === 'string' ? path.split('.') : [path]; + + let result = obj; + for (let i = 0, length = key.length; i < length; i++) { + if (!result) break; + result = (result as any)[key[i]]; + } + + return result === undefined ? fallback : result; +} // Types // --------------- @@ -63,28 +61,27 @@ export function useComponentStyles< export function useComponentStyles( componentName: string, - props: ComponentStylesProps = {}, + props: any = {}, options: any = {} ) { const { theme } = useTheme(); - const styles = get(theme, `components.${componentName}`); + const componentStyles = get(theme, `components.${componentName}`, {}); - // Just some PoC that the overloads work - if (options.parts) { - return { - [options.parts[0]]: {}, - [options.parts[1]]: {}, - }; - } + // Store styles in ref to prevent re-computation + const stylesRef = useRef({}); - return merge( - styles.base, - props.variant ? styles?.variant?.[props.variant] ?? {} : {} - ); -} + if (componentStyles) { + const base = componentStyles.base || {}; + const variant = componentStyles?.variant?.[props.variant] || {}; + const size = componentStyles?.size?.[props.size] || {}; + const state = componentStyles?.state?.[props.state] || {}; -// useRef for perf + const styles = merge.all([base, variant, size, state]); -// Q: if we get styles from the theme and deep merge directly, -// can we avoid using `variant` and instead pass it to `__baseCSS`/`css` directly? -// -> Make we can remove variant from `` altogether then? (since this hook is the better appraoch) + if (!isEqual(stylesRef.current, styles)) { + stylesRef.current = styles; + } + } + + return stylesRef.current; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c85be5f3a1..7703860160 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -336,6 +336,7 @@ importers: '@theme-ui/css': 0.14.2 csstype: 3.0.11 deepmerge: ^4.2.2 + react-fast-compare: ^3.2.0 tsup: 5.12.4 dependencies: '@emotion/react': 11.8.2_react@17.0.2 @@ -343,6 +344,7 @@ importers: '@theme-ui/css': 0.14.2_@emotion+react@11.8.2 csstype: 3.0.11 deepmerge: 4.2.2 + react-fast-compare: 3.2.0 devDependencies: '@marigold/tsconfig': link:../../config/tsconfig tsup: 5.12.4_typescript@4.6.3 From 7291649bd894edb8b15fc273c6a373fe47bd327f Mon Sep 17 00:00:00 2001 From: Sebastian Sebald Date: Thu, 7 Apr 2022 09:49:54 +0200 Subject: [PATCH 13/24] aaaadd tests --- .../system/src/useComponentStyles.test.tsx | 383 ++++++++++++++---- packages/system/src/useComponentStyles.ts | 12 +- 2 files changed, 322 insertions(+), 73 deletions(-) diff --git a/packages/system/src/useComponentStyles.test.tsx b/packages/system/src/useComponentStyles.test.tsx index 5356a4df2b..85d226aabe 100644 --- a/packages/system/src/useComponentStyles.test.tsx +++ b/packages/system/src/useComponentStyles.test.tsx @@ -7,11 +7,25 @@ import { useComponentStyles } from './useComponentStyles'; // Setup // --------------- const theme = { + /** + * Design tokens will not applied in the tests, + * but adding them will make sure that they are + * REALLY not applied! + */ colors: { primary: '#0070f3', secondary: '#ff4081', - white: '#fff', - black: '#000', + white: '#f8f9fa', + black: '#212529', + blue: '#228be6', + red: '#c92a2a', + }, + fontSizes: { + 'small-1': '12px', + 'small-2': '14px', + 'medium-1': '16px', + 'medium-2': '18px', + 'large-1': '20px', }, space: { none: 0, @@ -51,51 +65,94 @@ const theme = { color: 'secondary', }, }, + state: { + hover: { + bg: 'blue', + }, + error: { + bg: 'red', + }, + }, }, // Component with multiple parts Checkbox: { base: { - container: {}, - icon: {}, - label: {}, + container: { + display: 'flex', + alignItems: 'center', + gap: 'small-1', + }, + icon: { + size: 'small-1', + }, + label: { + color: 'black', + fontSize: 'small-2', + }, }, state: { checked: { - container: {}, - icon: {}, - label: {}, + icon: { + opacity: 1, + bg: 'blue', + }, }, unchecked: { - container: {}, - icon: {}, - label: {}, + icon: { + opacity: 0, + }, }, indeterminate: { - container: {}, - icon: {}, - label: {}, + icon: { + opacity: 1, + }, + label: { + fontStyle: 'italic', + }, }, error: { - container: {}, - icon: {}, - label: {}, + container: { + border: '1px solid', + borderColor: 'red', + }, + icon: { + fill: 'red', + }, + label: { + color: 'red', + }, }, }, size: { small: { - container: {}, - icon: {}, - label: {}, + container: { + p: 'small-1', + }, + icon: { + size: 'small-1', + }, }, medium: { - container: {}, - icon: {}, - label: {}, + container: { + p: 'medium-1', + }, + icon: { + size: 'medium-1', + }, + label: { + fontSize: 'medium-2', + }, }, large: { - container: {}, - icon: {}, - label: {}, + container: { + p: 'large-1', + }, + icon: { + size: 'large-1', + }, + label: { + fontSize: 'large-1', + }, }, }, }, @@ -108,66 +165,250 @@ const wrapper: React.FC = ({ children }) => ( // Tests // --------------- -test('get base styles for a component', () => { - const { result } = renderHook(() => useComponentStyles('Button'), { - wrapper, +describe('useComponentStyles (simple)', () => { + test('get base styles for a component', () => { + const { result } = renderHook(() => useComponentStyles('Button'), { + wrapper, + }); + expect(result.current).toMatchInlineSnapshot(` + { + "appearance": "none", + "bg": "white", + } + `); + }); + + test('get variant styles for a component', () => { + let view = renderHook( + () => useComponentStyles('Button', { variant: 'primary' }), + { + wrapper, + } + ); + expect(view.result.current).toMatchInlineSnapshot(` + { + "appearance": "none", + "bg": "white", + "color": "primary", + } + `); + + view = renderHook( + () => useComponentStyles('Button', { variant: 'secondary' }), + { + wrapper, + } + ); + expect(view.result.current).toMatchInlineSnapshot(` + { + "appearance": "none", + "bg": "white", + "color": "secondary", + } + `); + }); + + test('works if variant does not exist', () => { + const { result } = renderHook( + () => useComponentStyles('Button', { variant: 'non-existing-variant' }), + { + wrapper, + } + ); + expect(result.current).toMatchInlineSnapshot(` + { + "appearance": "none", + "bg": "white", + } + `); + }); + + test('base styles are applied together with variant', () => { + const { result } = renderHook( + () => useComponentStyles('Button', { variant: 'primary' }), + { + wrapper, + } + ); + expect(result.current).toMatchObject({ + appearance: 'none', + bg: 'white', + }); }); - expect(result.current).toMatchInlineSnapshot(` - { - "appearance": "none", - "bg": "white", - } - `); -}); -test('get variant styles for a component', () => { - let view = renderHook( - () => useComponentStyles('Button', { variant: 'primary' }), - { + test('get size styles for a component', () => { + let view = renderHook( + () => useComponentStyles('Button', { size: 'small' }), + { + wrapper, + } + ); + expect(view.result.current).toMatchInlineSnapshot(` + { + "appearance": "none", + "bg": "white", + "height": "small-1", + } + `); + + view = renderHook(() => useComponentStyles('Button', { size: 'medium' }), { wrapper, - } - ); - expect(view.result.current).toMatchInlineSnapshot(` - { - "appearance": "none", - "bg": "white", - "color": "primary", - } - `); - - view = renderHook( - () => useComponentStyles('Button', { variant: 'secondary' }), - { + }); + expect(view.result.current).toMatchInlineSnapshot(` + { + "appearance": "none", + "bg": "white", + "height": "medium-1", + } + `); + }); + + test('works if size does not exist', () => { + const { result } = renderHook( + () => useComponentStyles('Button', { size: 'non-existing-size' }), + { + wrapper, + } + ); + expect(result.current).toMatchInlineSnapshot(` + { + "appearance": "none", + "bg": "white", + } + `); + }); + + test('base styles are applied together with size', () => { + const { result } = renderHook( + () => useComponentStyles('Button', { size: 'small' }), + { + wrapper, + } + ); + expect(result.current).toMatchObject({ + appearance: 'none', + bg: 'white', + }); + }); + + test('get state styles for a component', () => { + let view = renderHook( + () => useComponentStyles('Button', { state: 'hover' }), + { + wrapper, + } + ); + expect(view.result.current).toMatchInlineSnapshot(` + { + "appearance": "none", + "bg": "blue", + } + `); + + view = renderHook(() => useComponentStyles('Button', { state: 'error' }), { wrapper, - } - ); - expect(view.result.current).toMatchInlineSnapshot(` - { - "appearance": "none", - "bg": "white", - "color": "secondary", - } - `); + }); + expect(view.result.current).toMatchInlineSnapshot(` + { + "appearance": "none", + "bg": "red", + } + `); + }); + + test('works if state does not exist', () => { + const { result } = renderHook( + () => useComponentStyles('Button', { state: 'visited' }), + { + wrapper, + } + ); + expect(result.current).toMatchInlineSnapshot(` + { + "appearance": "none", + "bg": "white", + } + `); + }); }); -// test('works if variant does not exist'); +describe('useComponentStyles (complex)', () => { + test('get base styles for a component (with parts)', () => { + const { result } = renderHook( + () => + useComponentStyles( + 'Checkbox', + {}, + { parts: ['container', 'icon', 'label'] } + ), + { + wrapper, + } + ); + expect(result.current).toMatchInlineSnapshot(` + { + "container": { + "alignItems": "center", + "display": "flex", + "gap": "small-1", + }, + "icon": { + "size": "small-1", + }, + "label": { + "color": "black", + "fontSize": "small-2", + }, + } + `); + }); -// test('get size styles for a component'); -// test('get state styles for a component'); + // test('get variant styles for a component (with parts)'); + // test('get size styles for a component (with parts)'); + // test('get state styles for a component (with parts)'); -// test('get base styles for a component (with parts)'); -// test('get variant styles for a component (with parts)'); -// test('get size styles for a component (with parts)'); -// test('get state styles for a component (with parts)'); + test('returns empty objects if part does not exist', () => { + const { result } = renderHook( + () => + useComponentStyles( + 'Checkbox', + {}, + { parts: ['container', 'non-existing-part'] } + ), + { + wrapper, + } + ); + expect(result.current).toMatchInlineSnapshot(` + { + "container": { + "alignItems": "center", + "display": "flex", + "gap": "small-1", + }, + "icon": { + "size": "small-1", + }, + "label": { + "color": "black", + "fontSize": "small-2", + }, + "non-existing-part": {}, + } + `); + }); +}); // test('base styles are always added'); -// test('override order: base < variant < size < state'); -// test('override order: base < variant < size < state (with parts)'); +// test('override order: base < size < state < variant'); +// test('override order: base < size < state < variant (with parts)'); // // example 'Button.state.hover' => 'Button:hover' // test('transform state styles'); // test('transform state styles (with parts)'); +// test('styles are not transpiled with tokens') + // test('usage with '); // test('usage with (with parts)'); diff --git a/packages/system/src/useComponentStyles.ts b/packages/system/src/useComponentStyles.ts index 1f5c52f970..69faa90ef5 100644 --- a/packages/system/src/useComponentStyles.ts +++ b/packages/system/src/useComponentStyles.ts @@ -21,6 +21,8 @@ export function get(obj: object, path: string, fallback?: any): any { // Types // --------------- +type IndexObject = { [key: string]: any }; + export type ComponentState = | 'hover' | 'focus' @@ -72,11 +74,17 @@ export function useComponentStyles( if (componentStyles) { const base = componentStyles.base || {}; - const variant = componentStyles?.variant?.[props.variant] || {}; const size = componentStyles?.size?.[props.size] || {}; const state = componentStyles?.state?.[props.state] || {}; + const variant = componentStyles?.variant?.[props.variant] || {}; + + const styles = merge.all([base, size, state, variant]) as IndexObject; - const styles = merge.all([base, variant, size, state]); + // if (options.parts) { + // options.parts.forEach((part: string) => { + // styles[part] = styles[part] ?? {}; + // }); + // } if (!isEqual(stylesRef.current, styles)) { stylesRef.current = styles; From 804c34b406277ae71206eb986b15fbb128063be9 Mon Sep 17 00:00:00 2001 From: Sebastian Sebald Date: Thu, 7 Apr 2022 10:04:04 +0200 Subject: [PATCH 14/24] even more tests --- .../system/src/useComponentStyles.test.tsx | 95 +++++++++++++++++-- packages/system/src/useComponentStyles.ts | 12 ++- 2 files changed, 95 insertions(+), 12 deletions(-) diff --git a/packages/system/src/useComponentStyles.test.tsx b/packages/system/src/useComponentStyles.test.tsx index 85d226aabe..e11fb99a28 100644 --- a/packages/system/src/useComponentStyles.test.tsx +++ b/packages/system/src/useComponentStyles.test.tsx @@ -19,6 +19,7 @@ const theme = { black: '#212529', blue: '#228be6', red: '#c92a2a', + pink: '#d6336c', }, fontSizes: { 'small-1': '12px', @@ -155,6 +156,13 @@ const theme = { }, }, }, + variant: { + pink: { + label: { + color: 'pink', + }, + }, + }, }, }, }; @@ -363,17 +371,43 @@ describe('useComponentStyles (complex)', () => { `); }); - // test('get variant styles for a component (with parts)'); - // test('get size styles for a component (with parts)'); - // test('get state styles for a component (with parts)'); + test('get variant styles for a component (with parts)', () => { + const { result } = renderHook( + () => + useComponentStyles( + 'Checkbox', + { variant: 'pink' }, + { parts: ['container', 'icon', 'label'] } + ), + { + wrapper, + } + ); + expect(result.current).toMatchInlineSnapshot(` + { + "container": { + "alignItems": "center", + "display": "flex", + "gap": "small-1", + }, + "icon": { + "size": "small-1", + }, + "label": { + "color": "pink", + "fontSize": "small-2", + }, + } + `); + }); - test('returns empty objects if part does not exist', () => { + test('get size styles for a component (with parts)', () => { const { result } = renderHook( () => useComponentStyles( 'Checkbox', - {}, - { parts: ['container', 'non-existing-part'] } + { size: 'small' }, + { parts: ['container', 'icon', 'label'] } ), { wrapper, @@ -385,6 +419,7 @@ describe('useComponentStyles (complex)', () => { "alignItems": "center", "display": "flex", "gap": "small-1", + "p": "small-1", }, "icon": { "size": "small-1", @@ -393,10 +428,56 @@ describe('useComponentStyles (complex)', () => { "color": "black", "fontSize": "small-2", }, - "non-existing-part": {}, } `); }); + + test('get state styles for a component (with parts)', () => { + const { result } = renderHook( + () => + useComponentStyles( + 'Checkbox', + { state: 'checked' }, + { parts: ['container', 'icon', 'label'] } + ), + { + wrapper, + } + ); + expect(result.current).toMatchInlineSnapshot(` + { + "container": { + "alignItems": "center", + "display": "flex", + "gap": "small-1", + }, + "icon": { + "bg": "blue", + "opacity": 1, + "size": "small-1", + }, + "label": { + "color": "black", + "fontSize": "small-2", + }, + } + `); + }); + + test('returns empty objects if part does not exist', () => { + const { result } = renderHook( + () => + useComponentStyles( + 'Checkbox', + {}, + { parts: ['container', 'non-existing-part'] } + ), + { + wrapper, + } + ); + expect(result.current['non-existing-part']).toMatchInlineSnapshot(`{}`); + }); }); // test('base styles are always added'); diff --git a/packages/system/src/useComponentStyles.ts b/packages/system/src/useComponentStyles.ts index 69faa90ef5..c134d0d2b8 100644 --- a/packages/system/src/useComponentStyles.ts +++ b/packages/system/src/useComponentStyles.ts @@ -78,13 +78,15 @@ export function useComponentStyles( const state = componentStyles?.state?.[props.state] || {}; const variant = componentStyles?.variant?.[props.variant] || {}; + // We deep merge so that parts (if they exists) also get put together const styles = merge.all([base, size, state, variant]) as IndexObject; - // if (options.parts) { - // options.parts.forEach((part: string) => { - // styles[part] = styles[part] ?? {}; - // }); - // } + // If a part does not exists in the theme, well add an empty object + if (options.parts) { + options.parts.forEach((part: string) => { + styles[part] = styles[part] ?? {}; + }); + } if (!isEqual(stylesRef.current, styles)) { stylesRef.current = styles; From 34056342d0caa16b491a185d698b1a7d9389098b Mon Sep 17 00:00:00 2001 From: Sebastian Sebald Date: Thu, 7 Apr 2022 10:10:50 +0200 Subject: [PATCH 15/24] state is an array ... --- .../system/src/useComponentStyles.test.tsx | 21 ++++++++----------- packages/system/src/useComponentStyles.ts | 8 ++++--- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/packages/system/src/useComponentStyles.test.tsx b/packages/system/src/useComponentStyles.test.tsx index e11fb99a28..251d3d9d6f 100644 --- a/packages/system/src/useComponentStyles.test.tsx +++ b/packages/system/src/useComponentStyles.test.tsx @@ -301,7 +301,7 @@ describe('useComponentStyles (simple)', () => { test('get state styles for a component', () => { let view = renderHook( - () => useComponentStyles('Button', { state: 'hover' }), + () => useComponentStyles('Button', { states: ['hover'] }), { wrapper, } @@ -313,9 +313,12 @@ describe('useComponentStyles (simple)', () => { } `); - view = renderHook(() => useComponentStyles('Button', { state: 'error' }), { - wrapper, - }); + view = renderHook( + () => useComponentStyles('Button', { states: ['error'] }), + { + wrapper, + } + ); expect(view.result.current).toMatchInlineSnapshot(` { "appearance": "none", @@ -326,7 +329,7 @@ describe('useComponentStyles (simple)', () => { test('works if state does not exist', () => { const { result } = renderHook( - () => useComponentStyles('Button', { state: 'visited' }), + () => useComponentStyles('Button', { states: ['visited'] }), { wrapper, } @@ -437,7 +440,7 @@ describe('useComponentStyles (complex)', () => { () => useComponentStyles( 'Checkbox', - { state: 'checked' }, + { states: ['checked'] }, { parts: ['container', 'icon', 'label'] } ), { @@ -492,9 +495,3 @@ describe('useComponentStyles (complex)', () => { // test('usage with '); // test('usage with (with parts)'); - -// const Component = () => { -// const r = useComponentStyles('name'); -// const f = useComponentStyles('name', {}, { parts: ['wrapper', 'icon'] }); -// console.log(f.wrapper, f.icon, f.name); -// }; diff --git a/packages/system/src/useComponentStyles.ts b/packages/system/src/useComponentStyles.ts index c134d0d2b8..6a5b29d25c 100644 --- a/packages/system/src/useComponentStyles.ts +++ b/packages/system/src/useComponentStyles.ts @@ -37,7 +37,7 @@ export type ComponentState = export interface ComponentStylesProps { variant?: string; size?: string; - state?: ComponentState; + states?: ComponentState[]; } export function useComponentStyles( @@ -75,11 +75,13 @@ export function useComponentStyles( if (componentStyles) { const base = componentStyles.base || {}; const size = componentStyles?.size?.[props.size] || {}; - const state = componentStyles?.state?.[props.state] || {}; const variant = componentStyles?.variant?.[props.variant] || {}; + const states = (props.state || []).map( + (state: string) => componentStyles?.state?.[state] || {} + ); // We deep merge so that parts (if they exists) also get put together - const styles = merge.all([base, size, state, variant]) as IndexObject; + const styles = merge.all([base, size, states, variant]) as IndexObject; // If a part does not exists in the theme, well add an empty object if (options.parts) { From aa6165d7ebb76135f490226f6d397420d5794e12 Mon Sep 17 00:00:00 2001 From: Sebastian Sebald Date: Thu, 7 Apr 2022 10:14:59 +0200 Subject: [PATCH 16/24] indeterminate is a variant not a state --- packages/system/src/useComponentStyles.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/system/src/useComponentStyles.ts b/packages/system/src/useComponentStyles.ts index 6a5b29d25c..30e22459d3 100644 --- a/packages/system/src/useComponentStyles.ts +++ b/packages/system/src/useComponentStyles.ts @@ -31,8 +31,7 @@ export type ComponentState = | 'disabled' | 'readOnly' | 'error' - | 'checked' - | 'indeterminate'; + | 'checked'; export interface ComponentStylesProps { variant?: string; From f7c9f2cef2978a0d1cc90653b89f08f47cb26d64 Mon Sep 17 00:00:00 2001 From: Sebastian Sebald Date: Thu, 7 Apr 2022 11:46:45 +0200 Subject: [PATCH 17/24] use obj for states --- .../system/src/useComponentStyles.test.tsx | 32 +++++--- packages/system/src/useComponentStyles.ts | 74 +++++++++++++------ 2 files changed, 73 insertions(+), 33 deletions(-) diff --git a/packages/system/src/useComponentStyles.test.tsx b/packages/system/src/useComponentStyles.test.tsx index 251d3d9d6f..230a7399d9 100644 --- a/packages/system/src/useComponentStyles.test.tsx +++ b/packages/system/src/useComponentStyles.test.tsx @@ -17,6 +17,7 @@ const theme = { secondary: '#ff4081', white: '#f8f9fa', black: '#212529', + grey: '#adb5bd', blue: '#228be6', red: '#c92a2a', pink: '#d6336c', @@ -73,6 +74,9 @@ const theme = { error: { bg: 'red', }, + disabled: { + bg: 'grey', + }, }, }, // Component with multiple parts @@ -103,14 +107,6 @@ const theme = { opacity: 0, }, }, - indeterminate: { - icon: { - opacity: 1, - }, - label: { - fontStyle: 'italic', - }, - }, error: { container: { border: '1px solid', @@ -301,7 +297,7 @@ describe('useComponentStyles (simple)', () => { test('get state styles for a component', () => { let view = renderHook( - () => useComponentStyles('Button', { states: ['hover'] }), + () => useComponentStyles('Button', { states: { hover: true } }), { wrapper, } @@ -314,7 +310,7 @@ describe('useComponentStyles (simple)', () => { `); view = renderHook( - () => useComponentStyles('Button', { states: ['error'] }), + () => useComponentStyles('Button', { states: { error: true } }), { wrapper, } @@ -327,9 +323,21 @@ describe('useComponentStyles (simple)', () => { `); }); + test('get multiple states', () => { + let view = renderHook( + () => + useComponentStyles('Button', { + states: { hover: true, disabled: true }, + }), + { + wrapper, + } + ); + }); + test('works if state does not exist', () => { const { result } = renderHook( - () => useComponentStyles('Button', { states: ['visited'] }), + () => useComponentStyles('Button', { states: { visited: true } }), { wrapper, } @@ -440,7 +448,7 @@ describe('useComponentStyles (complex)', () => { () => useComponentStyles( 'Checkbox', - { states: ['checked'] }, + { states: { checked: true } }, { parts: ['container', 'icon', 'label'] } ), { diff --git a/packages/system/src/useComponentStyles.ts b/packages/system/src/useComponentStyles.ts index 30e22459d3..f9b3d51a67 100644 --- a/packages/system/src/useComponentStyles.ts +++ b/packages/system/src/useComponentStyles.ts @@ -5,9 +5,30 @@ import isEqual from 'react-fast-compare'; import { CSSObject } from './types'; import { useTheme } from './useTheme'; +// Types +// --------------- +type IndexObject = { [key: string]: any }; + +export type ComponentState = + | 'hover' + | 'focus' + | 'active' + | 'visited' + | 'disabled' + | 'readOnly' + | 'checked' + | 'error'; + // Helper // --------------- -export function get(obj: object, path: string, fallback?: any): any { +/** + * Safely get a dot-notated path within a nested object, with ability + * to return a default if the full key path does not exist or + * the value is undefined + * + * Based on: https://github.com/developit/dlv + */ +const get = (obj: object, path: string, fallback?: any): any => { const key = typeof path === 'string' ? path.split('.') : [path]; let result = obj; @@ -17,26 +38,37 @@ export function get(obj: object, path: string, fallback?: any): any { } return result === undefined ? fallback : result; -} +}; -// Types -// --------------- -type IndexObject = { [key: string]: any }; +/** + * Convert an object of states, where the key is the state name and + * the value is a boolean, to an array of strings. + */ +const statesToFlags = ({ + disabled, + ...states +}: { [key in ComponentState]?: boolean } = {}): ComponentState[] => { + let flags = Object.keys(states).filter( + key => states[key as keyof typeof states] + ) as ComponentState[]; + + /** + * Adding `disabled` at the end of the array so that it + * will be the most prominent state and override the others. + */ + if (disabled) { + flags.push('disabled'); + } -export type ComponentState = - | 'hover' - | 'focus' - | 'active' - | 'visited' - | 'disabled' - | 'readOnly' - | 'error' - | 'checked'; + return flags; +}; +// Hook +// --------------- export interface ComponentStylesProps { variant?: string; size?: string; - states?: ComponentState[]; + states?: { [key in ComponentState]?: boolean }; } export function useComponentStyles( @@ -62,7 +94,7 @@ export function useComponentStyles< export function useComponentStyles( componentName: string, - props: any = {}, + props: ComponentStylesProps = {}, options: any = {} ) { const { theme } = useTheme(); @@ -73,14 +105,14 @@ export function useComponentStyles( if (componentStyles) { const base = componentStyles.base || {}; - const size = componentStyles?.size?.[props.size] || {}; - const variant = componentStyles?.variant?.[props.variant] || {}; - const states = (props.state || []).map( - (state: string) => componentStyles?.state?.[state] || {} + const size = componentStyles.size?.[props.size as any] || {}; + const variant = componentStyles.variant?.[props.variant as any] || {}; + const states = statesToFlags(props.states).map( + state => componentStyles.state?.[state] || {} ); // We deep merge so that parts (if they exists) also get put together - const styles = merge.all([base, size, states, variant]) as IndexObject; + const styles = merge.all([base, size, ...states, variant]) as IndexObject; // If a part does not exists in the theme, well add an empty object if (options.parts) { From b57e4176a90f1f6745cb614780417c15218ab7c2 Mon Sep 17 00:00:00 2001 From: Sebastian Sebald Date: Thu, 7 Apr 2022 11:55:23 +0200 Subject: [PATCH 18/24] yay for testing --- .../system/src/useComponentStyles.test.tsx | 50 ++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/packages/system/src/useComponentStyles.test.tsx b/packages/system/src/useComponentStyles.test.tsx index 230a7399d9..8367029b3d 100644 --- a/packages/system/src/useComponentStyles.test.tsx +++ b/packages/system/src/useComponentStyles.test.tsx @@ -74,7 +74,12 @@ const theme = { error: { bg: 'red', }, + focus: { + outline: '2px solid', + outlineColor: 'blue', + }, disabled: { + cursor: 'not-allowed', bg: 'grey', }, }, @@ -323,8 +328,26 @@ describe('useComponentStyles (simple)', () => { `); }); - test('get multiple states', () => { + test('get multiple states (disabled overrides other states)', () => { let view = renderHook( + () => + useComponentStyles('Button', { + states: { hover: true, focus: true }, + }), + { + wrapper, + } + ); + expect(view.result.current).toMatchInlineSnapshot(` + { + "appearance": "none", + "bg": "blue", + "outline": "2px solid", + "outlineColor": "blue", + } + `); + + view = renderHook( () => useComponentStyles('Button', { states: { hover: true, disabled: true }, @@ -333,6 +356,13 @@ describe('useComponentStyles (simple)', () => { wrapper, } ); + expect(view.result.current).toMatchInlineSnapshot(` + { + "appearance": "none", + "bg": "grey", + "cursor": "not-allowed", + } + `); }); test('works if state does not exist', () => { @@ -349,6 +379,24 @@ describe('useComponentStyles (simple)', () => { } `); }); + + test('only applies set styles', () => { + const { result } = renderHook( + () => + useComponentStyles('Button', { + states: { hover: true, disabled: false }, + }), + { + wrapper, + } + ); + expect(result.current).toMatchInlineSnapshot(` + { + "appearance": "none", + "bg": "blue", + } + `); + }); }); describe('useComponentStyles (complex)', () => { From de639a6bc771a46f877bf7264b5d6c08885f341a Mon Sep 17 00:00:00 2001 From: Sebastian Sebald Date: Thu, 7 Apr 2022 12:07:19 +0200 Subject: [PATCH 19/24] edge case tests added --- .../system/src/useComponentStyles.test.tsx | 71 ++++++++++++++++++- packages/system/src/useComponentStyles.ts | 2 +- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/packages/system/src/useComponentStyles.test.tsx b/packages/system/src/useComponentStyles.test.tsx index 8367029b3d..b7fee36133 100644 --- a/packages/system/src/useComponentStyles.test.tsx +++ b/packages/system/src/useComponentStyles.test.tsx @@ -187,6 +187,13 @@ describe('useComponentStyles (simple)', () => { `); }); + test('returns empty object if component does not exist in theme', () => { + const { result } = renderHook(() => useComponentStyles('NotExisting'), { + wrapper, + }); + expect(result.current).toEqual({}); + }); + test('get variant styles for a component', () => { let view = renderHook( () => useComponentStyles('Button', { variant: 'primary' }), @@ -539,9 +546,67 @@ describe('useComponentStyles (complex)', () => { }); }); -// test('base styles are always added'); -// test('override order: base < size < state < variant'); -// test('override order: base < size < state < variant (with parts)'); +describe('style superiority', () => { + test('override order: base < size < state < variant', () => { + const { result } = renderHook( + () => + useComponentStyles('Button', { + size: 'small', + variant: 'pink', + states: { disabled: true }, + }), + { + wrapper, + } + ); + expect(result.current).toMatchInlineSnapshot(` + { + "appearance": "none", + "bg": "grey", + "cursor": "not-allowed", + "height": "small-1", + } + `); + }); + + test('override order: base < size < state < variant (with parts)', () => { + const { result } = renderHook( + () => + useComponentStyles( + 'Checkbox', + { + size: 'small', + variant: 'pink', + states: { error: true }, + }, + { parts: ['container', 'icon', 'label'] } + ), + { + wrapper, + } + ); + expect(result.current).toMatchInlineSnapshot(` + { + "container": { + "alignItems": "center", + "border": "1px solid", + "borderColor": "red", + "display": "flex", + "gap": "small-1", + "p": "small-1", + }, + "icon": { + "fill": "red", + "size": "small-1", + }, + "label": { + "color": "pink", + "fontSize": "small-2", + }, + } + `); + }); +}); // // example 'Button.state.hover' => 'Button:hover' // test('transform state styles'); diff --git a/packages/system/src/useComponentStyles.ts b/packages/system/src/useComponentStyles.ts index f9b3d51a67..45015dc093 100644 --- a/packages/system/src/useComponentStyles.ts +++ b/packages/system/src/useComponentStyles.ts @@ -98,7 +98,7 @@ export function useComponentStyles( options: any = {} ) { const { theme } = useTheme(); - const componentStyles = get(theme, `components.${componentName}`, {}); + const componentStyles = get(theme, `components.${componentName}`); // Store styles in ref to prevent re-computation const stylesRef = useRef({}); From 163b87d9ceada4dc575e4f8896cb90e053fe2cc6 Mon Sep 17 00:00:00 2001 From: Sebastian Sebald Date: Thu, 7 Apr 2022 12:26:08 +0200 Subject: [PATCH 20/24] add indeterminate state again --- packages/system/src/useComponentStyles.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/system/src/useComponentStyles.ts b/packages/system/src/useComponentStyles.ts index 45015dc093..274721e8a7 100644 --- a/packages/system/src/useComponentStyles.ts +++ b/packages/system/src/useComponentStyles.ts @@ -17,6 +17,7 @@ export type ComponentState = | 'disabled' | 'readOnly' | 'checked' + | 'indeterminate' | 'error'; // Helper From 354fe83298fcb2a6ac93015ab4eb908d7b5be364 Mon Sep 17 00:00:00 2001 From: Sebastian Sebald Date: Thu, 7 Apr 2022 13:07:55 +0200 Subject: [PATCH 21/24] finish tests --- .../system/src/useComponentStyles.test.tsx | 116 +++++++++++++++--- 1 file changed, 98 insertions(+), 18 deletions(-) diff --git a/packages/system/src/useComponentStyles.test.tsx b/packages/system/src/useComponentStyles.test.tsx index b7fee36133..ae93825dd8 100644 --- a/packages/system/src/useComponentStyles.test.tsx +++ b/packages/system/src/useComponentStyles.test.tsx @@ -1,16 +1,16 @@ import React from 'react'; +import { render, screen } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; import { ThemeProvider } from './useTheme'; import { useComponentStyles } from './useComponentStyles'; +import { Box } from './Box'; // Setup // --------------- const theme = { /** - * Design tokens will not applied in the tests, - * but adding them will make sure that they are - * REALLY not applied! + * Design tokens will not applied in most tests! */ colors: { primary: '#0070f3', @@ -93,7 +93,8 @@ const theme = { gap: 'small-1', }, icon: { - size: 'small-1', + height: 'small-1', + width: 'small-1', }, label: { color: 'black', @@ -131,7 +132,8 @@ const theme = { p: 'small-1', }, icon: { - size: 'small-1', + height: 'small-1', + width: 'small-1', }, }, medium: { @@ -139,7 +141,8 @@ const theme = { p: 'medium-1', }, icon: { - size: 'medium-1', + height: 'medium-1', + width: 'medium-1', }, label: { fontSize: 'medium-2', @@ -150,7 +153,8 @@ const theme = { p: 'large-1', }, icon: { - size: 'large-1', + height: 'large-1', + width: 'large-1', }, label: { fontSize: 'large-1', @@ -427,7 +431,8 @@ describe('useComponentStyles (complex)', () => { "gap": "small-1", }, "icon": { - "size": "small-1", + "height": "small-1", + "width": "small-1", }, "label": { "color": "black", @@ -457,7 +462,8 @@ describe('useComponentStyles (complex)', () => { "gap": "small-1", }, "icon": { - "size": "small-1", + "height": "small-1", + "width": "small-1", }, "label": { "color": "pink", @@ -488,7 +494,8 @@ describe('useComponentStyles (complex)', () => { "p": "small-1", }, "icon": { - "size": "small-1", + "height": "small-1", + "width": "small-1", }, "label": { "color": "black", @@ -519,8 +526,9 @@ describe('useComponentStyles (complex)', () => { }, "icon": { "bg": "blue", + "height": "small-1", "opacity": 1, - "size": "small-1", + "width": "small-1", }, "label": { "color": "black", @@ -597,7 +605,8 @@ describe('style superiority', () => { }, "icon": { "fill": "red", - "size": "small-1", + "height": "small-1", + "width": "small-1", }, "label": { "color": "pink", @@ -608,11 +617,82 @@ describe('style superiority', () => { }); }); -// // example 'Button.state.hover' => 'Button:hover' -// test('transform state styles'); -// test('transform state styles (with parts)'); +test('styles are not transpiled with tokens', () => { + const { result } = renderHook( + () => + useComponentStyles('Button', { + size: 'small', + variant: 'pink', + states: { disabled: true }, + }), + { + wrapper, + } + ); + expect(result.current).toMatchInlineSnapshot(` + { + "appearance": "none", + "bg": "grey", + "cursor": "not-allowed", + "height": "small-1", + } + `); +}); + +test('usage with ', () => { + const Button: React.FC = ({ children }) => { + const styles = useComponentStyles('Button'); + return ( + + {children} + + ); + }; + + render( + + + + ); + + const element = screen.getByTestId('button'); + expect(element).toHaveStyle('appearance: none'); + expect(element).toHaveStyle(`background: ${theme.colors.white}`); +}); + +test('usage with (with parts)', () => { + const Checkbox: React.FC = ({ children }) => { + const styles = useComponentStyles( + 'Checkbox', + {}, + { parts: ['container', 'icon', 'label'] } + ); + return ( + + + {children} + + + + ); + }; -// test('styles are not transpiled with tokens') + render( + + Click me! + + ); -// test('usage with '); -// test('usage with (with parts)'); + const container = screen.getByTestId('container'); + expect(container).toHaveStyle('display: flex'); + expect(container).toHaveStyle('align-items: center'); + expect(container).toHaveStyle(`gap: ${theme.space['small-1']}px`); + + const label = screen.getByTestId('label'); + expect(label).toHaveStyle(`color: ${theme.colors.black}`); + expect(label).toHaveStyle(`font-size: ${theme.fontSizes['small-2']}`); + + const icon = screen.getByTestId('icon'); + expect(icon).toHaveStyle(`height: ${theme.sizes['small-1']}px`); + expect(icon).toHaveStyle(`width: ${theme.sizes['small-1']}px`); +}); From 1fcb39f52f4a46f62251de78dee012811ec0ec19 Mon Sep 17 00:00:00 2001 From: Sebastian Sebald Date: Thu, 7 Apr 2022 13:09:55 +0200 Subject: [PATCH 22/24] Create dull-eyes-walk.md --- .changeset/dull-eyes-walk.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/dull-eyes-walk.md diff --git a/.changeset/dull-eyes-walk.md b/.changeset/dull-eyes-walk.md new file mode 100644 index 0000000000..32b8d277bf --- /dev/null +++ b/.changeset/dull-eyes-walk.md @@ -0,0 +1,5 @@ +--- +"@marigold/system": patch +--- + +feat: introduce `useComponentStyles` hook From 3c01f50f3faf81e034c855370047037a3f249a67 Mon Sep 17 00:00:00 2001 From: Sebastian Sebald Date: Thu, 7 Apr 2022 20:27:48 +0200 Subject: [PATCH 23/24] update for coverage --- .../system/src/useComponentStyles.test.tsx | 140 ++++++++++-------- packages/system/src/useComponentStyles.ts | 2 +- 2 files changed, 76 insertions(+), 66 deletions(-) diff --git a/packages/system/src/useComponentStyles.test.tsx b/packages/system/src/useComponentStyles.test.tsx index ae93825dd8..68800e3f4c 100644 --- a/packages/system/src/useComponentStyles.test.tsx +++ b/packages/system/src/useComponentStyles.test.tsx @@ -169,6 +169,7 @@ const theme = { }, }, }, + GotNoBase: {}, }, }; @@ -191,6 +192,13 @@ describe('useComponentStyles (simple)', () => { `); }); + test('returns empty object if component has no base styles', () => { + const { result } = renderHook(() => useComponentStyles('GotNoBase'), { + wrapper, + }); + expect(result.current).toEqual({}); + }); + test('returns empty object if component does not exist in theme', () => { const { result } = renderHook(() => useComponentStyles('NotExisting'), { wrapper, @@ -617,19 +625,20 @@ describe('style superiority', () => { }); }); -test('styles are not transpiled with tokens', () => { - const { result } = renderHook( - () => - useComponentStyles('Button', { - size: 'small', - variant: 'pink', - states: { disabled: true }, - }), - { - wrapper, - } - ); - expect(result.current).toMatchInlineSnapshot(` +describe('style usage', () => { + test('styles are not transpiled with tokens', () => { + const { result } = renderHook( + () => + useComponentStyles('Button', { + size: 'small', + variant: 'pink', + states: { disabled: true }, + }), + { + wrapper, + } + ); + expect(result.current).toMatchInlineSnapshot(` { "appearance": "none", "bg": "grey", @@ -637,62 +646,63 @@ test('styles are not transpiled with tokens', () => { "height": "small-1", } `); -}); + }); -test('usage with ', () => { - const Button: React.FC = ({ children }) => { - const styles = useComponentStyles('Button'); - return ( - - {children} - - ); - }; + test('usage with ', () => { + const Button: React.FC = ({ children }) => { + const styles = useComponentStyles('Button'); + return ( + + {children} + + ); + }; - render( - - - - ); + render( + + + + ); - const element = screen.getByTestId('button'); - expect(element).toHaveStyle('appearance: none'); - expect(element).toHaveStyle(`background: ${theme.colors.white}`); -}); + const element = screen.getByTestId('button'); + expect(element).toHaveStyle('appearance: none'); + expect(element).toHaveStyle(`background: ${theme.colors.white}`); + }); -test('usage with (with parts)', () => { - const Checkbox: React.FC = ({ children }) => { - const styles = useComponentStyles( - 'Checkbox', - {}, - { parts: ['container', 'icon', 'label'] } - ); - return ( - - - {children} - + test('usage with (with parts)', () => { + const Checkbox: React.FC = ({ children }) => { + const styles = useComponentStyles( + 'Checkbox', + {}, + { parts: ['container', 'icon', 'label'] } + ); + return ( + + + {children} + + - + ); + }; + + render( + + Click me! + ); - }; - - render( - - Click me! - - ); - - const container = screen.getByTestId('container'); - expect(container).toHaveStyle('display: flex'); - expect(container).toHaveStyle('align-items: center'); - expect(container).toHaveStyle(`gap: ${theme.space['small-1']}px`); - - const label = screen.getByTestId('label'); - expect(label).toHaveStyle(`color: ${theme.colors.black}`); - expect(label).toHaveStyle(`font-size: ${theme.fontSizes['small-2']}`); - - const icon = screen.getByTestId('icon'); - expect(icon).toHaveStyle(`height: ${theme.sizes['small-1']}px`); - expect(icon).toHaveStyle(`width: ${theme.sizes['small-1']}px`); + + const container = screen.getByTestId('container'); + expect(container).toHaveStyle('display: flex'); + expect(container).toHaveStyle('align-items: center'); + expect(container).toHaveStyle(`gap: ${theme.space['small-1']}px`); + + const label = screen.getByTestId('label'); + expect(label).toHaveStyle(`color: ${theme.colors.black}`); + expect(label).toHaveStyle(`font-size: ${theme.fontSizes['small-2']}`); + + const icon = screen.getByTestId('icon'); + expect(icon).toHaveStyle(`height: ${theme.sizes['small-1']}px`); + expect(icon).toHaveStyle(`width: ${theme.sizes['small-1']}px`); + }); }); diff --git a/packages/system/src/useComponentStyles.ts b/packages/system/src/useComponentStyles.ts index 274721e8a7..3b1987b73f 100644 --- a/packages/system/src/useComponentStyles.ts +++ b/packages/system/src/useComponentStyles.ts @@ -30,7 +30,7 @@ export type ComponentState = * Based on: https://github.com/developit/dlv */ const get = (obj: object, path: string, fallback?: any): any => { - const key = typeof path === 'string' ? path.split('.') : [path]; + const key = path.split('.'); let result = obj; for (let i = 0, length = key.length; i < length; i++) { From 861241930f5e37f8ac0669b602bd0cf5fdece6a7 Mon Sep 17 00:00:00 2001 From: Sebastian Sebald Date: Thu, 7 Apr 2022 20:38:46 +0200 Subject: [PATCH 24/24] :100: --- .../system/src/useComponentStyles.test.tsx | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/system/src/useComponentStyles.test.tsx b/packages/system/src/useComponentStyles.test.tsx index 68800e3f4c..29b056fddb 100644 --- a/packages/system/src/useComponentStyles.test.tsx +++ b/packages/system/src/useComponentStyles.test.tsx @@ -179,6 +179,20 @@ const wrapper: React.FC = ({ children }) => ( // Tests // --------------- +describe('smoketests', () => { + test('works without a theme', () => { + const { result } = renderHook(() => useComponentStyles('NotExisting')); + expect(result.current).toEqual({}); + }); + + test('returns empty object if component does not exist in theme', () => { + const { result } = renderHook(() => useComponentStyles('NotExisting'), { + wrapper, + }); + expect(result.current).toEqual({}); + }); +}); + describe('useComponentStyles (simple)', () => { test('get base styles for a component', () => { const { result } = renderHook(() => useComponentStyles('Button'), { @@ -199,13 +213,6 @@ describe('useComponentStyles (simple)', () => { expect(result.current).toEqual({}); }); - test('returns empty object if component does not exist in theme', () => { - const { result } = renderHook(() => useComponentStyles('NotExisting'), { - wrapper, - }); - expect(result.current).toEqual({}); - }); - test('get variant styles for a component', () => { let view = renderHook( () => useComponentStyles('Button', { variant: 'primary' }),