tharaday 0.7.6 → 0.8.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/ds.css +1 -1
- package/dist/ds.js +752 -702
- package/dist/ds.umd.cjs +1 -1
- package/dist/src/components/Button/Button.d.ts +1 -1
- package/dist/src/components/Button/Button.stories.d.ts +1 -1
- package/dist/src/components/ProgressBar/ProgressBar.types.d.ts +1 -1
- package/dist/src/components/Select/Select.types.d.ts +10 -4
- package/dist/src/components/Table/Table.d.ts +1 -1
- package/dist/src/components/Table/Table.stories.d.ts +1 -1
- package/dist/src/components/Table/Table.types.d.ts +1 -0
- package/dist/src/components/Text/Text.d.ts +1 -1
- package/dist/src/components/Text/Text.stories.d.ts +1 -1
- package/dist/src/components/Text/Text.types.d.ts +2 -2
- package/dist/src/hooks/useComponentId.d.ts +5 -0
- package/package.json +1 -1
- package/src/components/Accordion/Accordion.tsx +4 -3
- package/src/components/Avatar/Avatar.test.tsx +10 -0
- package/src/components/Avatar/Avatar.tsx +8 -1
- package/src/components/Box/Box.test.tsx +84 -0
- package/src/components/Breadcrumbs/Breadcrumbs.tsx +2 -2
- package/src/components/Button/Button.test.tsx +10 -0
- package/src/components/Button/Button.tsx +2 -1
- package/src/components/Card/Card.test.tsx +85 -0
- package/src/components/Checkbox/Checkbox.tsx +2 -3
- package/src/components/Divider/Divider.test.tsx +41 -0
- package/src/components/Dropdown/Dropdown.module.css +16 -1
- package/src/components/Dropdown/Dropdown.test.tsx +9 -0
- package/src/components/Dropdown/Dropdown.tsx +42 -10
- package/src/components/Header/Header.test.tsx +69 -0
- package/src/components/Input/Input.tsx +2 -3
- package/src/components/Loader/Loader.test.tsx +50 -0
- package/src/components/Modal/Modal.test.tsx +5 -0
- package/src/components/Modal/Modal.tsx +4 -3
- package/src/components/NavBar/NavBar.test.tsx +63 -0
- package/src/components/Pagination/Pagination.tsx +5 -4
- package/src/components/ProgressBar/ProgressBar.module.css +4 -0
- package/src/components/ProgressBar/ProgressBar.test.tsx +5 -0
- package/src/components/ProgressBar/ProgressBar.tsx +2 -3
- package/src/components/ProgressBar/ProgressBar.types.ts +1 -1
- package/src/components/RadioButton/RadioButton.tsx +2 -3
- package/src/components/Select/Select.tsx +2 -3
- package/src/components/Select/Select.types.ts +6 -4
- package/src/components/Skeleton/Skeleton.test.tsx +61 -0
- package/src/components/Slider/Slider.test.tsx +23 -0
- package/src/components/Slider/Slider.tsx +20 -7
- package/src/components/Stepper/Stepper.tsx +2 -3
- package/src/components/Switch/Switch.tsx +2 -3
- package/src/components/Table/Table.module.css +8 -0
- package/src/components/Table/Table.test.tsx +18 -0
- package/src/components/Table/Table.tsx +2 -0
- package/src/components/Table/Table.types.ts +1 -0
- package/src/components/Tabs/Tabs.tsx +4 -3
- package/src/components/Text/Text.test.tsx +60 -0
- package/src/components/Text/Text.tsx +42 -15
- package/src/components/Text/Text.types.ts +3 -2
- package/src/components/Textarea/Textarea.tsx +2 -3
- package/src/components/Tooltip/Tooltip.tsx +2 -3
- package/src/components/Tree/Tree.test.tsx +39 -0
- package/src/hooks/useComponentId.ts +10 -0
|
@@ -8,13 +8,15 @@ export interface SelectOption {
|
|
|
8
8
|
disabled?: boolean;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
type SelectBaseProps = Omit<SelectHTMLAttributes<HTMLSelectElement>, 'size'> & {
|
|
12
12
|
className?: string;
|
|
13
13
|
size?: SelectSize;
|
|
14
14
|
error?: boolean;
|
|
15
15
|
label?: string;
|
|
16
16
|
helperText?: string;
|
|
17
|
-
options?: SelectOption[];
|
|
18
|
-
children?: ReactNode;
|
|
19
17
|
fullWidth?: boolean;
|
|
20
|
-
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type SelectProps =
|
|
21
|
+
| (SelectBaseProps & { options: SelectOption[]; children?: never })
|
|
22
|
+
| (SelectBaseProps & { options?: never; children?: ReactNode });
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { render } from '@testing-library/react';
|
|
2
|
+
|
|
3
|
+
import { Skeleton } from './Skeleton.tsx';
|
|
4
|
+
|
|
5
|
+
describe('Skeleton', () => {
|
|
6
|
+
it('renders a div', () => {
|
|
7
|
+
const { container } = render(<Skeleton />);
|
|
8
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('is aria-hidden by default', () => {
|
|
12
|
+
const { container } = render(<Skeleton />);
|
|
13
|
+
expect(container.firstChild).toHaveAttribute('aria-hidden', 'true');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('allows overriding aria-hidden', () => {
|
|
17
|
+
const { container } = render(<Skeleton aria-hidden={false} />);
|
|
18
|
+
expect(container.firstChild).toHaveAttribute('aria-hidden', 'false');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('applies rectangular variant class by default', () => {
|
|
22
|
+
const { container } = render(<Skeleton />);
|
|
23
|
+
expect(container.firstChild).toHaveClass('rectangular');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('applies circular variant class', () => {
|
|
27
|
+
const { container } = render(<Skeleton variant="circular" />);
|
|
28
|
+
expect(container.firstChild).toHaveClass('circular');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('applies text variant class', () => {
|
|
32
|
+
const { container } = render(<Skeleton variant="text" />);
|
|
33
|
+
expect(container.firstChild).toHaveClass('text');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('applies pulse animation class by default', () => {
|
|
37
|
+
const { container } = render(<Skeleton />);
|
|
38
|
+
expect(container.firstChild).toHaveClass('pulse');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('applies wave animation class', () => {
|
|
42
|
+
const { container } = render(<Skeleton animation="wave" />);
|
|
43
|
+
expect(container.firstChild).toHaveClass('wave');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('applies no animation class when animation="none"', () => {
|
|
47
|
+
const { container } = render(<Skeleton animation="none" />);
|
|
48
|
+
expect(container.firstChild).not.toHaveClass('pulse');
|
|
49
|
+
expect(container.firstChild).not.toHaveClass('wave');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('applies custom width as inline style', () => {
|
|
53
|
+
const { container } = render(<Skeleton width="200px" />);
|
|
54
|
+
expect((container.firstChild as HTMLElement).style.width).toBe('200px');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('applies custom height as inline style', () => {
|
|
58
|
+
const { container } = render(<Skeleton height="40px" />);
|
|
59
|
+
expect((container.firstChild as HTMLElement).style.height).toBe('40px');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -46,4 +46,27 @@ describe('Slider', () => {
|
|
|
46
46
|
render(<Slider defaultValue={[20, 80]} />);
|
|
47
47
|
expect(screen.getAllByRole('slider')).toHaveLength(2);
|
|
48
48
|
});
|
|
49
|
+
|
|
50
|
+
it('renders a number input when showInputs is true', () => {
|
|
51
|
+
render(<Slider showInputs />);
|
|
52
|
+
expect(screen.getByRole('spinbutton')).toBeInTheDocument();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('renders two number inputs for dual range with showInputs', () => {
|
|
56
|
+
render(<Slider defaultValue={[20, 80]} showInputs />);
|
|
57
|
+
expect(screen.getAllByRole('spinbutton')).toHaveLength(2);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('labels the dual range inputs correctly', () => {
|
|
61
|
+
render(<Slider defaultValue={[20, 80]} showInputs label="Price" />);
|
|
62
|
+
expect(screen.getByLabelText('Price minimum input')).toBeInTheDocument();
|
|
63
|
+
expect(screen.getByLabelText('Price maximum input')).toBeInTheDocument();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('shows aria-valuetext on both range inputs when showValue is true', () => {
|
|
67
|
+
render(<Slider defaultValue={[10, 90]} showValue />);
|
|
68
|
+
const [start, end] = screen.getAllByRole('slider');
|
|
69
|
+
expect(start).toHaveAttribute('aria-valuetext', '10 - 90');
|
|
70
|
+
expect(end).toHaveAttribute('aria-valuetext', '10 - 90');
|
|
71
|
+
});
|
|
49
72
|
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import classnames from 'classnames';
|
|
2
|
-
import {
|
|
2
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
import { useComponentId } from '../../hooks/useComponentId.ts';
|
|
3
5
|
|
|
4
6
|
import { Input } from '../Input/Input.tsx';
|
|
5
7
|
import styles from './Slider.module.css';
|
|
@@ -59,8 +61,7 @@ export const Slider = ({
|
|
|
59
61
|
onValueChange,
|
|
60
62
|
...props
|
|
61
63
|
}: SliderProps) => {
|
|
62
|
-
const
|
|
63
|
-
const componentId = id ?? `ds-slider-${baseId}`;
|
|
64
|
+
const componentId = useComponentId('slider', id);
|
|
64
65
|
const helperId = helperText ? `${componentId}-help` : undefined;
|
|
65
66
|
const min = toNumber(props.min as number | string | undefined, 0);
|
|
66
67
|
const max = toNumber(props.max as number | string | undefined, 100);
|
|
@@ -75,6 +76,17 @@ export const Slider = ({
|
|
|
75
76
|
const [internalPair, setInternalPair] = useState<[number, number]>(initialPair);
|
|
76
77
|
const resolvedPair = value != null ? normalizeToPair(value, min, max) : internalPair;
|
|
77
78
|
const [startValue, endValue] = resolvedPair;
|
|
79
|
+
|
|
80
|
+
const [startInputValue, setStartInputValue] = useState(String(startValue));
|
|
81
|
+
const [endInputValue, setEndInputValue] = useState(String(endValue));
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
setStartInputValue(String(startValue));
|
|
85
|
+
}, [startValue]);
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
setEndInputValue(String(endValue));
|
|
89
|
+
}, [endValue]);
|
|
78
90
|
const showValueText = isDual ? `${startValue} - ${endValue}` : String(startValue);
|
|
79
91
|
const rangeSpan = Math.max(max - min, 1);
|
|
80
92
|
const leftPercent = ((startValue - min) / rangeSpan) * 100;
|
|
@@ -164,6 +176,7 @@ export const Slider = ({
|
|
|
164
176
|
className={classnames(styles.inputRoot, styles.inputEnd)}
|
|
165
177
|
aria-describedby={helperId}
|
|
166
178
|
aria-label={label ? `${label} maximum` : 'Slider maximum'}
|
|
179
|
+
aria-valuetext={showValue ? showValueText : undefined}
|
|
167
180
|
{...props}
|
|
168
181
|
min={min}
|
|
169
182
|
max={max}
|
|
@@ -179,11 +192,11 @@ export const Slider = ({
|
|
|
179
192
|
{showInputs && (
|
|
180
193
|
<div className={classnames(styles.inputsRow, !isDual && styles.singleInputRow)}>
|
|
181
194
|
<Input
|
|
182
|
-
key={`slider-start-${startValue}-${endValue}`}
|
|
183
195
|
type="number"
|
|
184
196
|
inputMode="decimal"
|
|
185
197
|
size={size}
|
|
186
|
-
|
|
198
|
+
value={startInputValue}
|
|
199
|
+
onChange={(event) => setStartInputValue(event.target.value)}
|
|
187
200
|
min={min}
|
|
188
201
|
max={isDual ? endValue : max}
|
|
189
202
|
step={step}
|
|
@@ -203,11 +216,11 @@ export const Slider = ({
|
|
|
203
216
|
{isDual && <span className={styles.separator}>-</span>}
|
|
204
217
|
{isDual && (
|
|
205
218
|
<Input
|
|
206
|
-
key={`slider-end-${startValue}-${endValue}`}
|
|
207
219
|
type="number"
|
|
208
220
|
inputMode="decimal"
|
|
209
221
|
size={size}
|
|
210
|
-
|
|
222
|
+
value={endInputValue}
|
|
223
|
+
onChange={(event) => setEndInputValue(event.target.value)}
|
|
211
224
|
min={startValue}
|
|
212
225
|
max={max}
|
|
213
226
|
step={step}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import classnames from 'classnames';
|
|
2
|
-
import {
|
|
2
|
+
import { useComponentId } from '../../hooks/useComponentId.ts';
|
|
3
3
|
|
|
4
4
|
import { Step } from './Step.tsx';
|
|
5
5
|
import styles from './Stepper.module.css';
|
|
@@ -17,8 +17,7 @@ export const Stepper = ({
|
|
|
17
17
|
id,
|
|
18
18
|
...props
|
|
19
19
|
}: StepperProps) => {
|
|
20
|
-
const
|
|
21
|
-
const componentId = id ?? `ds-stepper-${baseId}`;
|
|
20
|
+
const componentId = useComponentId('stepper', id);
|
|
22
21
|
const currentIndex = resolveCurrentIndex(currentStep, steps);
|
|
23
22
|
const listLabel = ariaLabel ?? 'Progress';
|
|
24
23
|
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import classnames from 'classnames';
|
|
2
|
-
import {
|
|
2
|
+
import { useComponentId } from '../../hooks/useComponentId.ts';
|
|
3
3
|
|
|
4
4
|
import styles from './Switch.module.css';
|
|
5
5
|
import type { SwitchProps } from './Switch.types.ts';
|
|
6
6
|
|
|
7
7
|
export const Switch = ({ label, helperText, className, disabled, id, ...props }: SwitchProps) => {
|
|
8
|
-
const
|
|
9
|
-
const componentId = id ?? `ds-switch-${baseId}`;
|
|
8
|
+
const componentId = useComponentId('switch', id);
|
|
10
9
|
const helperId = helperText ? `${componentId}-help` : undefined;
|
|
11
10
|
|
|
12
11
|
return (
|
|
@@ -72,6 +72,14 @@
|
|
|
72
72
|
text-align: justify;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
.caption {
|
|
76
|
+
caption-side: top;
|
|
77
|
+
padding: var(--ds-space-3) var(--ds-space-4);
|
|
78
|
+
font-size: var(--ds-font-size-sm);
|
|
79
|
+
color: var(--ds-text-2);
|
|
80
|
+
text-align: left;
|
|
81
|
+
}
|
|
82
|
+
|
|
75
83
|
.loading {
|
|
76
84
|
opacity: 0.7;
|
|
77
85
|
pointer-events: none;
|
|
@@ -63,6 +63,24 @@ describe('Table', () => {
|
|
|
63
63
|
expect(screen.getByRole('table')).not.toHaveAttribute('aria-busy');
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
+
it('renders a caption when provided', () => {
|
|
67
|
+
render(
|
|
68
|
+
<Table caption="User list">
|
|
69
|
+
<TableBody>
|
|
70
|
+
<TableRow>
|
|
71
|
+
<TableCell>Alice</TableCell>
|
|
72
|
+
</TableRow>
|
|
73
|
+
</TableBody>
|
|
74
|
+
</Table>
|
|
75
|
+
);
|
|
76
|
+
expect(screen.getByText('User list')).toBeInTheDocument();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('does not render a caption element when caption is omitted', () => {
|
|
80
|
+
render(<BasicTable />);
|
|
81
|
+
expect(document.querySelector('caption')).not.toBeInTheDocument();
|
|
82
|
+
});
|
|
83
|
+
|
|
66
84
|
it('renders a footer', () => {
|
|
67
85
|
render(
|
|
68
86
|
<Table>
|
|
@@ -18,6 +18,7 @@ export const Table = ({
|
|
|
18
18
|
hoverable,
|
|
19
19
|
dense,
|
|
20
20
|
isLoading,
|
|
21
|
+
caption,
|
|
21
22
|
...props
|
|
22
23
|
}: TableProps) => {
|
|
23
24
|
return (
|
|
@@ -32,6 +33,7 @@ export const Table = ({
|
|
|
32
33
|
aria-busy={isLoading || undefined}
|
|
33
34
|
{...props}
|
|
34
35
|
>
|
|
36
|
+
{caption && <caption className={styles.caption}>{caption}</caption>}
|
|
35
37
|
{children}
|
|
36
38
|
</table>
|
|
37
39
|
</div>
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import classnames from 'classnames';
|
|
2
|
-
import {
|
|
2
|
+
import { useState, useRef, type KeyboardEvent } from 'react';
|
|
3
|
+
|
|
4
|
+
import { useComponentId } from '../../hooks/useComponentId.ts';
|
|
3
5
|
|
|
4
6
|
import styles from './Tabs.module.css';
|
|
5
7
|
import type { TabsProps } from './Tabs.types.ts';
|
|
@@ -13,8 +15,7 @@ export const Tabs = ({
|
|
|
13
15
|
variant = 'line',
|
|
14
16
|
id,
|
|
15
17
|
}: TabsProps) => {
|
|
16
|
-
const
|
|
17
|
-
const componentId = id ?? `ds-tabs-${baseId}`;
|
|
18
|
+
const componentId = useComponentId('tabs', id);
|
|
18
19
|
const [uncontrolledActiveId, setUncontrolledActiveId] = useState(
|
|
19
20
|
defaultActiveId || (items.length > 0 ? items[0].id : '')
|
|
20
21
|
);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
|
|
3
|
+
import { Text } from './Text.tsx';
|
|
4
|
+
|
|
5
|
+
describe('Text', () => {
|
|
6
|
+
it('renders children', () => {
|
|
7
|
+
render(<Text>Hello</Text>);
|
|
8
|
+
expect(screen.getByText('Hello')).toBeInTheDocument();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('renders as a <p> by default (body-md variant)', () => {
|
|
12
|
+
const { container } = render(<Text>Content</Text>);
|
|
13
|
+
expect(container.querySelector('p')).toBeInTheDocument();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('renders as h1 for variant="h1"', () => {
|
|
17
|
+
render(<Text variant="h1">Title</Text>);
|
|
18
|
+
expect(screen.getByRole('heading', { level: 1, name: 'Title' })).toBeInTheDocument();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('renders as h2 for variant="h2"', () => {
|
|
22
|
+
render(<Text variant="h2">Subtitle</Text>);
|
|
23
|
+
expect(screen.getByRole('heading', { level: 2, name: 'Subtitle' })).toBeInTheDocument();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('renders as code element for variant="code"', () => {
|
|
27
|
+
const { container } = render(<Text variant="code">const x = 1</Text>);
|
|
28
|
+
expect(container.querySelector('code')).toBeInTheDocument();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('renders as a custom element via as prop', () => {
|
|
32
|
+
const { container } = render(<Text as="span">Span text</Text>);
|
|
33
|
+
expect(container.querySelector('span')).toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('applies numeric padding as a CSS class', () => {
|
|
37
|
+
const { container } = render(<Text padding={4}>Text</Text>);
|
|
38
|
+
expect(container.firstChild).toHaveClass('p-4');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('applies string padding as inline style', () => {
|
|
42
|
+
const { container } = render(<Text padding="1.5rem">Text</Text>);
|
|
43
|
+
expect((container.firstChild as HTMLElement).style.padding).toBe('1.5rem');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('applies numeric margin as a CSS class', () => {
|
|
47
|
+
const { container } = render(<Text margin={2}>Text</Text>);
|
|
48
|
+
expect(container.firstChild).toHaveClass('margin-2');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('applies noWrap class when noWrap is true', () => {
|
|
52
|
+
const { container } = render(<Text noWrap>Text</Text>);
|
|
53
|
+
expect(container.firstChild).toHaveClass('noWrap');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('forwards className', () => {
|
|
57
|
+
const { container } = render(<Text className="custom">Text</Text>);
|
|
58
|
+
expect(container.firstChild).toHaveClass('custom');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import classnames from 'classnames';
|
|
2
|
-
import type { ElementType } from 'react';
|
|
2
|
+
import type { CSSProperties, ElementType } from 'react';
|
|
3
3
|
|
|
4
|
+
import { getSpacingStyles } from '../Box/helpers/getSpacingStyles.ts';
|
|
4
5
|
import styles from './Text.module.css';
|
|
5
6
|
import type { TextProps, TextVariant } from './Text.types.ts';
|
|
6
7
|
|
|
@@ -27,6 +28,7 @@ export const Text = ({
|
|
|
27
28
|
color,
|
|
28
29
|
noWrap,
|
|
29
30
|
className,
|
|
31
|
+
style,
|
|
30
32
|
padding,
|
|
31
33
|
paddingX,
|
|
32
34
|
paddingY,
|
|
@@ -45,6 +47,30 @@ export const Text = ({
|
|
|
45
47
|
}: TextProps) => {
|
|
46
48
|
const Component = as || variantElementMap[variant] || 'span';
|
|
47
49
|
|
|
50
|
+
const spacingStyles: CSSProperties = {
|
|
51
|
+
...style,
|
|
52
|
+
...getSpacingStyles(
|
|
53
|
+
'padding',
|
|
54
|
+
padding,
|
|
55
|
+
paddingX,
|
|
56
|
+
paddingY,
|
|
57
|
+
paddingTop,
|
|
58
|
+
paddingBottom,
|
|
59
|
+
paddingLeft,
|
|
60
|
+
paddingRight
|
|
61
|
+
),
|
|
62
|
+
...getSpacingStyles(
|
|
63
|
+
'margin',
|
|
64
|
+
margin,
|
|
65
|
+
marginX,
|
|
66
|
+
marginY,
|
|
67
|
+
marginTop,
|
|
68
|
+
marginBottom,
|
|
69
|
+
marginLeft,
|
|
70
|
+
marginRight
|
|
71
|
+
),
|
|
72
|
+
};
|
|
73
|
+
|
|
48
74
|
return (
|
|
49
75
|
<Component
|
|
50
76
|
className={classnames(
|
|
@@ -54,22 +80,23 @@ export const Text = ({
|
|
|
54
80
|
weight && styles[weight],
|
|
55
81
|
color && styles[`color-${color}`],
|
|
56
82
|
noWrap && styles.noWrap,
|
|
57
|
-
padding
|
|
58
|
-
paddingX
|
|
59
|
-
paddingY
|
|
60
|
-
paddingTop
|
|
61
|
-
paddingBottom
|
|
62
|
-
paddingLeft
|
|
63
|
-
paddingRight
|
|
64
|
-
margin
|
|
65
|
-
marginX
|
|
66
|
-
marginY
|
|
67
|
-
marginTop
|
|
68
|
-
marginBottom
|
|
69
|
-
marginLeft
|
|
70
|
-
marginRight
|
|
83
|
+
typeof padding === 'number' && styles[`p-${padding}`],
|
|
84
|
+
typeof paddingX === 'number' && styles[`px-${paddingX}`],
|
|
85
|
+
typeof paddingY === 'number' && styles[`py-${paddingY}`],
|
|
86
|
+
typeof paddingTop === 'number' && styles[`pt-${paddingTop}`],
|
|
87
|
+
typeof paddingBottom === 'number' && styles[`pb-${paddingBottom}`],
|
|
88
|
+
typeof paddingLeft === 'number' && styles[`pl-${paddingLeft}`],
|
|
89
|
+
typeof paddingRight === 'number' && styles[`pr-${paddingRight}`],
|
|
90
|
+
typeof margin === 'number' && styles[`margin-${margin}`],
|
|
91
|
+
typeof marginX === 'number' && styles[`marginX-${marginX}`],
|
|
92
|
+
typeof marginY === 'number' && styles[`marginY-${marginY}`],
|
|
93
|
+
typeof marginTop === 'number' && styles[`marginTop-${marginTop}`],
|
|
94
|
+
typeof marginBottom === 'number' && styles[`marginBottom-${marginBottom}`],
|
|
95
|
+
typeof marginLeft === 'number' && styles[`marginLeft-${marginLeft}`],
|
|
96
|
+
typeof marginRight === 'number' && styles[`marginRight-${marginRight}`],
|
|
71
97
|
className
|
|
72
98
|
)}
|
|
99
|
+
style={spacingStyles}
|
|
73
100
|
{...props}
|
|
74
101
|
>
|
|
75
102
|
{children}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { ReactNode, ElementType, HTMLAttributes } from 'react';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import type { BoxPadding, BoxMargin } from '../Box/Box.types.ts';
|
|
4
|
+
|
|
5
|
+
export type { BoxPadding, BoxMargin };
|
|
5
6
|
|
|
6
7
|
export type TextVariant =
|
|
7
8
|
| 'h1'
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import classnames from 'classnames';
|
|
2
|
-
import {
|
|
2
|
+
import { useComponentId } from '../../hooks/useComponentId.ts';
|
|
3
3
|
|
|
4
4
|
import styles from './Textarea.module.css';
|
|
5
5
|
import type { TextareaProps } from './Textarea.types.ts';
|
|
@@ -15,8 +15,7 @@ export const Textarea = ({
|
|
|
15
15
|
rows = 4,
|
|
16
16
|
...props
|
|
17
17
|
}: TextareaProps) => {
|
|
18
|
-
const
|
|
19
|
-
const componentId = id ?? `ds-textarea-${baseId}`;
|
|
18
|
+
const componentId = useComponentId('textarea', id);
|
|
20
19
|
const helperId = helperText ? `${componentId}-help` : undefined;
|
|
21
20
|
|
|
22
21
|
return (
|
|
@@ -2,13 +2,13 @@ import classnames from 'classnames';
|
|
|
2
2
|
import {
|
|
3
3
|
useState,
|
|
4
4
|
useRef,
|
|
5
|
-
useId,
|
|
6
5
|
isValidElement,
|
|
7
6
|
cloneElement,
|
|
8
7
|
type KeyboardEvent,
|
|
9
8
|
type ReactElement,
|
|
10
9
|
} from 'react';
|
|
11
10
|
|
|
11
|
+
import { useComponentId } from '../../hooks/useComponentId.ts';
|
|
12
12
|
import styles from './Tooltip.module.css';
|
|
13
13
|
import type { TooltipProps } from './Tooltip.types.ts';
|
|
14
14
|
|
|
@@ -23,8 +23,7 @@ export const Tooltip = ({
|
|
|
23
23
|
}: TooltipProps) => {
|
|
24
24
|
const [isVisible, setIsVisible] = useState(false);
|
|
25
25
|
const timeoutRef = useRef<number | null>(null);
|
|
26
|
-
const
|
|
27
|
-
const componentId = id ?? `ds-tooltip-${baseId}`;
|
|
26
|
+
const componentId = useComponentId('tooltip', id);
|
|
28
27
|
const tooltipId = `${componentId}-content`;
|
|
29
28
|
|
|
30
29
|
const showTooltip = () => {
|
|
@@ -113,4 +113,43 @@ describe('Tree', () => {
|
|
|
113
113
|
render(<Tree data={{ val: null }} />);
|
|
114
114
|
expect(screen.getByText('null')).toBeInTheDocument();
|
|
115
115
|
});
|
|
116
|
+
|
|
117
|
+
it('moves focus to first item with Home key', async () => {
|
|
118
|
+
render(<Tree data={objectData} />);
|
|
119
|
+
const items = screen.getAllByRole('treeitem');
|
|
120
|
+
items[2].tabIndex = 0;
|
|
121
|
+
items[2].focus();
|
|
122
|
+
await userEvent.keyboard('{Home}');
|
|
123
|
+
expect(document.activeElement).toBe(items[0]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('moves focus to last item with End key', async () => {
|
|
127
|
+
render(<Tree data={objectData} />);
|
|
128
|
+
const items = screen.getAllByRole('treeitem');
|
|
129
|
+
items[0].focus();
|
|
130
|
+
await userEvent.keyboard('{End}');
|
|
131
|
+
expect(document.activeElement).toBe(items[items.length - 1]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('sets aria-setsize and aria-posinset on top-level items', () => {
|
|
135
|
+
render(<Tree data={objectData} />);
|
|
136
|
+
const items = screen.getAllByRole('treeitem');
|
|
137
|
+
const topLevel = items.filter((el) => el.getAttribute('aria-level') === '1');
|
|
138
|
+
topLevel.forEach((item, i) => {
|
|
139
|
+
expect(item).toHaveAttribute('aria-setsize', String(topLevel.length));
|
|
140
|
+
expect(item).toHaveAttribute('aria-posinset', String(i + 1));
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('sets aria-level="2" on nested items when expanded', async () => {
|
|
145
|
+
render(<Tree data={objectData} />);
|
|
146
|
+
const addressItem = screen
|
|
147
|
+
.getAllByRole('treeitem')
|
|
148
|
+
.find((el) => el.textContent?.includes('address'))!;
|
|
149
|
+
await userEvent.click(addressItem);
|
|
150
|
+
const nested = screen
|
|
151
|
+
.getAllByRole('treeitem')
|
|
152
|
+
.filter((el) => el.getAttribute('aria-level') === '2');
|
|
153
|
+
expect(nested.length).toBeGreaterThan(0);
|
|
154
|
+
});
|
|
116
155
|
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useId } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generates a stable, unique component ID.
|
|
5
|
+
* Returns the provided id if given, otherwise creates one prefixed with `ds-{prefix}-`.
|
|
6
|
+
*/
|
|
7
|
+
export const useComponentId = (prefix: string, id?: string): string => {
|
|
8
|
+
const baseId = useId();
|
|
9
|
+
return id ?? `ds-${prefix}-${baseId}`;
|
|
10
|
+
};
|