paris 0.22.1 → 0.22.2
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 +35 -0
- package/package.json +43 -44
- package/src/stories/accordion/Accordion.module.scss +32 -2
- package/src/stories/accordion/Accordion.test.tsx +7 -5
- package/src/stories/accordion/Accordion.tsx +28 -33
- package/src/stories/accordionselect/AccordionSelect.module.scss +13 -1
- package/src/stories/accordionselect/AccordionSelect.test.tsx +6 -3
- package/src/stories/accordionselect/AccordionSelect.tsx +50 -55
- package/src/stories/text/Text.module.scss +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# paris
|
|
2
2
|
|
|
3
|
+
## 0.22.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 5173ecf: Fix scroll snap when collapsing `Accordion` and `AccordionSelect` inside a scrollable parent.
|
|
8
|
+
|
|
9
|
+
When the dropdown was opened, the user scrolled past it, and then closed it,
|
|
10
|
+
the scrollable ancestor would instantly snap to the position it would occupy
|
|
11
|
+
once the dropdown was fully closed — while the visual collapse animation was
|
|
12
|
+
still running. The cause was framer-motion's `height: 'auto' → 0` exit
|
|
13
|
+
animation thrashing the layout in the first paint frame, which the browser
|
|
14
|
+
responded to by clamping `scrollTop`.
|
|
15
|
+
|
|
16
|
+
The collapse animation now uses the CSS grid-rows trick (`grid-template-rows:
|
|
17
|
+
1fr` → `0fr`) instead. Layout stays stable across the entire transition, so
|
|
18
|
+
`scrollTop` clamps smoothly in step with the animation. Duration and easing
|
|
19
|
+
match the previous behavior (800ms, `cubic-bezier(0.87, 0, 0.13, 1)`).
|
|
20
|
+
|
|
21
|
+
One small behavior change: collapsed content remains in the DOM (it was
|
|
22
|
+
previously unmounted by `AnimatePresence`). The container is marked
|
|
23
|
+
`aria-hidden` when closed and option buttons receive `tabIndex={-1}`, so
|
|
24
|
+
screen readers and keyboard navigation continue to skip hidden content.
|
|
25
|
+
|
|
26
|
+
- 5173ecf: Update runtime dependencies to current semver-compatible versions and patch
|
|
27
|
+
security advisories. Notable bumps: Tiptap 3.22 → 3.23, Framer Motion 12.24
|
|
28
|
+
→ 12.40, Headless UI 2.2.4 → 2.2.10, Ariakit 0.4.20 → 0.4.28, react-hot-toast
|
|
29
|
+
2.4 → 2.6, lucide-react 1.7 → 1.16, ts-deepmerge 6.0 → 6.2. No API changes
|
|
30
|
+
expected, but consumers will pick up the newer transitives on install.
|
|
31
|
+
- 5173ecf: `<Text fontStyle="italic">` is now reliably italic. Mirror the pattern already
|
|
32
|
+
used by weight classes and apply `!important` to `.fontStyle-*` rules.
|
|
33
|
+
Without it, the per-kind typography classes (e.g. `.paragraphSmall { font-style: normal }`,
|
|
34
|
+
emitted when consumers define per-style `font-style` theme variables) win
|
|
35
|
+
over `.fontStyle-italic` and suppress italic on `<Text>` and anything that
|
|
36
|
+
delegates to it — notably `<Markdown>` rendering `<em>`.
|
|
37
|
+
|
|
3
38
|
## 0.22.1
|
|
4
39
|
|
|
5
40
|
### Patch 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.22.
|
|
5
|
+
"version": "0.22.2",
|
|
6
6
|
"homepage": "https://paris.slingshot.fm",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"repository": {
|
|
@@ -85,38 +85,38 @@
|
|
|
85
85
|
"./utility": "./src/stories/utility/index.ts"
|
|
86
86
|
},
|
|
87
87
|
"dependencies": {
|
|
88
|
-
"@ariakit/react": "^0.4.
|
|
88
|
+
"@ariakit/react": "^0.4.28",
|
|
89
89
|
"@emotion/is-prop-valid": "^1.4.0",
|
|
90
90
|
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
|
91
91
|
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
|
92
92
|
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
|
93
|
-
"@fortawesome/react-fontawesome": "^0.2.
|
|
94
|
-
"@headlessui/react": "^2.2.
|
|
93
|
+
"@fortawesome/react-fontawesome": "^0.2.6",
|
|
94
|
+
"@headlessui/react": "^2.2.10",
|
|
95
95
|
"@radix-ui/react-checkbox": "^1.3.3",
|
|
96
96
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
97
|
-
"@tiptap/extension-image": "^3.
|
|
98
|
-
"@tiptap/extension-link": "^3.
|
|
99
|
-
"@tiptap/extension-placeholder": "^3.
|
|
100
|
-
"@tiptap/extension-table": "^3.
|
|
101
|
-
"@tiptap/extension-table-cell": "^3.
|
|
102
|
-
"@tiptap/extension-table-header": "^3.
|
|
103
|
-
"@tiptap/extension-table-row": "^3.
|
|
104
|
-
"@tiptap/extension-task-item": "^3.
|
|
105
|
-
"@tiptap/extension-task-list": "^3.
|
|
106
|
-
"@tiptap/markdown": "^3.
|
|
107
|
-
"@tiptap/react": "^3.
|
|
108
|
-
"@tiptap/starter-kit": "^3.
|
|
97
|
+
"@tiptap/extension-image": "^3.23.6",
|
|
98
|
+
"@tiptap/extension-link": "^3.23.6",
|
|
99
|
+
"@tiptap/extension-placeholder": "^3.23.6",
|
|
100
|
+
"@tiptap/extension-table": "^3.23.6",
|
|
101
|
+
"@tiptap/extension-table-cell": "^3.23.6",
|
|
102
|
+
"@tiptap/extension-table-header": "^3.23.6",
|
|
103
|
+
"@tiptap/extension-table-row": "^3.23.6",
|
|
104
|
+
"@tiptap/extension-task-item": "^3.23.6",
|
|
105
|
+
"@tiptap/extension-task-list": "^3.23.6",
|
|
106
|
+
"@tiptap/markdown": "^3.23.6",
|
|
107
|
+
"@tiptap/react": "^3.23.6",
|
|
108
|
+
"@tiptap/starter-kit": "^3.23.6",
|
|
109
109
|
"clsx": "^1.2.1",
|
|
110
110
|
"font-color-contrast": "^11.1.0",
|
|
111
|
-
"framer-motion": "^12.
|
|
112
|
-
"lucide-react": "^1.
|
|
111
|
+
"framer-motion": "^12.40.0",
|
|
112
|
+
"lucide-react": "^1.16.0",
|
|
113
113
|
"pte": "^0.5.0",
|
|
114
|
-
"react-hot-toast": "^2.
|
|
114
|
+
"react-hot-toast": "^2.6.0",
|
|
115
115
|
"react-markdown": "^10.1.0",
|
|
116
116
|
"react-tiny-popover": "^8.1.6",
|
|
117
117
|
"rehype-raw": "^7.0.0",
|
|
118
118
|
"remark-gfm": "^4.0.1",
|
|
119
|
-
"ts-deepmerge": "^6.
|
|
119
|
+
"ts-deepmerge": "^6.2.1"
|
|
120
120
|
},
|
|
121
121
|
"peerDependencies": {
|
|
122
122
|
"@types/react": "^19",
|
|
@@ -127,10 +127,10 @@
|
|
|
127
127
|
"typescript": "^5.0"
|
|
128
128
|
},
|
|
129
129
|
"devDependencies": {
|
|
130
|
-
"@biomejs/biome": "^2.
|
|
131
|
-
"@changesets/cli": "^2.
|
|
132
|
-
"@commitlint/cli": "^
|
|
133
|
-
"@commitlint/config-conventional": "^
|
|
130
|
+
"@biomejs/biome": "^2.4.15",
|
|
131
|
+
"@changesets/cli": "^2.31.0",
|
|
132
|
+
"@commitlint/cli": "^21",
|
|
133
|
+
"@commitlint/config-conventional": "^21",
|
|
134
134
|
"@ssh/csstypes": "^1.1.0",
|
|
135
135
|
"@storybook/addon-docs": "10.3.4",
|
|
136
136
|
"@storybook/addon-links": "10.3.4",
|
|
@@ -139,32 +139,31 @@
|
|
|
139
139
|
"@testing-library/jest-dom": "^6.9.1",
|
|
140
140
|
"@testing-library/react": "^16.3.2",
|
|
141
141
|
"@testing-library/user-event": "^14.6.1",
|
|
142
|
-
"@types/node": "^22.
|
|
143
|
-
"@types/react": "^19",
|
|
144
|
-
"@types/react-dom": "^19",
|
|
142
|
+
"@types/node": "^22.19.19",
|
|
143
|
+
"@types/react": "^19.2.15",
|
|
144
|
+
"@types/react-dom": "^19.2.3",
|
|
145
145
|
"@vitest/browser-playwright": "4.1.2",
|
|
146
|
-
"@vitest/coverage-v8": "^4.1.
|
|
147
|
-
"autoprefixer": "^10.
|
|
146
|
+
"@vitest/coverage-v8": "^4.1.7",
|
|
147
|
+
"autoprefixer": "^10.5.0",
|
|
148
148
|
"change-case": "^4.1.2",
|
|
149
|
-
"csstype": "^3.
|
|
150
|
-
"
|
|
151
|
-
"jsdom": "^29.0.1",
|
|
149
|
+
"csstype": "^3.2.3",
|
|
150
|
+
"jsdom": "^29.1.1",
|
|
152
151
|
"jss": "^10.10.0",
|
|
153
152
|
"jss-preset-default": "^10.10.0",
|
|
154
|
-
"lefthook": "^1.
|
|
155
|
-
"next": "^16.2.
|
|
156
|
-
"playwright": "^1.
|
|
157
|
-
"react": "^19.
|
|
158
|
-
"react-dom": "^19.
|
|
159
|
-
"sass": "^1.
|
|
153
|
+
"lefthook": "^1.13.6",
|
|
154
|
+
"next": "^16.2.6",
|
|
155
|
+
"playwright": "^1.60.0",
|
|
156
|
+
"react": "^19.2.6",
|
|
157
|
+
"react-dom": "^19.2.6",
|
|
158
|
+
"sass": "^1.100.0",
|
|
160
159
|
"storybook": "10.3.4",
|
|
161
160
|
"storybook-dark-mode": "^5.0.0",
|
|
162
161
|
"title-case": "^3.0.3",
|
|
163
|
-
"ts-node": "^10.9.
|
|
164
|
-
"tsup": "^
|
|
165
|
-
"type-fest": "^3.
|
|
166
|
-
"typescript": "^5.
|
|
167
|
-
"vite": "^7.
|
|
168
|
-
"vitest": "^4.1.
|
|
162
|
+
"ts-node": "^10.9.2",
|
|
163
|
+
"tsup": "^8",
|
|
164
|
+
"type-fest": "^3.13.1",
|
|
165
|
+
"typescript": "^5.9.3",
|
|
166
|
+
"vite": "^7.3.3",
|
|
167
|
+
"vitest": "^4.1.7"
|
|
169
168
|
}
|
|
170
169
|
}
|
|
@@ -39,8 +39,23 @@
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
.dropdown {
|
|
42
|
-
|
|
42
|
+
display: grid;
|
|
43
|
+
grid-template-rows: 0fr;
|
|
44
|
+
opacity: 0;
|
|
43
45
|
cursor: auto;
|
|
46
|
+
transition:
|
|
47
|
+
grid-template-rows 800ms cubic-bezier(0.87, 0, 0.13, 1),
|
|
48
|
+
opacity 800ms cubic-bezier(0.87, 0, 0.13, 1);
|
|
49
|
+
|
|
50
|
+
&.open {
|
|
51
|
+
grid-template-rows: 1fr;
|
|
52
|
+
opacity: 1;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.dropdownClip {
|
|
57
|
+
min-height: 0;
|
|
58
|
+
overflow: hidden;
|
|
44
59
|
}
|
|
45
60
|
|
|
46
61
|
.dropdownContent {
|
|
@@ -100,8 +115,23 @@
|
|
|
100
115
|
}
|
|
101
116
|
|
|
102
117
|
.dropdown {
|
|
103
|
-
|
|
118
|
+
display: grid;
|
|
119
|
+
grid-template-rows: 0fr;
|
|
120
|
+
opacity: 0;
|
|
104
121
|
cursor: auto;
|
|
122
|
+
transition:
|
|
123
|
+
grid-template-rows 800ms cubic-bezier(0.87, 0, 0.13, 1),
|
|
124
|
+
opacity 800ms cubic-bezier(0.87, 0, 0.13, 1);
|
|
125
|
+
|
|
126
|
+
&.open {
|
|
127
|
+
grid-template-rows: 1fr;
|
|
128
|
+
opacity: 1;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.dropdownClip {
|
|
133
|
+
min-height: 0;
|
|
134
|
+
overflow: hidden;
|
|
105
135
|
}
|
|
106
136
|
|
|
107
137
|
.dropdownContent {
|
|
@@ -9,7 +9,9 @@ describe('Accordion', () => {
|
|
|
9
9
|
|
|
10
10
|
it('does not show children when collapsed', () => {
|
|
11
11
|
render(<Accordion title="Title">Hidden content</Accordion>);
|
|
12
|
-
|
|
12
|
+
// Content is in the DOM but hidden via aria-hidden on the collapsed dropdown
|
|
13
|
+
const content = screen.getByText('Hidden content');
|
|
14
|
+
expect(content.closest('[aria-hidden]')).toHaveAttribute('aria-hidden', 'true');
|
|
13
15
|
});
|
|
14
16
|
|
|
15
17
|
it('expands on click to reveal children', async () => {
|
|
@@ -26,9 +28,9 @@ describe('Accordion', () => {
|
|
|
26
28
|
expect(screen.getByText('Toggle content')).toBeInTheDocument();
|
|
27
29
|
|
|
28
30
|
await user.click(button);
|
|
29
|
-
// AnimatePresence exit animation may keep element mounted briefly
|
|
30
31
|
await waitFor(() => {
|
|
31
|
-
|
|
32
|
+
const content = screen.getByText('Toggle content');
|
|
33
|
+
expect(content.closest('[aria-hidden]')).toHaveAttribute('aria-hidden', 'true');
|
|
32
34
|
});
|
|
33
35
|
});
|
|
34
36
|
|
|
@@ -69,7 +71,7 @@ describe('Accordion', () => {
|
|
|
69
71
|
</Accordion>,
|
|
70
72
|
);
|
|
71
73
|
|
|
72
|
-
expect(screen.
|
|
74
|
+
expect(screen.getByText('Controlled content').closest('[aria-hidden]')).toHaveAttribute('aria-hidden', 'true');
|
|
73
75
|
|
|
74
76
|
// Open externally
|
|
75
77
|
rerender(
|
|
@@ -77,7 +79,7 @@ describe('Accordion', () => {
|
|
|
77
79
|
Controlled content
|
|
78
80
|
</Accordion>,
|
|
79
81
|
);
|
|
80
|
-
expect(screen.getByText('Controlled content')).
|
|
82
|
+
expect(screen.getByText('Controlled content').closest('[aria-hidden]')).toHaveAttribute('aria-hidden', 'false');
|
|
81
83
|
|
|
82
84
|
// Click should call onOpenChange but not change state (controlled)
|
|
83
85
|
await user.click(screen.getByRole('button'));
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { faPlus } from '@fortawesome/free-solid-svg-icons';
|
|
2
2
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
3
3
|
import { clsx } from 'clsx';
|
|
4
|
-
import type { MotionProps } from 'framer-motion';
|
|
5
|
-
import { AnimatePresence, motion } from 'framer-motion';
|
|
6
4
|
import type { ComponentPropsWithoutRef, FC, ReactNode } from 'react';
|
|
7
5
|
import { useState } from 'react';
|
|
8
6
|
import { ChevronRight, Icon } from '../icon';
|
|
@@ -31,7 +29,7 @@ export type AccordionProps = {
|
|
|
31
29
|
overrides?: {
|
|
32
30
|
container?: ComponentPropsWithoutRef<'div'>;
|
|
33
31
|
titleContainer?: ComponentPropsWithoutRef<'div'>;
|
|
34
|
-
dropdownContainer?: ComponentPropsWithoutRef<'div'
|
|
32
|
+
dropdownContainer?: ComponentPropsWithoutRef<'div'>;
|
|
35
33
|
dropdownContent?: ComponentPropsWithoutRef<'div'>;
|
|
36
34
|
};
|
|
37
35
|
};
|
|
@@ -107,37 +105,34 @@ export const Accordion: FC<AccordionProps> = ({
|
|
|
107
105
|
<Icon icon={ChevronRight} size={16} className={clsx(styles.chevron, open && styles.open)} />
|
|
108
106
|
)}
|
|
109
107
|
</div>
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
108
|
+
{/*
|
|
109
|
+
* Collapse uses the CSS grid-rows trick (`1fr` → `0fr`) instead of
|
|
110
|
+
* animating `height: auto` via JS. JS height-auto animations flash an
|
|
111
|
+
* intermediate layout state on open/close that causes scrollable
|
|
112
|
+
* ancestors to clamp `scrollTop` to 0 in the first paint frame — a
|
|
113
|
+
* visible scroll-snap before the animation starts.
|
|
114
|
+
*/}
|
|
115
|
+
<div
|
|
116
|
+
aria-hidden={!open}
|
|
117
|
+
{...overrides?.dropdownContainer}
|
|
118
|
+
className={clsx(styles.dropdown, open && styles.open, overrides?.dropdownContainer?.className)}
|
|
119
|
+
>
|
|
120
|
+
{/*
|
|
121
|
+
* dropdownClip is the grid item. It owns `min-height: 0` and
|
|
122
|
+
* `overflow: hidden` so the parent's `grid-template-rows: 0fr`
|
|
123
|
+
* can fully collapse to 0 height. Padding/background-color
|
|
124
|
+
* stay on .dropdownContent (one level deeper) so they don't
|
|
125
|
+
* extend the grid item's box when closed.
|
|
126
|
+
*/}
|
|
127
|
+
<div className={styles.dropdownClip}>
|
|
128
|
+
<div
|
|
129
|
+
{...overrides?.dropdownContent}
|
|
130
|
+
className={clsx(styles.dropdownContent, styles[size], overrides?.dropdownContent?.className)}
|
|
127
131
|
>
|
|
128
|
-
<
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
styles[size],
|
|
133
|
-
overrides?.dropdownContent?.className,
|
|
134
|
-
)}
|
|
135
|
-
>
|
|
136
|
-
<TextWhenString kind="paragraphXSmall">{children}</TextWhenString>
|
|
137
|
-
</div>
|
|
138
|
-
</motion.div>
|
|
139
|
-
)}
|
|
140
|
-
</AnimatePresence>
|
|
132
|
+
<TextWhenString kind="paragraphXSmall">{children}</TextWhenString>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
141
136
|
</div>
|
|
142
137
|
);
|
|
143
138
|
};
|
|
@@ -53,10 +53,22 @@
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
.dropdown {
|
|
56
|
-
|
|
56
|
+
display: grid;
|
|
57
|
+
grid-template-rows: 0fr;
|
|
58
|
+
opacity: 0;
|
|
59
|
+
transition:
|
|
60
|
+
grid-template-rows 800ms cubic-bezier(0.87, 0, 0.13, 1),
|
|
61
|
+
opacity 800ms cubic-bezier(0.87, 0, 0.13, 1);
|
|
62
|
+
|
|
63
|
+
&.open {
|
|
64
|
+
grid-template-rows: 1fr;
|
|
65
|
+
opacity: 1;
|
|
66
|
+
}
|
|
57
67
|
}
|
|
58
68
|
|
|
59
69
|
.dropdownContent {
|
|
70
|
+
min-height: 0;
|
|
71
|
+
overflow: hidden;
|
|
60
72
|
display: flex;
|
|
61
73
|
flex-direction: column;
|
|
62
74
|
padding: 0;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
|
-
import { render, screen, waitFor } from '../../test/render';
|
|
2
|
+
import { render, screen, waitFor, within } from '../../test/render';
|
|
3
3
|
import type { AccordionSelectOption } from './AccordionSelect';
|
|
4
4
|
import { AccordionSelect } from './AccordionSelect';
|
|
5
5
|
|
|
@@ -38,7 +38,8 @@ describe('AccordionSelect', () => {
|
|
|
38
38
|
|
|
39
39
|
it('displays the selected option in the header', () => {
|
|
40
40
|
render(<AccordionSelect options={options} value="champagne" />);
|
|
41
|
-
|
|
41
|
+
// Option text also appears in the collapsed-but-mounted dropdown list, so scope to the header
|
|
42
|
+
expect(within(screen.getByRole('button')).getByText('In an alleyway, drinking champagne')).toBeInTheDocument();
|
|
42
43
|
});
|
|
43
44
|
|
|
44
45
|
it('expands to show all options when header is clicked', async () => {
|
|
@@ -253,7 +254,9 @@ describe('AccordionSelect', () => {
|
|
|
253
254
|
describe('uncontrolled selection', () => {
|
|
254
255
|
it('renders with defaultValue', () => {
|
|
255
256
|
render(<AccordionSelect options={options} defaultValue="champagne" />);
|
|
256
|
-
expect(
|
|
257
|
+
expect(
|
|
258
|
+
within(screen.getByRole('button')).getByText('In an alleyway, drinking champagne'),
|
|
259
|
+
).toBeInTheDocument();
|
|
257
260
|
});
|
|
258
261
|
|
|
259
262
|
it('renders with placeholder when no defaultValue', () => {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { clsx } from 'clsx';
|
|
4
|
-
import { AnimatePresence, motion } from 'framer-motion';
|
|
5
4
|
import type { ComponentPropsWithoutRef, FC, ReactNode } from 'react';
|
|
6
5
|
import { useEffect, useRef } from 'react';
|
|
7
6
|
import { useControllableState } from '../../helpers/useControllableState';
|
|
@@ -195,60 +194,56 @@ export const AccordionSelect: FC<AccordionSelectProps> = ({
|
|
|
195
194
|
</div>
|
|
196
195
|
</div>
|
|
197
196
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
</div>
|
|
249
|
-
</motion.div>
|
|
250
|
-
)}
|
|
251
|
-
</AnimatePresence>
|
|
197
|
+
{/*
|
|
198
|
+
* Collapse animation uses the CSS grid-rows trick (`1fr` → `0fr`) instead
|
|
199
|
+
* of animating `height: auto`. JS-driven height-auto animations (e.g.
|
|
200
|
+
* framer-motion) flash an intermediate layout state on open/close that
|
|
201
|
+
* causes scrollable ancestors to clamp `scrollTop` to 0 in the first
|
|
202
|
+
* paint frame — visible as an instant scroll snap before the animation
|
|
203
|
+
* starts. The grid-rows approach keeps the layout stable across the
|
|
204
|
+
* entire transition.
|
|
205
|
+
*/}
|
|
206
|
+
<div
|
|
207
|
+
{...overrides?.dropdown}
|
|
208
|
+
aria-hidden={!open}
|
|
209
|
+
className={clsx(styles.dropdown, open && styles.open, overrides?.dropdown?.className)}
|
|
210
|
+
>
|
|
211
|
+
<div
|
|
212
|
+
{...overrides?.dropdownContent}
|
|
213
|
+
className={clsx(styles.dropdownContent, overrides?.dropdownContent?.className)}
|
|
214
|
+
>
|
|
215
|
+
{options.map((option) => {
|
|
216
|
+
const isOptionSelected = option.id === resolvedValue;
|
|
217
|
+
return (
|
|
218
|
+
<button
|
|
219
|
+
key={option.id}
|
|
220
|
+
type="button"
|
|
221
|
+
disabled={option.disabled || !open}
|
|
222
|
+
tabIndex={open ? undefined : -1}
|
|
223
|
+
data-selected={isOptionSelected}
|
|
224
|
+
{...overrides?.option}
|
|
225
|
+
className={clsx(styles.option, overrides?.option?.className)}
|
|
226
|
+
onClick={() => {
|
|
227
|
+
setResolvedValue(option.id);
|
|
228
|
+
if (closeOnSelect) setOpen(false);
|
|
229
|
+
}}
|
|
230
|
+
>
|
|
231
|
+
<div className={styles.optionContent}>
|
|
232
|
+
{renderOption ? (
|
|
233
|
+
renderOption(option, isOptionSelected)
|
|
234
|
+
) : (
|
|
235
|
+
<TextWhenString kind="paragraphXSmall" weight="medium">
|
|
236
|
+
{option.node}
|
|
237
|
+
</TextWhenString>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
{isOptionSelected && <Icon icon={Check} size={13} className={styles.check} />}
|
|
241
|
+
</button>
|
|
242
|
+
);
|
|
243
|
+
})}
|
|
244
|
+
{action}
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
252
247
|
</div>
|
|
253
248
|
);
|
|
254
249
|
};
|