Skip to content

Commit 3c606ed

Browse files
authored
Merge pull request #23 from maxholman/refactor-variants
fix: various rc
2 parents 7cf9e82 + 1bbe021 commit 3c606ed

16 files changed

Lines changed: 363 additions & 204 deletions

Makefile

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@ build/global.css: node_modules dist/bin/token.js
3030
pnpm exec prettier --write $@
3131

3232
tsconfig.json: node_modules tsconfig-vite.src.json
33-
echo "// last updated: $(shell date)" > $@
34-
pnpm exec tsc -p tsconfig-vite.src.json --showConfig 1>> $@
33+
pnpm exec tsc -p tsconfig-vite.src.json --showConfig 1> $@
3534

3635
build: $(SRCS) node_modules vite.config.ts vite-env.d.ts
3736
NODE_ENV=production pnpm exec vite build

lib/button.css.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,10 @@ export const buttonClassName = style([
112112
},
113113
]);
114114

115-
export const iconClass = style({
115+
export const iconClassName = style({
116116
aspectRatio: '1/1',
117-
lineHeight: 0,
117+
display: 'inline-flex',
118+
width: '0.7em', // hack, this should be variable based on the font-size
118119
});
119120

120121
export const visiblyHiddenClass = style({

lib/button.tsx

Lines changed: 52 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import {
2-
cloneElement,
32
forwardRef,
4-
isValidElement,
53
type FC,
64
type ForwardedRef,
75
type ReactElement,
6+
isValidElement,
87
} from 'react';
98
import { Box, type BoxProps } from './box.js';
109
import type { ButtonState, ButtonVariant } from './button.css.js';
@@ -13,7 +12,7 @@ import {
1312
buttonClassName,
1413
buttonStateClassNames,
1514
buttonVariantClassNames,
16-
iconClass,
15+
iconClassName,
1716
inlineBleedClass,
1817
visiblyHiddenClass,
1918
} from './button.css.js';
@@ -22,7 +21,7 @@ import { useStringLikeDetector } from './hooks/use-string-like.js';
2221
import { Flex, type FlexProps } from './layout.js';
2322
import { Spinner } from './loaders.js';
2423
import type { Falsy, Merge, ReactHTMLElementsHacked } from './types.js';
25-
import { ExactText } from './typography.js';
24+
import { ExactText, type FontSize } from './typography.js';
2625

2726
export { ButtonState, ButtonVariant };
2827

@@ -73,11 +72,16 @@ export type ButtonIconProps<
7372
const IconBox: FC<{
7473
icon: ReactElement | FC;
7574
busy?: boolean | Falsy;
76-
}> = ({ icon, busy }) => (
77-
<Box component="span" className={[iconClass, busy && visiblyHiddenClass]}>
75+
capSize?: FontSize;
76+
}> = ({ icon, busy, ...props }) => (
77+
<Box
78+
{...props}
79+
component="span"
80+
className={[iconClassName, busy && visiblyHiddenClass]}
81+
>
7882
{isValidElement<ReactElementDefaultPropsType>(icon)
79-
? cloneElement(icon, { className: iconClass })
80-
: icon({ className: iconClass })}
83+
? icon
84+
: icon({} /* { className: iconClassName } */)}
8185
</Box>
8286
);
8387

@@ -104,14 +108,14 @@ function getSizeProps(size: ButtonSize | Falsy) {
104108
switch (size) {
105109
case 'small':
106110
return {
107-
space: '1',
111+
space: '2',
108112
fontSize: '0',
109113
paddingBlock: '4',
110114
paddingInline: '5',
111115
} satisfies BoxProps;
112116
case 'large':
113117
return {
114-
space: '2',
118+
space: '3',
115119
fontSize: '3',
116120
paddingBlock: '6',
117121
paddingInline: '7',
@@ -144,7 +148,7 @@ export const Button = forwardRef(
144148
flexGrow,
145149
state,
146150
size = 'medium',
147-
variant = 'default',
151+
variant,
148152
...props
149153
}: ButtonProps<T>,
150154
ref: ForwardedRef<HTMLElement>,
@@ -163,6 +167,15 @@ export const Button = forwardRef(
163167
'aria-live': busy ? 'polite' : undefined,
164168
} as const;
165169

170+
const autoButtonTypeVariant =
171+
'type' in props && props.type === 'submit' && !variant
172+
? 'primary'
173+
: variant;
174+
175+
const resolvedVariant = autoButtonTypeVariant || 'default';
176+
177+
const hasStringChildren = isStringLike(children);
178+
166179
return (
167180
<UnstyledButton
168181
ref={ref}
@@ -178,34 +191,37 @@ export const Button = forwardRef(
178191
inline && inlineBleedClass,
179192

180193
// order is important here, as we want the state to override the variant
181-
state && variant && buttonStateClassNames[variant][state],
194+
state &&
195+
resolvedVariant &&
196+
buttonStateClassNames[resolvedVariant][state],
182197

183-
variant && buttonVariantClassNames[variant],
198+
resolvedVariant && buttonVariantClassNames[resolvedVariant],
184199
]}
185200
>
186-
{iconStart && <IconBox icon={iconStart} busy={busy} />}
187-
188-
{!busy &&
189-
(isStringLike(children) ? (
190-
<ExactText
191-
component="span"
192-
textAlign={textAlign}
193-
capSize={fontSize}
194-
{...busyAttributes}
195-
>
196-
{children}
197-
</ExactText>
198-
) : (
199-
children && (
200-
<Box flexGrow component="span" {...busyAttributes}>
201-
{children}
202-
</Box>
203-
)
204-
))}
205-
206-
{busy && <Spinner />}
207-
208-
{iconEnd && <IconBox icon={iconEnd} busy={busy} />}
201+
{iconStart && (
202+
<IconBox capSize={fontSize} busy={busy} icon={iconStart} />
203+
)}
204+
205+
{!busy && hasStringChildren && (
206+
<ExactText
207+
component="span"
208+
textAlign={textAlign}
209+
capSize={fontSize}
210+
{...busyAttributes}
211+
>
212+
{children}
213+
</ExactText>
214+
)}
215+
216+
{!busy && children && !hasStringChildren && (
217+
<Box flexGrow component="span" {...busyAttributes}>
218+
{children}
219+
</Box>
220+
)}
221+
222+
{busy && <Spinner capSize={fontSize} />}
223+
224+
{iconEnd && <IconBox capSize={fontSize} busy={busy} icon={iconEnd} />}
209225
</UnstyledButton>
210226
);
211227
},

lib/design-system.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,14 @@ export const DesignSystem = <
2525
>({
2626
className,
2727
integrationMode,
28-
stringLikeComponents,
28+
stringLikeComponents = [],
2929
component = 'div',
3030
...props
3131
}: DesignSystemProps<T>): ReactElement | null => (
3232
<DesignSystemContext.Provider
3333
value={{
3434
className,
35-
...(stringLikeComponents && { stringLikeComponents }),
35+
stringLikeComponents,
3636
}}
3737
>
3838
<Box

lib/grid.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,13 @@ import {
88
import { Box, type BoxProps } from './box.js';
99
import { matchViewportVariants } from './component-utils.js';
1010
import { gridClass } from './grid.css.js';
11-
import type { Falsy, Merge, ReactHTMLElementsHacked } from './types.js';
11+
import type { Falsy, ReactHTMLElementsHacked } from './types.js';
1212

13-
export type GridProps<T extends keyof ReactHTMLElementsHacked = 'div'> = Merge<
14-
BoxProps<T>,
15-
{
13+
export type GridProps<T extends keyof ReactHTMLElementsHacked = 'div'> =
14+
BoxProps<T> & {
1615
space?: OrResponsive<Space | Falsy>;
1716
cols?: OrResponsive<Columns>;
18-
}
19-
>;
17+
};
2018

2119
export const Grid = <T extends keyof ReactHTMLElementsHacked = 'div'>({
2220
className,

lib/hooks/hooks.test.tsx

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
// import userEvent from '@testing-library/user-event';
2+
import '@testing-library/jest-dom';
3+
import { render, screen } from '@testing-library/react';
4+
import type { FC, PropsWithChildren } from 'react';
5+
import { describe, expect, it } from 'vitest';
6+
import { DesignSystem } from '../main.js';
7+
import { useStringLikeDetector } from './use-string-like.js';
8+
9+
const IsStringLikeTester: FC<PropsWithChildren> = (props) => {
10+
const isStringLike = useStringLikeDetector();
11+
return <div data-testid="result">{String(isStringLike(props.children))}</div>;
12+
};
13+
14+
const StringMcStringFace: FC<PropsWithChildren> = (props) => (
15+
<div>{props.children}</div>
16+
);
17+
18+
describe('Hooks', () => {
19+
it('passes string is string like', async () => {
20+
// ARRANGE
21+
render(
22+
<DesignSystem>
23+
<IsStringLikeTester>test</IsStringLikeTester>
24+
</DesignSystem>,
25+
);
26+
27+
// ASSERT
28+
expect(screen.getByTestId('result')).toHaveTextContent('true');
29+
});
30+
31+
it('passes fragments with strings', async () => {
32+
// ARRANGE
33+
render(
34+
<DesignSystem>
35+
<IsStringLikeTester>
36+
<>test</>
37+
<>test</>
38+
</IsStringLikeTester>
39+
</DesignSystem>,
40+
);
41+
42+
// ASSERT
43+
expect(screen.getByTestId('result')).toHaveTextContent('true');
44+
});
45+
46+
it('passes fragments with primitives', async () => {
47+
// ARRANGE
48+
render(
49+
<DesignSystem>
50+
<IsStringLikeTester>
51+
<>123</>
52+
<>test</>
53+
<>{null}</>
54+
<>{undefined}</>
55+
{undefined}
56+
{null}
57+
{true}
58+
</IsStringLikeTester>
59+
</DesignSystem>,
60+
);
61+
62+
// ASSERT
63+
expect(screen.getByTestId('result')).toHaveTextContent('true');
64+
});
65+
66+
it('passes nested fragments with strings', async () => {
67+
// ARRANGE
68+
render(
69+
<DesignSystem>
70+
<IsStringLikeTester>
71+
<>
72+
<>
73+
<>test</>
74+
</>
75+
</>
76+
</IsStringLikeTester>
77+
</DesignSystem>,
78+
);
79+
80+
// ASSERT
81+
expect(screen.getByTestId('result')).toHaveTextContent('true');
82+
});
83+
84+
it('fails with element', async () => {
85+
// ARRANGE
86+
render(
87+
<DesignSystem>
88+
<IsStringLikeTester>
89+
<h1>NOT STRING LIKE</h1>
90+
</IsStringLikeTester>
91+
</DesignSystem>,
92+
);
93+
94+
// ASSERT
95+
expect(screen.getByTestId('result')).toHaveTextContent('false');
96+
});
97+
98+
it('fails nested fragments with element', async () => {
99+
// ARRANGE
100+
render(
101+
<DesignSystem>
102+
<IsStringLikeTester>
103+
<>
104+
<>
105+
<>
106+
<h1>BAD</h1>
107+
</>
108+
</>
109+
</>
110+
</IsStringLikeTester>
111+
</DesignSystem>,
112+
);
113+
114+
// ASSERT
115+
expect(screen.getByTestId('result')).toHaveTextContent('false');
116+
});
117+
118+
it('passes with custom string like', async () => {
119+
// ARRANGE
120+
render(
121+
<DesignSystem stringLikeComponents={[StringMcStringFace]}>
122+
<IsStringLikeTester>
123+
<StringMcStringFace>WORKS</StringMcStringFace>
124+
</IsStringLikeTester>
125+
</DesignSystem>,
126+
);
127+
128+
// ASSERT
129+
expect(screen.getByTestId('result')).toHaveTextContent('true');
130+
});
131+
132+
it('passes with fragment nested custom string like', async () => {
133+
// ARRANGE
134+
render(
135+
<DesignSystem stringLikeComponents={[StringMcStringFace]}>
136+
<IsStringLikeTester>
137+
<>
138+
<>
139+
<StringMcStringFace>WORKS</StringMcStringFace>
140+
</>
141+
</>
142+
</IsStringLikeTester>
143+
</DesignSystem>,
144+
);
145+
146+
// ASSERT
147+
expect(screen.getByTestId('result')).toHaveTextContent('true');
148+
});
149+
150+
it('fails with fragment nested custom string like + bad', async () => {
151+
// ARRANGE
152+
render(
153+
<DesignSystem stringLikeComponents={[StringMcStringFace]}>
154+
<IsStringLikeTester>
155+
<>
156+
<>
157+
<StringMcStringFace>GOOD</StringMcStringFace>
158+
<h1>BAD</h1>
159+
</>
160+
</>
161+
</IsStringLikeTester>
162+
</DesignSystem>,
163+
);
164+
165+
// ASSERT
166+
expect(screen.getByTestId('result')).toHaveTextContent('false');
167+
});
168+
});

0 commit comments

Comments
 (0)