@hyphen/hyphen-components 3.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Button/Button.d.ts +12 -51
- package/dist/components/Button/Button.stories.d.ts +1 -1
- package/dist/components/Drawer/Drawer.d.ts +2 -8
- package/dist/components/Sidebar/Sidebar.d.ts +2 -6
- package/dist/css/index.css +1 -1
- package/dist/hyphen-components.cjs.development.js +41 -104
- package/dist/hyphen-components.cjs.development.js.map +1 -1
- package/dist/hyphen-components.cjs.production.min.js +1 -1
- package/dist/hyphen-components.cjs.production.min.js.map +1 -1
- package/dist/hyphen-components.esm.js +42 -105
- package/dist/hyphen-components.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Button/Button.mdx +7 -7
- package/src/components/Button/Button.module.scss +3 -0
- package/src/components/Button/Button.stories.tsx +12 -12
- package/src/components/Button/Button.test.tsx +128 -112
- package/src/components/Button/Button.tsx +80 -178
- package/src/components/Drawer/Drawer.tsx +8 -12
- package/src/components/Sidebar/Sidebar.stories.tsx +4 -4
- package/src/lib/tokens.ts +11 -8
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@ import { Box } from '../Box/Box';
|
|
|
4
4
|
|
|
5
5
|
import * as Stories from './Button.stories';
|
|
6
6
|
|
|
7
|
-
<Meta of={Stories} />
|
|
7
|
+
<Meta of={Stories} title="Components/Button" component={Button} />
|
|
8
8
|
|
|
9
9
|
# Button
|
|
10
10
|
|
|
@@ -18,6 +18,12 @@ Actions almost always occur on the same page.
|
|
|
18
18
|
|
|
19
19
|
<ArgTypes of={Button} />
|
|
20
20
|
|
|
21
|
+
## As Child
|
|
22
|
+
|
|
23
|
+
Apply the `asChild` prop to apply the button styles to a child element.
|
|
24
|
+
|
|
25
|
+
<Canvas of={Stories.AsChild} />
|
|
26
|
+
|
|
21
27
|
## Variants
|
|
22
28
|
|
|
23
29
|
The `variant` prop determines which color visual weight to render.
|
|
@@ -70,9 +76,3 @@ The interface should make it clear why the button is disabled and what needs to
|
|
|
70
76
|
|
|
71
77
|
<Canvas of={Stories.Disabled} />
|
|
72
78
|
|
|
73
|
-
## As an Anchor
|
|
74
|
-
|
|
75
|
-
You can render an anchor tag with the style of a button by using the `as` prop. You
|
|
76
|
-
can render buttons as one of `button`, `a`, and `input`.
|
|
77
|
-
|
|
78
|
-
<Canvas of={Stories.Anchor} />
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
--button-size-lg-border-radius,
|
|
59
59
|
var(--INTERNAL_form-control-size-lg-border-radius)
|
|
60
60
|
);
|
|
61
|
+
gap: var(--size-spacing-lg);
|
|
61
62
|
padding: var(
|
|
62
63
|
--button-size-lg-padding-vertical,
|
|
63
64
|
var(--INTERNAL_form-control-size-lg-padding)
|
|
@@ -89,6 +90,7 @@
|
|
|
89
90
|
line-height: 1;
|
|
90
91
|
font-family: var(--assets-font-family-body);
|
|
91
92
|
display: inline-flex;
|
|
93
|
+
gap: var(--size-spacing-sm);
|
|
92
94
|
align-items: center;
|
|
93
95
|
justify-content: center;
|
|
94
96
|
position: relative;
|
|
@@ -103,6 +105,7 @@
|
|
|
103
105
|
text-decoration: none;
|
|
104
106
|
}
|
|
105
107
|
|
|
108
|
+
&[aria-disabled='true'],
|
|
106
109
|
&:disabled {
|
|
107
110
|
opacity: 0.45;
|
|
108
111
|
cursor: not-allowed;
|
|
@@ -23,6 +23,14 @@ export const Default = () => (
|
|
|
23
23
|
<Button onClick={() => alert('clicked')}>Button</Button>
|
|
24
24
|
);
|
|
25
25
|
|
|
26
|
+
export const AsChild = () => (
|
|
27
|
+
<Button asChild>
|
|
28
|
+
<a href="https://ux.hyphen.ai" target="_blank" rel="noreferrer">
|
|
29
|
+
I'm an anchor
|
|
30
|
+
</a>
|
|
31
|
+
</Button>
|
|
32
|
+
);
|
|
33
|
+
|
|
26
34
|
export const Variants = () => (
|
|
27
35
|
<Box gap="md" style={{ backgroundColor: 'var(--background-primary)' }}>
|
|
28
36
|
<Box gap="sm" direction="row" alignItems="flex-start">
|
|
@@ -66,7 +74,7 @@ export const FullWidth = () => (
|
|
|
66
74
|
);
|
|
67
75
|
|
|
68
76
|
export const Icons = () => (
|
|
69
|
-
<Box direction="row" gap="
|
|
77
|
+
<Box direction="row" gap="md" alignItems="flex-start">
|
|
70
78
|
<Button variant="primary" iconPrefix="mail">
|
|
71
79
|
Email
|
|
72
80
|
</Button>
|
|
@@ -85,7 +93,7 @@ export const IconButton = () => (
|
|
|
85
93
|
);
|
|
86
94
|
|
|
87
95
|
export const Loading = () => (
|
|
88
|
-
<Box direction="row" gap="
|
|
96
|
+
<Box direction="row" gap="md">
|
|
89
97
|
<Button isLoading>Primary Loading</Button>
|
|
90
98
|
<Button variant="secondary" isLoading>
|
|
91
99
|
Secondary Loading
|
|
@@ -97,7 +105,7 @@ export const Loading = () => (
|
|
|
97
105
|
);
|
|
98
106
|
|
|
99
107
|
export const Disabled = () => (
|
|
100
|
-
<Box direction="row" gap="
|
|
108
|
+
<Box direction="row" gap="md">
|
|
101
109
|
<Button variant="primary" isDisabled>
|
|
102
110
|
Primary Disabled
|
|
103
111
|
</Button>
|
|
@@ -111,7 +119,7 @@ export const Disabled = () => (
|
|
|
111
119
|
);
|
|
112
120
|
|
|
113
121
|
export const Shadow = () => (
|
|
114
|
-
<Box direction="row" gap="
|
|
122
|
+
<Box direction="row" gap="md">
|
|
115
123
|
<Button variant="secondary" shadow="xs">
|
|
116
124
|
xs shadow
|
|
117
125
|
</Button>
|
|
@@ -123,11 +131,3 @@ export const Shadow = () => (
|
|
|
123
131
|
</Button>
|
|
124
132
|
</Box>
|
|
125
133
|
);
|
|
126
|
-
|
|
127
|
-
export const Anchor = () => (
|
|
128
|
-
<Box direction="row" gap="sm">
|
|
129
|
-
<Button as="a" href="https://ux.hyphen.ai" target="_blank">
|
|
130
|
-
I'm an anchor tag
|
|
131
|
-
</Button>
|
|
132
|
-
</Box>
|
|
133
|
-
);
|
|
@@ -1,43 +1,40 @@
|
|
|
1
|
+
import React from 'react';
|
|
1
2
|
import { BUTTON_SIZES, BUTTON_VARIANTS } from './Button.constants';
|
|
2
3
|
import { Button, ButtonVariant } from './Button';
|
|
3
4
|
import { fireEvent, render, screen } from '@testing-library/react';
|
|
4
5
|
|
|
5
|
-
import React from 'react';
|
|
6
|
-
|
|
7
6
|
const renderButton = (props = {}) => render(<Button {...props} />);
|
|
8
7
|
const getButton = (text: string): HTMLButtonElement =>
|
|
9
8
|
screen.getByText(text).closest('button') as HTMLButtonElement;
|
|
10
|
-
const getAnchor = (text: string): HTMLAnchorElement =>
|
|
11
|
-
screen.getByText(text).closest('a') as HTMLAnchorElement;
|
|
12
9
|
|
|
13
10
|
describe('Button', () => {
|
|
14
|
-
describe('
|
|
15
|
-
test('
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
11
|
+
describe('Disabled states', () => {
|
|
12
|
+
test('supports controlled isLoading state', () => {
|
|
13
|
+
const { rerender } = renderButton({
|
|
14
|
+
isLoading: true,
|
|
15
|
+
children: 'Loading Button',
|
|
16
|
+
});
|
|
17
|
+
const button = getButton('Loading Button');
|
|
18
|
+
expect(button).toHaveAttribute('aria-disabled', 'true');
|
|
19
|
+
expect(button).toBeDisabled();
|
|
20
|
+
|
|
21
|
+
rerender(<Button isLoading={false} children="Loading Button" />);
|
|
22
|
+
expect(button).not.toHaveAttribute('aria-disabled');
|
|
23
|
+
expect(button).not.toBeDisabled();
|
|
25
24
|
});
|
|
26
25
|
|
|
27
|
-
test('
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test('is not set if as prop is an anchor tag', () => {
|
|
34
|
-
renderButton({
|
|
35
|
-
as: 'a',
|
|
36
|
-
href: 'https://www.hyphen.ai',
|
|
37
|
-
children: 'link button',
|
|
26
|
+
test('supports controlled isDisabled state', () => {
|
|
27
|
+
const { rerender } = renderButton({
|
|
28
|
+
isDisabled: true,
|
|
29
|
+
children: 'Disabled Button',
|
|
38
30
|
});
|
|
39
|
-
const
|
|
40
|
-
expect(
|
|
31
|
+
const button = getButton('Disabled Button');
|
|
32
|
+
expect(button).toHaveAttribute('aria-disabled', 'true');
|
|
33
|
+
expect(button).toBeDisabled();
|
|
34
|
+
|
|
35
|
+
rerender(<Button isDisabled={false} children="Disabled Button" />);
|
|
36
|
+
expect(button).not.toHaveAttribute('aria-disabled');
|
|
37
|
+
expect(button).not.toBeDisabled();
|
|
41
38
|
});
|
|
42
39
|
});
|
|
43
40
|
|
|
@@ -107,6 +104,18 @@ describe('Button', () => {
|
|
|
107
104
|
expect(mockedHandleClick).toHaveBeenCalledTimes(1);
|
|
108
105
|
});
|
|
109
106
|
|
|
107
|
+
test('does not fire onClick callback when disabled', () => {
|
|
108
|
+
const mockedHandleClick = jest.fn();
|
|
109
|
+
renderButton({
|
|
110
|
+
onClick: mockedHandleClick,
|
|
111
|
+
children: 'Click',
|
|
112
|
+
isDisabled: true,
|
|
113
|
+
});
|
|
114
|
+
const buttonElement: HTMLButtonElement = getButton('Click');
|
|
115
|
+
fireEvent.click(buttonElement);
|
|
116
|
+
expect(mockedHandleClick).toHaveBeenCalledTimes(0);
|
|
117
|
+
});
|
|
118
|
+
|
|
110
119
|
test('does not fire function if onClick callback not provided', () => {
|
|
111
120
|
const mockedHandleClick = jest.fn();
|
|
112
121
|
renderButton({ children: 'Click' });
|
|
@@ -139,6 +148,18 @@ describe('Button', () => {
|
|
|
139
148
|
expect(mockedHandleFocus).toHaveBeenCalledTimes(1);
|
|
140
149
|
});
|
|
141
150
|
|
|
151
|
+
test('does not fire onFocus callback when disabled', () => {
|
|
152
|
+
const mockedHandleFocus = jest.fn();
|
|
153
|
+
renderButton({
|
|
154
|
+
onFocus: mockedHandleFocus,
|
|
155
|
+
children: 'Focus',
|
|
156
|
+
isDisabled: true,
|
|
157
|
+
});
|
|
158
|
+
const buttonElement = getButton('Focus');
|
|
159
|
+
fireEvent.focus(buttonElement);
|
|
160
|
+
expect(mockedHandleFocus).toHaveBeenCalledTimes(0);
|
|
161
|
+
});
|
|
162
|
+
|
|
142
163
|
test('does not fire function if onFocus callback not provided', () => {
|
|
143
164
|
const mockedHandleFocus = jest.fn();
|
|
144
165
|
renderButton({ children: 'Focus' });
|
|
@@ -157,6 +178,18 @@ describe('Button', () => {
|
|
|
157
178
|
expect(mockedHandleBlur).toHaveBeenCalledTimes(1);
|
|
158
179
|
});
|
|
159
180
|
|
|
181
|
+
test('does not fire onBlur callback when disabled', () => {
|
|
182
|
+
const mockedHandleBlur = jest.fn();
|
|
183
|
+
renderButton({
|
|
184
|
+
onBlur: mockedHandleBlur,
|
|
185
|
+
children: 'Blur',
|
|
186
|
+
isDisabled: true,
|
|
187
|
+
});
|
|
188
|
+
const buttonElement = getButton('Blur');
|
|
189
|
+
fireEvent.blur(buttonElement);
|
|
190
|
+
expect(mockedHandleBlur).toHaveBeenCalledTimes(0);
|
|
191
|
+
});
|
|
192
|
+
|
|
160
193
|
test('does not fire onBlur callback if not provided', () => {
|
|
161
194
|
const mockedHandleBlur = jest.fn();
|
|
162
195
|
renderButton({ children: 'Blur' });
|
|
@@ -305,104 +338,87 @@ describe('Button', () => {
|
|
|
305
338
|
});
|
|
306
339
|
});
|
|
307
340
|
});
|
|
341
|
+
});
|
|
308
342
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
as: 'a',
|
|
323
|
-
href: 'http://hyphen.ai',
|
|
324
|
-
children: 'hey there',
|
|
325
|
-
});
|
|
326
|
-
const buttonElement = screen.getByRole('link');
|
|
327
|
-
expect(buttonElement).not.toHaveAttribute('type');
|
|
328
|
-
});
|
|
343
|
+
describe('Role Attribute', () => {
|
|
344
|
+
test('applies role attribute', () => {
|
|
345
|
+
renderButton({ role: 'button', children: 'Button with Role' });
|
|
346
|
+
const buttonElement = getButton('Button with Role');
|
|
347
|
+
expect(buttonElement).toHaveAttribute('role', 'button');
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
describe('Aria Attributes', () => {
|
|
351
|
+
test('applies aria-label attribute', () => {
|
|
352
|
+
renderButton({ 'aria-label': 'Aria Label Button', children: 'Button' });
|
|
353
|
+
const buttonElement = getButton('Button');
|
|
354
|
+
expect(buttonElement).toHaveAttribute('aria-label', 'Aria Label Button');
|
|
355
|
+
});
|
|
329
356
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
});
|
|
337
|
-
const buttonElement = screen.getByRole('link');
|
|
338
|
-
expect(buttonElement).toHaveAttribute('target', '_blank');
|
|
339
|
-
});
|
|
357
|
+
test('applies aria-labelledby attribute', () => {
|
|
358
|
+
renderButton({ 'aria-labelledby': 'label-id', children: 'Button' });
|
|
359
|
+
const buttonElement = getButton('Button');
|
|
360
|
+
expect(buttonElement).toHaveAttribute('aria-labelledby', 'label-id');
|
|
361
|
+
});
|
|
362
|
+
});
|
|
340
363
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const buttonElement = screen.getByRole('button');
|
|
348
|
-
expect(buttonElement).not.toHaveAttribute('target');
|
|
349
|
-
});
|
|
364
|
+
describe('Shadow Prop', () => {
|
|
365
|
+
test('applies shadow class when shadow prop is provided', () => {
|
|
366
|
+
renderButton({ shadow: 'lg', children: 'Shadow Button' });
|
|
367
|
+
const buttonElement = getButton('Shadow Button');
|
|
368
|
+
expect(buttonElement).toHaveClass('shadow-lg');
|
|
369
|
+
});
|
|
350
370
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
});
|
|
371
|
+
test('applies responsive shadow classes', () => {
|
|
372
|
+
renderButton({
|
|
373
|
+
shadow: { base: 'sm', tablet: 'md', desktop: 'lg' },
|
|
374
|
+
children: 'Responsive Shadow Button',
|
|
375
|
+
});
|
|
376
|
+
const buttonElement = getButton('Responsive Shadow Button');
|
|
377
|
+
expect(buttonElement).toHaveClass(
|
|
378
|
+
'shadow-sm',
|
|
379
|
+
'shadow-md-tablet',
|
|
380
|
+
'shadow-lg-desktop'
|
|
381
|
+
);
|
|
382
|
+
});
|
|
383
|
+
});
|
|
356
384
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
target: '_blank',
|
|
363
|
-
children: 'Link with rel',
|
|
364
|
-
});
|
|
365
|
-
const anchorElement = getAnchor('Link with rel');
|
|
366
|
-
expect(anchorElement).toHaveAttribute('rel', 'noopener noreferrer');
|
|
367
|
-
});
|
|
385
|
+
describe('As Child', () => {
|
|
386
|
+
test('renders as a different component when asChild is true', () => {
|
|
387
|
+
renderButton({
|
|
388
|
+
asChild: true,
|
|
389
|
+
children: <a href="https://ux.hyphen.ai">Link Button</a>,
|
|
368
390
|
});
|
|
391
|
+
const linkElement = screen.getByRole('link');
|
|
392
|
+
expect(linkElement).toBeInTheDocument();
|
|
393
|
+
expect(linkElement).toHaveAttribute('href', 'https://ux.hyphen.ai');
|
|
369
394
|
});
|
|
370
395
|
});
|
|
371
396
|
|
|
372
|
-
describe('
|
|
373
|
-
test('
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
navigate: mockedNavigate,
|
|
378
|
-
href: '/',
|
|
379
|
-
children: 'react router link',
|
|
380
|
-
});
|
|
381
|
-
const anchorElement = getAnchor('react router link');
|
|
382
|
-
fireEvent.click(anchorElement);
|
|
383
|
-
expect(mockedNavigate).toHaveBeenCalledTimes(1);
|
|
397
|
+
describe('Button Type', () => {
|
|
398
|
+
test('renders with type button', () => {
|
|
399
|
+
renderButton({ children: 'Default Type Button', type: 'button' });
|
|
400
|
+
const buttonElement = getButton('Default Type Button');
|
|
401
|
+
expect(buttonElement).toHaveAttribute('type', 'button');
|
|
384
402
|
});
|
|
385
403
|
|
|
386
|
-
test('
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
fireEvent.click(anchorElement);
|
|
397
|
-
expect(mockedNavigate).toHaveBeenCalledTimes(0);
|
|
404
|
+
test('renders with type submit when specified', () => {
|
|
405
|
+
renderButton({ type: 'submit', children: 'Submit Button' });
|
|
406
|
+
const buttonElement = getButton('Submit Button');
|
|
407
|
+
expect(buttonElement).toHaveAttribute('type', 'submit');
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test('renders with type reset when specified', () => {
|
|
411
|
+
renderButton({ type: 'reset', children: 'Reset Button' });
|
|
412
|
+
const buttonElement = getButton('Reset Button');
|
|
413
|
+
expect(buttonElement).toHaveAttribute('type', 'reset');
|
|
398
414
|
});
|
|
399
415
|
});
|
|
400
416
|
|
|
401
|
-
describe('
|
|
402
|
-
test('
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
expect(
|
|
417
|
+
describe('Button Ref', () => {
|
|
418
|
+
test('forwards ref to the button element', () => {
|
|
419
|
+
const ref = React.createRef<HTMLButtonElement>();
|
|
420
|
+
renderButton({ ref, children: 'Button with Ref' });
|
|
421
|
+
expect(ref.current).toBeInstanceOf(HTMLButtonElement);
|
|
406
422
|
});
|
|
407
423
|
});
|
|
408
424
|
});
|