paris 0.19.0 → 0.20.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/CHANGELOG.md +10 -0
- package/package.json +17 -2
- package/src/stories/accordion/Accordion.test.tsx +140 -0
- package/src/stories/accordionselect/AccordionSelect.test.tsx +252 -0
- package/src/stories/avatar/Avatar.test.tsx +77 -0
- package/src/stories/button/Button.test.tsx +266 -0
- package/src/stories/callout/Callout.test.tsx +79 -0
- package/src/stories/card/Card.test.tsx +81 -0
- package/src/stories/cardbutton/CardButton.test.tsx +174 -0
- package/src/stories/checkbox/Checkbox.test.tsx +531 -0
- package/src/stories/combobox/Combobox.test.tsx +164 -0
- package/src/stories/dialog/Dialog.module.scss +2 -2
- package/src/stories/dialog/Dialog.test.tsx +244 -0
- package/src/stories/drawer/Drawer.module.scss +2 -2
- package/src/stories/drawer/Drawer.test.tsx +259 -0
- package/src/stories/field/Field.test.tsx +146 -0
- package/src/stories/icon/Icon.test.tsx +59 -0
- package/src/stories/informationaltooltip/InformationalTooltip.test.tsx +178 -0
- package/src/stories/input/Input.test.tsx +174 -0
- package/src/stories/markdown/Markdown.test.tsx +228 -0
- package/src/stories/markdowneditor/FixedToolbar.tsx +44 -14
- package/src/stories/markdowneditor/LinkPopover.module.scss +1 -1
- package/src/stories/markdowneditor/MarkdownEditor.stories.tsx +4 -1
- package/src/stories/markdowneditor/MarkdownEditor.test.tsx +115 -0
- package/src/stories/markdowneditor/MarkdownEditor.tsx +11 -1
- package/src/stories/markdowneditor/MarkdownEditorContext.tsx +3 -0
- package/src/stories/markdowneditor/index.ts +1 -0
- package/src/stories/menu/Menu.module.scss +1 -1
- package/src/stories/menu/Menu.test.tsx +211 -0
- package/src/stories/pagination/usePagination.test.ts +259 -0
- package/src/stories/popover/Popover.test.tsx +152 -0
- package/src/stories/select/Select.module.scss +2 -1
- package/src/stories/select/Select.test.tsx +233 -0
- package/src/stories/styledlink/StyledLink.test.tsx +59 -0
- package/src/stories/table/Table.test.tsx +156 -0
- package/src/stories/tabs/Tabs.module.scss +1 -1
- package/src/stories/tabs/Tabs.test.tsx +167 -0
- package/src/stories/tag/Tag.test.tsx +90 -0
- package/src/stories/text/Text.test.tsx +81 -0
- package/src/stories/textarea/TextArea.test.tsx +147 -0
- package/src/stories/theme/themes.ts +16 -0
- package/src/stories/tilt/Tilt.test.tsx +203 -0
- package/src/stories/toast/Toast.test.tsx +86 -0
- package/src/stories/utility/Dropdown.module.scss +1 -1
- package/src/stories/utility/Utility.test.tsx +96 -0
- package/src/test/render.tsx +20 -0
- package/src/test/setup.ts +32 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# paris
|
|
2
2
|
|
|
3
|
+
## 0.20.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- f40892e: Add semantic z-index layer tokens (`Theme.new.layers`) with six named layers: below, sticky, dropdown, overlay, popover, menu. Migrate all hardcoded z-index values to use CSS variable tokens. Fix stacking conflict where SegmentedControl labels bled through GlassTabs sticky bar.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 680364a: MarkdownEditor: fix task list checkbox alignment, fix duplicate link extension, fix link popover not appearing, add handleImageUpload prop for file-based image insertion, match checkbox styling to Paris Checkbox component, use Lucide icons for toolbar, and update documentation.
|
|
12
|
+
|
|
3
13
|
## 0.19.0
|
|
4
14
|
|
|
5
15
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "paris",
|
|
3
3
|
"author": "Sanil Chawla <sanil@slingshot.fm> (https://sanil.co)",
|
|
4
4
|
"description": "Paris is Slingshot's React design system. It's a collection of reusable components, design tokens, and guidelines that help us build consistent, accessible, and performant user interfaces.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.20.0",
|
|
6
6
|
"homepage": "https://paris.slingshot.fm",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"repository": {
|
|
@@ -36,6 +36,12 @@
|
|
|
36
36
|
"generate:text": "ts-node --esm ./scripts/text.ts",
|
|
37
37
|
"release": "bun run generate:exports && bunx changeset publish",
|
|
38
38
|
"prepare": "test -n \"$CI\" || lefthook install",
|
|
39
|
+
"test": "vitest run --project unit",
|
|
40
|
+
"test:watch": "vitest --project unit",
|
|
41
|
+
"test:coverage": "vitest run --project unit --coverage",
|
|
42
|
+
"test:storybook": "vitest run --project storybook",
|
|
43
|
+
"test:all": "vitest run",
|
|
44
|
+
"test:coverage-check": "node scripts/checkTestCoverage.js",
|
|
39
45
|
"typecheck": "tsc --noEmit",
|
|
40
46
|
"lint": "biome check .",
|
|
41
47
|
"lint:fix": "biome check --write .",
|
|
@@ -128,18 +134,26 @@
|
|
|
128
134
|
"@ssh/csstypes": "^1.1.0",
|
|
129
135
|
"@storybook/addon-docs": "10.3.4",
|
|
130
136
|
"@storybook/addon-links": "10.3.4",
|
|
137
|
+
"@storybook/addon-vitest": "10.3.4",
|
|
131
138
|
"@storybook/nextjs-vite": "10.3.4",
|
|
139
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
140
|
+
"@testing-library/react": "^16.3.2",
|
|
141
|
+
"@testing-library/user-event": "^14.6.1",
|
|
132
142
|
"@types/node": "^22.0.0",
|
|
133
143
|
"@types/react": "^19",
|
|
134
144
|
"@types/react-dom": "^19",
|
|
145
|
+
"@vitest/browser-playwright": "4.1.2",
|
|
146
|
+
"@vitest/coverage-v8": "^4.1.2",
|
|
135
147
|
"autoprefixer": "^10.4.14",
|
|
136
148
|
"change-case": "^4.1.2",
|
|
137
149
|
"csstype": "^3.1.2",
|
|
138
150
|
"esbuild-sass-plugin": "^2.16.0",
|
|
151
|
+
"jsdom": "^29.0.1",
|
|
139
152
|
"jss": "^10.10.0",
|
|
140
153
|
"jss-preset-default": "^10.10.0",
|
|
141
154
|
"lefthook": "^1.11.13",
|
|
142
155
|
"next": "^16.2.2",
|
|
156
|
+
"playwright": "^1.59.1",
|
|
143
157
|
"react": "^19.0.0",
|
|
144
158
|
"react-dom": "^19.0.0",
|
|
145
159
|
"sass": "^1.62.1",
|
|
@@ -150,6 +164,7 @@
|
|
|
150
164
|
"tsup": "^7.2.0",
|
|
151
165
|
"type-fest": "^3.10.0",
|
|
152
166
|
"typescript": "^5.2.2",
|
|
153
|
-
"vite": "^7.0.0"
|
|
167
|
+
"vite": "^7.0.0",
|
|
168
|
+
"vitest": "^4.1.2"
|
|
154
169
|
}
|
|
155
170
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { render, screen, waitFor } from '../../test/render';
|
|
2
|
+
import { Accordion } from './Accordion';
|
|
3
|
+
|
|
4
|
+
describe('Accordion', () => {
|
|
5
|
+
it('renders with a title', () => {
|
|
6
|
+
render(<Accordion title="My Accordion" />);
|
|
7
|
+
expect(screen.getByText('My Accordion')).toBeInTheDocument();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('does not show children when collapsed', () => {
|
|
11
|
+
render(<Accordion title="Title">Hidden content</Accordion>);
|
|
12
|
+
expect(screen.queryByText('Hidden content')).not.toBeInTheDocument();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('expands on click to reveal children', async () => {
|
|
16
|
+
const { user } = render(<Accordion title="Title">Revealed content</Accordion>);
|
|
17
|
+
await user.click(screen.getByRole('button'));
|
|
18
|
+
expect(screen.getByText('Revealed content')).toBeInTheDocument();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('collapses on second click', async () => {
|
|
22
|
+
const { user } = render(<Accordion title="Title">Toggle content</Accordion>);
|
|
23
|
+
const button = screen.getByRole('button');
|
|
24
|
+
|
|
25
|
+
await user.click(button);
|
|
26
|
+
expect(screen.getByText('Toggle content')).toBeInTheDocument();
|
|
27
|
+
|
|
28
|
+
await user.click(button);
|
|
29
|
+
// AnimatePresence exit animation may keep element mounted briefly
|
|
30
|
+
await waitFor(() => {
|
|
31
|
+
expect(screen.queryByText('Toggle content')).not.toBeInTheDocument();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('expands on Enter key', async () => {
|
|
36
|
+
const { user } = render(<Accordion title="Title">Keyboard content</Accordion>);
|
|
37
|
+
screen.getByRole('button').focus();
|
|
38
|
+
await user.keyboard('{Enter}');
|
|
39
|
+
expect(screen.getByText('Keyboard content')).toBeInTheDocument();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('expands on Space key', async () => {
|
|
43
|
+
const { user } = render(<Accordion title="Title">Space content</Accordion>);
|
|
44
|
+
screen.getByRole('button').focus();
|
|
45
|
+
await user.keyboard(' ');
|
|
46
|
+
expect(screen.getByText('Space content')).toBeInTheDocument();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('calls onOpenChange when toggled (uncontrolled)', async () => {
|
|
50
|
+
const onChange = vi.fn();
|
|
51
|
+
const { user } = render(
|
|
52
|
+
<Accordion title="Title" onOpenChange={onChange}>
|
|
53
|
+
Content
|
|
54
|
+
</Accordion>,
|
|
55
|
+
);
|
|
56
|
+
await user.click(screen.getByRole('button'));
|
|
57
|
+
expect(onChange).toHaveBeenCalledWith(true);
|
|
58
|
+
|
|
59
|
+
await user.click(screen.getByRole('button'));
|
|
60
|
+
expect(onChange).toHaveBeenCalledWith(false);
|
|
61
|
+
expect(onChange).toHaveBeenCalledTimes(2);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('works as a controlled component', async () => {
|
|
65
|
+
const onChange = vi.fn();
|
|
66
|
+
const { rerender, user } = render(
|
|
67
|
+
<Accordion title="Title" isOpen={false} onOpenChange={onChange}>
|
|
68
|
+
Controlled content
|
|
69
|
+
</Accordion>,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
expect(screen.queryByText('Controlled content')).not.toBeInTheDocument();
|
|
73
|
+
|
|
74
|
+
// Open externally
|
|
75
|
+
rerender(
|
|
76
|
+
<Accordion title="Title" isOpen={true} onOpenChange={onChange}>
|
|
77
|
+
Controlled content
|
|
78
|
+
</Accordion>,
|
|
79
|
+
);
|
|
80
|
+
expect(screen.getByText('Controlled content')).toBeInTheDocument();
|
|
81
|
+
|
|
82
|
+
// Click should call onOpenChange but not change state (controlled)
|
|
83
|
+
await user.click(screen.getByRole('button'));
|
|
84
|
+
expect(onChange).toHaveBeenCalledWith(false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('renders with kind="card"', () => {
|
|
88
|
+
render(
|
|
89
|
+
<Accordion title="Card Accordion" kind="card">
|
|
90
|
+
Card content
|
|
91
|
+
</Accordion>,
|
|
92
|
+
);
|
|
93
|
+
expect(screen.getByText('Card Accordion')).toBeInTheDocument();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('renders with kind="default" by default', () => {
|
|
97
|
+
const { container } = render(<Accordion title="Default" />);
|
|
98
|
+
expect(container.firstElementChild).toHaveClass('default');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('applies custom className via overrides', () => {
|
|
102
|
+
const { container } = render(
|
|
103
|
+
<Accordion title="Styled" overrides={{ container: { className: 'custom-class' } }}>
|
|
104
|
+
Styled content
|
|
105
|
+
</Accordion>,
|
|
106
|
+
);
|
|
107
|
+
expect(container.firstElementChild).toHaveClass('custom-class');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('sets data-state attribute on the title container', () => {
|
|
111
|
+
render(<Accordion title="Title">Content</Accordion>);
|
|
112
|
+
const button = screen.getByRole('button');
|
|
113
|
+
expect(button).toHaveAttribute('data-state', 'closed');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('updates data-state to open when expanded', async () => {
|
|
117
|
+
const { user } = render(<Accordion title="Title">Content</Accordion>);
|
|
118
|
+
const button = screen.getByRole('button');
|
|
119
|
+
await user.click(button);
|
|
120
|
+
expect(button).toHaveAttribute('data-state', 'open');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('renders with size="large" for card kind', () => {
|
|
124
|
+
render(
|
|
125
|
+
<Accordion title="Large Card" kind="card" size="large">
|
|
126
|
+
Large content
|
|
127
|
+
</Accordion>,
|
|
128
|
+
);
|
|
129
|
+
expect(screen.getByRole('button')).toHaveClass('large');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('starts open when isOpen is true initially', () => {
|
|
133
|
+
render(
|
|
134
|
+
<Accordion title="Pre-opened" isOpen={true}>
|
|
135
|
+
Visible from start
|
|
136
|
+
</Accordion>,
|
|
137
|
+
);
|
|
138
|
+
expect(screen.getByText('Visible from start')).toBeInTheDocument();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { render, screen, waitFor } from '../../test/render';
|
|
3
|
+
import type { AccordionSelectOption } from './AccordionSelect';
|
|
4
|
+
import { AccordionSelect } from './AccordionSelect';
|
|
5
|
+
|
|
6
|
+
const options: AccordionSelectOption[] = [
|
|
7
|
+
{ id: 'champagne', node: 'In an alleyway, drinking champagne' },
|
|
8
|
+
{ id: 'rooftop', node: 'On a rooftop, watching the sunset' },
|
|
9
|
+
{ id: 'garden', node: 'In a garden, under the stars' },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
function ControlledAccordionSelect(props: Partial<React.ComponentProps<typeof AccordionSelect>>) {
|
|
13
|
+
const [value, setValue] = useState<string | null>((props.value as string | null) ?? null);
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<AccordionSelect
|
|
17
|
+
options={options}
|
|
18
|
+
value={value}
|
|
19
|
+
onChange={(opt) => {
|
|
20
|
+
setValue(opt.id);
|
|
21
|
+
props.onChange?.(opt);
|
|
22
|
+
}}
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('AccordionSelect', () => {
|
|
29
|
+
it('renders with placeholder when no value is selected', () => {
|
|
30
|
+
render(<AccordionSelect options={options} placeholder="Where were we?" />);
|
|
31
|
+
expect(screen.getByText('Where were we?')).toBeInTheDocument();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('renders default placeholder when none is provided', () => {
|
|
35
|
+
render(<AccordionSelect options={options} />);
|
|
36
|
+
expect(screen.getByText('Select an option')).toBeInTheDocument();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('displays the selected option in the header', () => {
|
|
40
|
+
render(<AccordionSelect options={options} value="champagne" />);
|
|
41
|
+
expect(screen.getByText('In an alleyway, drinking champagne')).toBeInTheDocument();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('expands to show all options when header is clicked', async () => {
|
|
45
|
+
const { user } = render(<AccordionSelect options={options} value="champagne" />);
|
|
46
|
+
|
|
47
|
+
await user.click(screen.getByRole('button'));
|
|
48
|
+
|
|
49
|
+
await waitFor(() => {
|
|
50
|
+
expect(screen.getByText('On a rooftop, watching the sunset')).toBeInTheDocument();
|
|
51
|
+
expect(screen.getByText('In a garden, under the stars')).toBeInTheDocument();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('calls onChange when an option is selected', async () => {
|
|
56
|
+
const handleChange = vi.fn();
|
|
57
|
+
const { user } = render(<ControlledAccordionSelect value="champagne" onChange={handleChange} />);
|
|
58
|
+
|
|
59
|
+
await user.click(screen.getByRole('button', { name: /champagne/i }));
|
|
60
|
+
|
|
61
|
+
await waitFor(() => {
|
|
62
|
+
expect(screen.getByText('On a rooftop, watching the sunset')).toBeInTheDocument();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await user.click(screen.getByText('On a rooftop, watching the sunset'));
|
|
66
|
+
|
|
67
|
+
expect(handleChange).toHaveBeenCalledWith(
|
|
68
|
+
expect.objectContaining({ id: 'rooftop', node: 'On a rooftop, watching the sunset' }),
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('closes the accordion after selection when closeOnSelect is true (default)', async () => {
|
|
73
|
+
const { user } = render(<ControlledAccordionSelect value="champagne" />);
|
|
74
|
+
|
|
75
|
+
await user.click(screen.getByRole('button', { name: /champagne/i }));
|
|
76
|
+
await waitFor(() => {
|
|
77
|
+
expect(screen.getByText('On a rooftop, watching the sunset')).toBeInTheDocument();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await user.click(screen.getByText('On a rooftop, watching the sunset'));
|
|
81
|
+
|
|
82
|
+
await waitFor(() => {
|
|
83
|
+
expect(screen.queryByRole('button', { name: /garden/i })).not.toBeInTheDocument();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('keeps the accordion open after selection when closeOnSelect is false', async () => {
|
|
88
|
+
const { user } = render(<ControlledAccordionSelect value="champagne" closeOnSelect={false} />);
|
|
89
|
+
|
|
90
|
+
await user.click(screen.getByRole('button', { name: /champagne/i }));
|
|
91
|
+
await waitFor(() => {
|
|
92
|
+
expect(screen.getByText('On a rooftop, watching the sunset')).toBeInTheDocument();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
await user.click(screen.getByText('On a rooftop, watching the sunset'));
|
|
96
|
+
|
|
97
|
+
expect(screen.getByText('In a garden, under the stars')).toBeInTheDocument();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('toggles open and closed on header click', async () => {
|
|
101
|
+
const { user } = render(<AccordionSelect options={options} value="champagne" />);
|
|
102
|
+
|
|
103
|
+
const header = screen.getByRole('button');
|
|
104
|
+
|
|
105
|
+
// Open
|
|
106
|
+
await user.click(header);
|
|
107
|
+
await waitFor(() => {
|
|
108
|
+
expect(screen.getByText('On a rooftop, watching the sunset')).toBeInTheDocument();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Close
|
|
112
|
+
await user.click(header);
|
|
113
|
+
await waitFor(() => {
|
|
114
|
+
expect(screen.queryByRole('button', { name: /rooftop/i })).not.toBeInTheDocument();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('supports keyboard interaction on the header (Enter)', async () => {
|
|
119
|
+
const { user } = render(<AccordionSelect options={options} value="champagne" />);
|
|
120
|
+
|
|
121
|
+
const header = screen.getByRole('button');
|
|
122
|
+
header.focus();
|
|
123
|
+
await user.keyboard('{Enter}');
|
|
124
|
+
|
|
125
|
+
await waitFor(() => {
|
|
126
|
+
expect(screen.getByText('On a rooftop, watching the sunset')).toBeInTheDocument();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('supports keyboard interaction on the header (Space)', async () => {
|
|
131
|
+
const { user } = render(<AccordionSelect options={options} value="champagne" />);
|
|
132
|
+
|
|
133
|
+
const header = screen.getByRole('button');
|
|
134
|
+
header.focus();
|
|
135
|
+
await user.keyboard(' ');
|
|
136
|
+
|
|
137
|
+
await waitFor(() => {
|
|
138
|
+
expect(screen.getByText('On a rooftop, watching the sunset')).toBeInTheDocument();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('renders disabled options as disabled buttons', async () => {
|
|
143
|
+
const disabledOptions = [...options, { id: 'nowhere', node: 'Nowhere, it was all a dream', disabled: true }];
|
|
144
|
+
const { user } = render(<AccordionSelect options={disabledOptions} value="champagne" />);
|
|
145
|
+
|
|
146
|
+
await user.click(screen.getByRole('button', { name: /champagne/i }));
|
|
147
|
+
|
|
148
|
+
await waitFor(() => {
|
|
149
|
+
const disabledButton = screen.getByText('Nowhere, it was all a dream').closest('button');
|
|
150
|
+
expect(disabledButton).toBeDisabled();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('calls onOpenChange when the accordion opens or closes', async () => {
|
|
155
|
+
const handleOpenChange = vi.fn();
|
|
156
|
+
const { user, container } = render(
|
|
157
|
+
<AccordionSelect options={options} value="champagne" onOpenChange={handleOpenChange} />,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// Click the header (the div[role=button][tabindex=0]) to open
|
|
161
|
+
const header = container.querySelector('[role="button"][tabindex="0"]') as HTMLElement;
|
|
162
|
+
await user.click(header);
|
|
163
|
+
expect(handleOpenChange).toHaveBeenCalledWith(true);
|
|
164
|
+
|
|
165
|
+
// Click the header again to close
|
|
166
|
+
await user.click(header);
|
|
167
|
+
expect(handleOpenChange).toHaveBeenCalledWith(false);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('renders custom content via renderSelected', () => {
|
|
171
|
+
render(
|
|
172
|
+
<AccordionSelect
|
|
173
|
+
options={options}
|
|
174
|
+
value="champagne"
|
|
175
|
+
renderSelected={(opt) => <span data-testid="custom-selected">{opt.id}</span>}
|
|
176
|
+
/>,
|
|
177
|
+
);
|
|
178
|
+
expect(screen.getByTestId('custom-selected')).toHaveTextContent('champagne');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('renders custom content via renderOption', async () => {
|
|
182
|
+
const { user } = render(
|
|
183
|
+
<AccordionSelect
|
|
184
|
+
options={options}
|
|
185
|
+
value="champagne"
|
|
186
|
+
renderOption={(opt, isSelected) => (
|
|
187
|
+
<span data-testid={`custom-opt-${opt.id}`}>
|
|
188
|
+
{opt.id} {isSelected ? '(selected)' : ''}
|
|
189
|
+
</span>
|
|
190
|
+
)}
|
|
191
|
+
/>,
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
await user.click(screen.getByRole('button'));
|
|
195
|
+
|
|
196
|
+
await waitFor(() => {
|
|
197
|
+
expect(screen.getByTestId('custom-opt-champagne')).toHaveTextContent('champagne (selected)');
|
|
198
|
+
expect(screen.getByTestId('custom-opt-rooftop')).toHaveTextContent('rooftop');
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('renders an action at the bottom of the dropdown', async () => {
|
|
203
|
+
const { user } = render(
|
|
204
|
+
<AccordionSelect options={options} value="champagne" action={<button type="button">Add new</button>} />,
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
await user.click(screen.getByRole('button', { name: /champagne/i }));
|
|
208
|
+
|
|
209
|
+
await waitFor(() => {
|
|
210
|
+
expect(screen.getByText('Add new')).toBeInTheDocument();
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('renders a label in the header', () => {
|
|
215
|
+
render(
|
|
216
|
+
<AccordionSelect
|
|
217
|
+
options={options}
|
|
218
|
+
value="champagne"
|
|
219
|
+
label={<span data-testid="header-label">Active</span>}
|
|
220
|
+
/>,
|
|
221
|
+
);
|
|
222
|
+
expect(screen.getByTestId('header-label')).toHaveTextContent('Active');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('closes on outside click when closeOnClickOutside is true (default)', async () => {
|
|
226
|
+
const { user } = render(
|
|
227
|
+
<div>
|
|
228
|
+
<AccordionSelect options={options} value="champagne" />
|
|
229
|
+
<button type="button">Outside</button>
|
|
230
|
+
</div>,
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
// Open
|
|
234
|
+
await user.click(screen.getByRole('button', { name: /champagne/i }));
|
|
235
|
+
await waitFor(() => {
|
|
236
|
+
expect(screen.getByText('On a rooftop, watching the sunset')).toBeInTheDocument();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Click outside
|
|
240
|
+
await user.click(screen.getByText('Outside'));
|
|
241
|
+
await waitFor(() => {
|
|
242
|
+
expect(screen.queryByRole('button', { name: /rooftop/i })).not.toBeInTheDocument();
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('respects controlled isOpen prop', () => {
|
|
247
|
+
render(<AccordionSelect options={options} value="champagne" isOpen />);
|
|
248
|
+
// When isOpen=true, dropdown content should be rendered
|
|
249
|
+
expect(screen.getByText('On a rooftop, watching the sunset')).toBeInTheDocument();
|
|
250
|
+
expect(screen.getByText('In a garden, under the stars')).toBeInTheDocument();
|
|
251
|
+
});
|
|
252
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { render, screen } from '../../test/render';
|
|
2
|
+
import { Avatar } from './Avatar';
|
|
3
|
+
|
|
4
|
+
describe('Avatar', () => {
|
|
5
|
+
it('renders children', () => {
|
|
6
|
+
render(
|
|
7
|
+
<Avatar>
|
|
8
|
+
<img src="/photo.jpg" alt="User" />
|
|
9
|
+
</Avatar>,
|
|
10
|
+
);
|
|
11
|
+
expect(screen.getByAltText('User')).toBeInTheDocument();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('renders text children (initials)', () => {
|
|
15
|
+
render(<Avatar>AB</Avatar>);
|
|
16
|
+
expect(screen.getByText('AB')).toBeInTheDocument();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('applies content class', () => {
|
|
20
|
+
const { container } = render(<Avatar>AB</Avatar>);
|
|
21
|
+
expect(container.firstElementChild).toHaveClass('content');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('forwards className', () => {
|
|
25
|
+
const { container } = render(<Avatar className="extra">AB</Avatar>);
|
|
26
|
+
expect(container.firstElementChild).toHaveClass('content', 'extra');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('sets fit-content width by default', () => {
|
|
30
|
+
const { container } = render(<Avatar>AB</Avatar>);
|
|
31
|
+
expect(container.firstElementChild).toHaveStyle({ width: 'fit-content' });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('sets numeric width in pixels', () => {
|
|
35
|
+
const { container } = render(<Avatar width={48}>AB</Avatar>);
|
|
36
|
+
expect(container.firstElementChild).toHaveStyle({ width: '48px' });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('sets string width as-is', () => {
|
|
40
|
+
const { container } = render(<Avatar width="3rem">AB</Avatar>);
|
|
41
|
+
expect(container.firstElementChild).toHaveStyle({ width: '3rem' });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('applies frame color as CSS variable', () => {
|
|
45
|
+
const { container } = render(<Avatar frameColor="red">AB</Avatar>);
|
|
46
|
+
const el = container.firstElementChild as HTMLElement;
|
|
47
|
+
expect(el.style.getPropertyValue('--frame-color')).toBe('red');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('applies default frame color CSS variable', () => {
|
|
51
|
+
const { container } = render(<Avatar>AB</Avatar>);
|
|
52
|
+
const el = container.firstElementChild as HTMLElement;
|
|
53
|
+
// Default is pvar('new.colors.borderMedium') which produces a var() string
|
|
54
|
+
expect(el.style.getPropertyValue('--frame-color')).toBeTruthy();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('forwards HTML div attributes', () => {
|
|
58
|
+
render(
|
|
59
|
+
<Avatar data-testid="avatar" id="avatar-1">
|
|
60
|
+
AB
|
|
61
|
+
</Avatar>,
|
|
62
|
+
);
|
|
63
|
+
const avatar = screen.getByTestId('avatar');
|
|
64
|
+
expect(avatar).toHaveAttribute('id', 'avatar-1');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('merges custom style with computed styles', () => {
|
|
68
|
+
const { container } = render(
|
|
69
|
+
<Avatar style={{ border: '2px solid blue' }} width={64}>
|
|
70
|
+
AB
|
|
71
|
+
</Avatar>,
|
|
72
|
+
);
|
|
73
|
+
const el = container.firstElementChild as HTMLElement;
|
|
74
|
+
expect(el.style.border).toBe('2px solid blue');
|
|
75
|
+
expect(el.style.width).toBe('64px');
|
|
76
|
+
});
|
|
77
|
+
});
|