@cdx-ui/components 0.3.2 → 0.3.4
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/README.md +2 -2
- package/coverage.svg +1 -1
- package/docs/components/color-field/content.md +2 -2
- package/docs/components/content.md +6 -4
- package/docs/components/data-grid/content.md +125 -1
- package/docs/components/date-field/content.md +2 -2
- package/docs/components/date-time-field/content.md +2 -2
- package/docs/components/input/content.md +20 -2
- package/docs/components/input/design.md +1 -1
- package/docs/components/insight/content.md +644 -0
- package/docs/components/insight-stack/content.md +231 -0
- package/docs/components/insurance-disclosure/content.md +1 -1
- package/docs/components/otp-input/content.md +2 -2
- package/docs/components/select/content.md +223 -24
- package/docs/components/text/content.md +97 -0
- package/docs/foundations/styles/tokens/all-tokens/content.md +23 -3
- package/docs/llms.txt +8 -4
- package/docs/patterns/forms/content.md +407 -0
- package/docs/resources/release-notes/0.3.2/content.md +17 -0
- package/docs/resources/release-notes/0.3.3/content.md +23 -0
- package/docs/resources/release-notes/content.md +2 -0
- package/lib/commonjs/components/Drawer/index.js +1 -1
- package/lib/commonjs/components/Drawer/index.js.map +1 -1
- package/lib/commonjs/components/Input/index.js.map +1 -1
- package/lib/commonjs/components/Input/styles.js +13 -6
- package/lib/commonjs/components/Input/styles.js.map +1 -1
- package/lib/commonjs/components/Insight/index.js +438 -0
- package/lib/commonjs/components/Insight/index.js.map +1 -0
- package/lib/commonjs/components/Insight/insetRadius.js +35 -0
- package/lib/commonjs/components/Insight/insetRadius.js.map +1 -0
- package/lib/commonjs/components/Insight/styles.js +186 -0
- package/lib/commonjs/components/Insight/styles.js.map +1 -0
- package/lib/commonjs/components/InsightStack/index.js +93 -0
- package/lib/commonjs/components/InsightStack/index.js.map +1 -0
- package/lib/commonjs/components/InsightStack/styles.js +61 -0
- package/lib/commonjs/components/InsightStack/styles.js.map +1 -0
- package/lib/commonjs/components/OtpInput/index.js.map +1 -1
- package/lib/commonjs/components/Select/index.js +326 -24
- package/lib/commonjs/components/Select/index.js.map +1 -1
- package/lib/commonjs/components/Select/styles.js +32 -1
- package/lib/commonjs/components/Select/styles.js.map +1 -1
- package/lib/commonjs/components/index.js +24 -0
- package/lib/commonjs/components/index.js.map +1 -1
- package/lib/commonjs/components/picker/surfaces.js +18 -3
- package/lib/commonjs/components/picker/surfaces.js.map +1 -1
- package/lib/commonjs/figma/Insight.figma.js +73 -0
- package/lib/commonjs/figma/Insight.figma.js.map +1 -0
- package/lib/commonjs/figma/InsightStack.figma.js +39 -0
- package/lib/commonjs/figma/InsightStack.figma.js.map +1 -0
- package/lib/module/components/Drawer/index.js +1 -1
- package/lib/module/components/Drawer/index.js.map +1 -1
- package/lib/module/components/Input/index.js.map +1 -1
- package/lib/module/components/Input/styles.js +14 -6
- package/lib/module/components/Input/styles.js.map +1 -1
- package/lib/module/components/Insight/index.js +433 -0
- package/lib/module/components/Insight/index.js.map +1 -0
- package/lib/module/components/Insight/insetRadius.js +30 -0
- package/lib/module/components/Insight/insetRadius.js.map +1 -0
- package/lib/module/components/Insight/styles.js +183 -0
- package/lib/module/components/Insight/styles.js.map +1 -0
- package/lib/module/components/InsightStack/index.js +89 -0
- package/lib/module/components/InsightStack/index.js.map +1 -0
- package/lib/module/components/InsightStack/styles.js +58 -0
- package/lib/module/components/InsightStack/styles.js.map +1 -0
- package/lib/module/components/OtpInput/index.js.map +1 -1
- package/lib/module/components/Select/index.js +328 -26
- package/lib/module/components/Select/index.js.map +1 -1
- package/lib/module/components/Select/styles.js +31 -0
- package/lib/module/components/Select/styles.js.map +1 -1
- package/lib/module/components/index.js +2 -0
- package/lib/module/components/index.js.map +1 -1
- package/lib/module/components/picker/surfaces.js +17 -3
- package/lib/module/components/picker/surfaces.js.map +1 -1
- package/lib/module/figma/Insight.figma.js +67 -0
- package/lib/module/figma/Insight.figma.js.map +1 -0
- package/lib/module/figma/InsightStack.figma.js +33 -0
- package/lib/module/figma/InsightStack.figma.js.map +1 -0
- package/lib/typescript/components/ColorField/styles.d.ts +4 -2
- package/lib/typescript/components/ColorField/styles.d.ts.map +1 -1
- package/lib/typescript/components/DateField/styles.d.ts +4 -2
- package/lib/typescript/components/DateField/styles.d.ts.map +1 -1
- package/lib/typescript/components/Input/index.d.ts +3 -2
- package/lib/typescript/components/Input/index.d.ts.map +1 -1
- package/lib/typescript/components/Input/styles.d.ts +1 -1
- package/lib/typescript/components/Input/styles.d.ts.map +1 -1
- package/lib/typescript/components/Insight/index.d.ts +87 -0
- package/lib/typescript/components/Insight/index.d.ts.map +1 -0
- package/lib/typescript/components/Insight/insetRadius.d.ts +8 -0
- package/lib/typescript/components/Insight/insetRadius.d.ts.map +1 -0
- package/lib/typescript/components/Insight/styles.d.ts +33 -0
- package/lib/typescript/components/Insight/styles.d.ts.map +1 -0
- package/lib/typescript/components/InsightStack/index.d.ts +21 -0
- package/lib/typescript/components/InsightStack/index.d.ts.map +1 -0
- package/lib/typescript/components/InsightStack/styles.d.ts +11 -0
- package/lib/typescript/components/InsightStack/styles.d.ts.map +1 -0
- package/lib/typescript/components/OtpInput/index.d.ts +3 -2
- package/lib/typescript/components/OtpInput/index.d.ts.map +1 -1
- package/lib/typescript/components/Select/index.d.ts +73 -6
- package/lib/typescript/components/Select/index.d.ts.map +1 -1
- package/lib/typescript/components/Select/styles.d.ts +9 -0
- package/lib/typescript/components/Select/styles.d.ts.map +1 -1
- package/lib/typescript/components/index.d.ts +2 -0
- package/lib/typescript/components/index.d.ts.map +1 -1
- package/lib/typescript/components/picker/surfaces.d.ts +14 -3
- package/lib/typescript/components/picker/surfaces.d.ts.map +1 -1
- package/lib/typescript/figma/Insight.figma.d.ts +8 -0
- package/lib/typescript/figma/Insight.figma.d.ts.map +1 -0
- package/lib/typescript/figma/InsightStack.figma.d.ts +8 -0
- package/lib/typescript/figma/InsightStack.figma.d.ts.map +1 -0
- package/package.json +5 -5
- package/src/components/ColorField/styles.ts +3 -1
- package/src/components/DateField/styles.ts +6 -1
- package/src/components/Drawer/index.tsx +1 -1
- package/src/components/Input/index.tsx +3 -2
- package/src/components/Input/styles.ts +35 -15
- package/src/components/Insight/index.tsx +531 -0
- package/src/components/Insight/insetRadius.ts +30 -0
- package/src/components/Insight/styles.ts +193 -0
- package/src/components/InsightStack/index.tsx +86 -0
- package/src/components/InsightStack/styles.ts +61 -0
- package/src/components/OtpInput/index.tsx +3 -2
- package/src/components/Select/index.tsx +411 -29
- package/src/components/Select/styles.ts +36 -0
- package/src/components/index.ts +2 -0
- package/src/components/picker/surfaces.tsx +29 -4
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# Insight Stack (https://design.candescent.com/docs/components/insight-stack)
|
|
2
|
+
|
|
3
|
+
Resources: [Figma](https://www.figma.com/design/0lEVMVlLj8ZS4fgItr3pmd/%E2%AD%90-Forge-DS--Components?node-id=13652-32306), [Storybook](https://victorious-wave-0daadf00f.6.azurestaticapps.net/?path=/story/surfaces-insightstack--default)
|
|
4
|
+
|
|
5
|
+
`InsightStack` wraps a summary [`Insight`](https://design.candescent.com/docs/components/insight/content.md) and draws up to two cards behind it, so a single card on Home can stand for a list of insights. The layers are decorative and never interactive.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { InsightStack } from '@cdx-ui/components';
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Basic
|
|
12
|
+
|
|
13
|
+
Pass the number of insights as `count`. The layers don't announce it, so include the count in the front Insight's accessible label.
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { Box, Insight, InsightStack } from '@cdx-ui/components';
|
|
17
|
+
|
|
18
|
+
const insightCount = 3;
|
|
19
|
+
const summary = 'Your spending this month is 12% lower than last month.';
|
|
20
|
+
|
|
21
|
+
export default function InsightStackBasicDemo() {
|
|
22
|
+
return (
|
|
23
|
+
<Box className="w-full max-w-md">
|
|
24
|
+
<InsightStack count={insightCount}>
|
|
25
|
+
<Insight
|
|
26
|
+
aiGenerated
|
|
27
|
+
onPress={() => {}}
|
|
28
|
+
accessibilityLabel={`${summary} ${insightCount} insights.`}
|
|
29
|
+
>
|
|
30
|
+
<Insight.Description>{summary}</Insight.Description>
|
|
31
|
+
</Insight>
|
|
32
|
+
</InsightStack>
|
|
33
|
+
</Box>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Item count
|
|
39
|
+
|
|
40
|
+
A `count` of 1 draws no layers, 2 draws one, and 3 or more draws two. When pressed, only the front Insight scales; the layers stay still.
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import { Box, Insight, InsightStack } from '@cdx-ui/components';
|
|
44
|
+
|
|
45
|
+
const counts = [1, 2, 3] as const;
|
|
46
|
+
|
|
47
|
+
export default function InsightStackCountsDemo() {
|
|
48
|
+
return (
|
|
49
|
+
<Box className="w-full max-w-md gap-4">
|
|
50
|
+
{counts.map((count) => {
|
|
51
|
+
const summary = count === 1 ? 'You have 1 new insight.' : `You have ${count} new insights.`;
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<InsightStack key={count} count={count}>
|
|
55
|
+
<Insight onPress={() => {}}>
|
|
56
|
+
<Insight.Description>{summary}</Insight.Description>
|
|
57
|
+
</Insight>
|
|
58
|
+
</InsightStack>
|
|
59
|
+
);
|
|
60
|
+
})}
|
|
61
|
+
</Box>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Colors
|
|
67
|
+
|
|
68
|
+
The layers are always neutral. The front Insight's `color` and `aiGenerated` tint only the front card.
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
import { Box, Insight, InsightStack } from '@cdx-ui/components';
|
|
72
|
+
|
|
73
|
+
const stacks = [
|
|
74
|
+
{ key: 'warning', color: 'warning', count: 2, copy: 'You have 2 bills due before payday.' },
|
|
75
|
+
{ key: 'danger', color: 'danger', count: 3, copy: 'You have 3 overdue payments.' },
|
|
76
|
+
{ key: 'ai', color: 'info', count: 3, copy: 'You have 3 new ways to save.', aiGenerated: true },
|
|
77
|
+
] as const;
|
|
78
|
+
|
|
79
|
+
export default function InsightStackColorsDemo() {
|
|
80
|
+
return (
|
|
81
|
+
<Box className="w-full max-w-md gap-4">
|
|
82
|
+
{stacks.map((stack) => (
|
|
83
|
+
<InsightStack key={stack.key} count={stack.count}>
|
|
84
|
+
<Insight color={stack.color} aiGenerated={'aiGenerated' in stack} onPress={() => {}}>
|
|
85
|
+
<Insight.Description>{stack.copy}</Insight.Description>
|
|
86
|
+
</Insight>
|
|
87
|
+
</InsightStack>
|
|
88
|
+
))}
|
|
89
|
+
</Box>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Dismissible
|
|
95
|
+
|
|
96
|
+
Pass the number of insights left as `count`, so the layers drop away as the user dismisses cards. Keep rendering the same front Insight and swap its copy, so `ref.current?.focus()` returns focus to it after each dismissal. When none is left, remove the stack and move focus elsewhere, here to the button below it.
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
import { useRef, useState, type ComponentRef } from 'react';
|
|
100
|
+
import { Box, Button, Insight, InsightStack, Text } from '@cdx-ui/components';
|
|
101
|
+
|
|
102
|
+
const insights = [
|
|
103
|
+
{ id: 'spending', copy: 'Your spending this month is 12% lower than last month.' },
|
|
104
|
+
{ id: 'subscriptions', copy: 'You have three subscriptions renewing in the next seven days.' },
|
|
105
|
+
{ id: 'savings', copy: 'You could save $45.00 a month with annual billing.' },
|
|
106
|
+
{ id: 'paycheck', copy: 'Your paycheck arrived a day early.' },
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
export default function InsightStackDismissibleDemo() {
|
|
110
|
+
const [remaining, setRemaining] = useState(insights);
|
|
111
|
+
const insightRef = useRef<ComponentRef<typeof Insight>>(null);
|
|
112
|
+
const restoreRef = useRef<ComponentRef<typeof Button>>(null);
|
|
113
|
+
const front = remaining.at(0);
|
|
114
|
+
const count = remaining.length;
|
|
115
|
+
|
|
116
|
+
const dismiss = () => {
|
|
117
|
+
setRemaining((current) => current.slice(1));
|
|
118
|
+
if (count > 1) {
|
|
119
|
+
insightRef.current?.focus();
|
|
120
|
+
} else {
|
|
121
|
+
restoreRef.current?.focus();
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<Box className="w-full max-w-md gap-4">
|
|
127
|
+
{front ? (
|
|
128
|
+
<InsightStack count={count}>
|
|
129
|
+
<Insight
|
|
130
|
+
ref={insightRef}
|
|
131
|
+
aiGenerated
|
|
132
|
+
onPress={() => {}}
|
|
133
|
+
onClose={dismiss}
|
|
134
|
+
accessibilityLabel={`${front.copy} ${count} ${count === 1 ? 'insight' : 'insights'}.`}
|
|
135
|
+
>
|
|
136
|
+
<Insight.Description>{front.copy}</Insight.Description>
|
|
137
|
+
</Insight>
|
|
138
|
+
</InsightStack>
|
|
139
|
+
) : (
|
|
140
|
+
<Box className="min-h-14 justify-center">
|
|
141
|
+
<Text color="secondary">{"You're all caught up."}</Text>
|
|
142
|
+
</Box>
|
|
143
|
+
)}
|
|
144
|
+
<Box className="items-start">
|
|
145
|
+
<Button
|
|
146
|
+
ref={restoreRef}
|
|
147
|
+
variant="outline"
|
|
148
|
+
size="small"
|
|
149
|
+
onPress={() => setRemaining(insights)}
|
|
150
|
+
>
|
|
151
|
+
<Button.Label>Restore insights</Button.Label>
|
|
152
|
+
</Button>
|
|
153
|
+
</Box>
|
|
154
|
+
</Box>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Detail
|
|
160
|
+
|
|
161
|
+
The front card can also be a detail Insight. The layers follow its height, so they peek below it however tall it grows.
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
import { Box, Button, Insight, InsightStack } from '@cdx-ui/components';
|
|
165
|
+
|
|
166
|
+
export default function InsightStackDetailDemo() {
|
|
167
|
+
return (
|
|
168
|
+
<Box className="w-full max-w-md gap-6">
|
|
169
|
+
<InsightStack count={3}>
|
|
170
|
+
<Insight layout="detail" onClose={() => {}}>
|
|
171
|
+
<Insight.Tag>Spending</Insight.Tag>
|
|
172
|
+
<Insight.Header>
|
|
173
|
+
<Insight.Title>Dining is trending higher</Insight.Title>
|
|
174
|
+
<Insight.Description>
|
|
175
|
+
You have spent $320.00 of your $400.00 dining budget, with 10 days left this month.
|
|
176
|
+
</Insight.Description>
|
|
177
|
+
</Insight.Header>
|
|
178
|
+
<Insight.Footer>
|
|
179
|
+
<Button variant="outline" size="small">
|
|
180
|
+
<Button.Label>View transactions</Button.Label>
|
|
181
|
+
</Button>
|
|
182
|
+
<Button size="small">
|
|
183
|
+
<Button.Label>Adjust budget</Button.Label>
|
|
184
|
+
</Button>
|
|
185
|
+
</Insight.Footer>
|
|
186
|
+
</Insight>
|
|
187
|
+
</InsightStack>
|
|
188
|
+
<InsightStack count={2}>
|
|
189
|
+
<Insight layout="detail" aiGenerated onClose={() => {}}>
|
|
190
|
+
<Insight.Tag>Savings</Insight.Tag>
|
|
191
|
+
<Insight.Header>
|
|
192
|
+
<Insight.Title>Grow your emergency fund</Insight.Title>
|
|
193
|
+
<Insight.Description>
|
|
194
|
+
Setting aside $150.00 each payday would reach your savings goal by June.
|
|
195
|
+
</Insight.Description>
|
|
196
|
+
</Insight.Header>
|
|
197
|
+
<Insight.Footer>
|
|
198
|
+
<Button size="small">
|
|
199
|
+
<Button.Label>Set up transfer</Button.Label>
|
|
200
|
+
</Button>
|
|
201
|
+
</Insight.Footer>
|
|
202
|
+
</Insight>
|
|
203
|
+
</InsightStack>
|
|
204
|
+
</Box>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## API
|
|
210
|
+
|
|
211
|
+
### InsightStack
|
|
212
|
+
|
|
213
|
+
| Prop | Type | Default | Description |
|
|
214
|
+
| --- | --- | --- | --- |
|
|
215
|
+
| `className?` | `string` | — | — |
|
|
216
|
+
| `children?` | `ReactNode` | — | The front card, usually a summary `Insight`. |
|
|
217
|
+
| `count` | `number` | — | How many insights the stack stands for. `1` or less draws no layers, `2` draws one, and `3` or more draws two. The layers are decorative, so put the count in the front Insight's accessible label or hint. |
|
|
218
|
+
| `asChild?` | `boolean` | — | Merges the root props onto a single child element instead of rendering a `View`. |
|
|
219
|
+
|
|
220
|
+
Inherits all [`ViewProps`](https://reactnative.dev/docs/view#props).
|
|
221
|
+
|
|
222
|
+
## Anatomy
|
|
223
|
+
|
|
224
|
+
| `data-slot` | Element |
|
|
225
|
+
| --------------------- | ---------------------------------------- |
|
|
226
|
+
| `insight-stack` | Root |
|
|
227
|
+
| `insight-stack-layer` | Decorative card behind the front Insight |
|
|
228
|
+
|
|
229
|
+
## Accessibility
|
|
230
|
+
|
|
231
|
+
The root adds no role, so the front Insight stays the only interactive element. The layers are hidden from assistive tech, so `count` is not announced. Include the count in the front Insight's accessible label, for example by appending "3 insights" to its copy in `accessibilityLabel`, or in its `accessibilityHint`.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Insurance Disclosure (https://design.candescent.com/docs/components/insurance-disclosure)
|
|
2
2
|
|
|
3
3
|
Resources: [Figma](https://www.figma.com/design/0lEVMVlLj8ZS4fgItr3pmd/%E2%AD%90-Forge-DS--Components?node-id=8550-3291), [Storybook](https://victorious-wave-0daadf00f.6.azurestaticapps.net/?path=/story/domain-insurancedisclosure--default)
|
|
4
4
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# OTP Input
|
|
1
|
+
# OTP Input (https://design.candescent.com/docs/components/otp-input)
|
|
2
2
|
|
|
3
3
|
Resources: [Figma](https://www.figma.com/design/0lEVMVlLj8ZS4fgItr3pmd/%E2%AD%90-Forge-DS--Components?node-id=5653-5035), [Storybook](https://victorious-wave-0daadf00f.6.azurestaticapps.net/?path=/story/inputs-otpinput--default)
|
|
4
4
|
|
|
@@ -298,7 +298,7 @@ Styled cell chrome built on `Input`. Inherits variant and size from the parent `
|
|
|
298
298
|
| --- | --- | --- | --- |
|
|
299
299
|
| `className?` | `string` | — | — |
|
|
300
300
|
| `children?` | `React.ReactNode` | — | — |
|
|
301
|
-
| `variant?` | `'outline'
|
|
301
|
+
| `variant?` | `'outline'` | — | — |
|
|
302
302
|
| `size?` | `'default' \| 'small' \| null` | — | — |
|
|
303
303
|
| `isInvalid?` | `boolean` | — | — |
|
|
304
304
|
| `isReadOnly?` | `boolean` | — | — |
|
|
@@ -107,6 +107,120 @@ export default function SelectWithFieldDemo() {
|
|
|
107
107
|
}
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
+
## Multiple selection
|
|
111
|
+
|
|
112
|
+
Set `type="multiple"` to let users pick several options. `value` becomes a `string[]`, each press toggles an option and keeps the dropdown open, and every toggle calls `onValueChange` with the full array. Add `Select.ItemControl` for a leading checkbox. Without custom children, `Select.Value` shows the selected labels on one line.
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
import { useState } from 'react';
|
|
116
|
+
import { Field, Select } from '@cdx-ui/components';
|
|
117
|
+
import { teams } from './_data';
|
|
118
|
+
|
|
119
|
+
export default function SelectMultipleDemo() {
|
|
120
|
+
const [teamIds, setTeamIds] = useState<string[]>([]);
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<Field className="w-full" isRequired>
|
|
124
|
+
<Field.Label>Assign teams</Field.Label>
|
|
125
|
+
<Select type="multiple" items={teams} value={teamIds} onValueChange={setTeamIds} isRequired>
|
|
126
|
+
<Select.Trigger>
|
|
127
|
+
<Select.Value placeholder="Select teams">
|
|
128
|
+
{({ isPlaceholder, selectedItems }) =>
|
|
129
|
+
isPlaceholder ? null : `${selectedItems.length} teams selected`
|
|
130
|
+
}
|
|
131
|
+
</Select.Value>
|
|
132
|
+
</Select.Trigger>
|
|
133
|
+
<Select.Content>
|
|
134
|
+
{teams.map((team) => (
|
|
135
|
+
<Select.Item key={team.value} value={team.value}>
|
|
136
|
+
<Select.ItemControl />
|
|
137
|
+
<Select.ItemLabel>{team.label}</Select.ItemLabel>
|
|
138
|
+
</Select.Item>
|
|
139
|
+
))}
|
|
140
|
+
</Select.Content>
|
|
141
|
+
</Select>
|
|
142
|
+
<Field.Helper>Each change applies immediately.</Field.Helper>
|
|
143
|
+
</Field>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## With search
|
|
149
|
+
|
|
150
|
+
Add `Select.Search` as the first child of `Select.Content` to build a searchable select, the pattern other libraries call an autocomplete. Typing filters the options in both `single` and `multiple` mode, and `Select.Empty` shows when nothing matches. Selected options that the query hides stay selected and still count in `Select.Value`. The default filter is a case- and accent-insensitive "contains" on each option's `label`; pass `filter` to replace it, or control `inputValue` and `onInputChange` to swap `items` for server results. The query clears when the dropdown closes.
|
|
151
|
+
|
|
152
|
+
In `Select.Content` on iOS and Android, the search input renders inside the anchored dropdown and the on-screen keyboard can cover the options. Use `Select.BottomSheet` for native search.
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
import { useState } from 'react';
|
|
156
|
+
import { Field, Select } from '@cdx-ui/components';
|
|
157
|
+
import { teams } from './_data';
|
|
158
|
+
|
|
159
|
+
export default function SelectWithSearchDemo() {
|
|
160
|
+
const [teamIds, setTeamIds] = useState<string[]>([]);
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<Field className="w-full" isRequired>
|
|
164
|
+
<Field.Label>Assign teams</Field.Label>
|
|
165
|
+
<Select type="multiple" items={teams} value={teamIds} onValueChange={setTeamIds}>
|
|
166
|
+
<Select.Trigger>
|
|
167
|
+
<Select.Value placeholder="Select teams you want to assign to this user">
|
|
168
|
+
{({ isPlaceholder, selectedItems }) =>
|
|
169
|
+
isPlaceholder ? null : `${String(selectedItems.length)} teams selected`
|
|
170
|
+
}
|
|
171
|
+
</Select.Value>
|
|
172
|
+
</Select.Trigger>
|
|
173
|
+
<Select.Content>
|
|
174
|
+
<Select.Search placeholder="Search Teams" accessibilityLabel="Search teams" />
|
|
175
|
+
{teams.map((t) => (
|
|
176
|
+
<Select.Item key={t.value} value={t.value} disabled={t.disabled}>
|
|
177
|
+
<Select.ItemControl />
|
|
178
|
+
<Select.ItemLabel>{t.label}</Select.ItemLabel>
|
|
179
|
+
</Select.Item>
|
|
180
|
+
))}
|
|
181
|
+
<Select.Empty>No teams found</Select.Empty>
|
|
182
|
+
</Select.Content>
|
|
183
|
+
</Select>
|
|
184
|
+
</Field>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Bottom sheet
|
|
190
|
+
|
|
191
|
+
Use `Select.BottomSheet` in place of `Select.Content` to present the options in a sheet that slides up from the bottom — the preferred surface on native, especially with `Select.Search`. It takes the same children: `Select.Search`, the options, and `Select.Empty`. The sheet keeps the search input and the results above the on-screen keyboard, and swiping it down, pressing the backdrop, or pressing Android back closes it without changing the value. On web it renders the same dropdown as `Select.Content`, so one composition serves every platform. On native it requires `BottomSheet.Provider` at the app root.
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
import { useState } from 'react';
|
|
195
|
+
import { Field, Select } from '@cdx-ui/components';
|
|
196
|
+
import { teams } from './_data';
|
|
197
|
+
|
|
198
|
+
export default function SelectBottomSheetDemo() {
|
|
199
|
+
const [teamIds, setTeamIds] = useState<string[]>([]);
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<Field className="w-full">
|
|
203
|
+
<Field.Label>Assign teams</Field.Label>
|
|
204
|
+
<Select type="multiple" items={teams} value={teamIds} onValueChange={setTeamIds}>
|
|
205
|
+
<Select.Trigger>
|
|
206
|
+
<Select.Value placeholder="Select teams" />
|
|
207
|
+
</Select.Trigger>
|
|
208
|
+
<Select.BottomSheet>
|
|
209
|
+
<Select.Search placeholder="Search Teams" accessibilityLabel="Search teams" />
|
|
210
|
+
{teams.map((t) => (
|
|
211
|
+
<Select.Item key={t.value} value={t.value} disabled={t.disabled}>
|
|
212
|
+
<Select.ItemControl />
|
|
213
|
+
<Select.ItemLabel>{t.label}</Select.ItemLabel>
|
|
214
|
+
</Select.Item>
|
|
215
|
+
))}
|
|
216
|
+
<Select.Empty>No teams found</Select.Empty>
|
|
217
|
+
</Select.BottomSheet>
|
|
218
|
+
</Select>
|
|
219
|
+
</Field>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
110
224
|
## Custom trigger
|
|
111
225
|
|
|
112
226
|
Set `asChild` on `Select.Trigger` to use a Forge pressable such as `Button`, `IconButton`, or `Chip`. Use the trigger and item `indicator` props to replace their default icons, or pass `null` to suppress an indicator.
|
|
@@ -145,7 +259,7 @@ export default function SelectCustomTriggerDemo() {
|
|
|
145
259
|
|
|
146
260
|
## Custom value display
|
|
147
261
|
|
|
148
|
-
Pass children to `Select.Value` to replace the default label. Read the selected option from your own state, and render the placeholder by leaving the children empty when nothing is selected.
|
|
262
|
+
Pass children to `Select.Value` to replace the default label. Read the selected option from your own state, and render the placeholder by leaving the children empty when nothing is selected. A function child receives `isPlaceholder`, `selectedItems`, and `selectedText`; return `null` to fall back to the default.
|
|
149
263
|
|
|
150
264
|
```tsx
|
|
151
265
|
import { useState } from 'react';
|
|
@@ -295,7 +409,7 @@ export default function SelectPlacementDemo() {
|
|
|
295
409
|
|
|
296
410
|
### Select
|
|
297
411
|
|
|
298
|
-
The root manages the selected value, open state, options, and form-control state.
|
|
412
|
+
The root manages the selected value, open state, options, and form-control state. `type` sets the selection mode: `single` (the default) takes a `string` value, and `multiple` takes a `string[]`, where an empty array means no selection.
|
|
299
413
|
|
|
300
414
|
| Prop | Type | Default | Description |
|
|
301
415
|
| --- | --- | --- | --- |
|
|
@@ -303,18 +417,23 @@ The root manages the selected value, open state, options, and form-control state
|
|
|
303
417
|
| `children?` | `ReactNode` | — | — |
|
|
304
418
|
| `variant?` | `'outline' \| null` | — | — |
|
|
305
419
|
| `size?` | `'default' \| 'small' \| null` | — | — |
|
|
306
|
-
| `
|
|
307
|
-
| `value?` | `string` | — | — |
|
|
308
|
-
| `defaultValue?` | `string` | — | — |
|
|
309
|
-
| `onValueChange?` | `((value: string) => void)` | — | — |
|
|
310
|
-
| `
|
|
420
|
+
| `type?` | `'single' \| 'multiple'` | — | — |
|
|
421
|
+
| `value?` | `string \| string[]` | — | — |
|
|
422
|
+
| `defaultValue?` | `string \| string[]` | — | — |
|
|
423
|
+
| `onValueChange?` | `((value: string) => void) \| ((value: string[]) => void)` | — | — |
|
|
424
|
+
| `items?` | `SelectItemData[]` | — | — |
|
|
425
|
+
| `isOpen?` | `boolean` | — | — |
|
|
311
426
|
| `defaultOpen?` | `boolean` | — | — |
|
|
312
427
|
| `onOpenChange?` | `((open: boolean) => void)` | — | — |
|
|
313
428
|
| `disabled?` | `boolean` | — | — |
|
|
314
429
|
| `isRequired?` | `boolean` | — | — |
|
|
315
430
|
| `isInvalid?` | `boolean` | — | — |
|
|
316
431
|
| `isReadOnly?` | `boolean` | — | — |
|
|
317
|
-
| `native?` | `boolean` | — |
|
|
432
|
+
| `native?` | `boolean` | — | — |
|
|
433
|
+
| `inputValue?` | `string` | — | — |
|
|
434
|
+
| `defaultInputValue?` | `string` | — | — |
|
|
435
|
+
| `onInputChange?` | `((inputValue: string) => void)` | — | — |
|
|
436
|
+
| `filter?` | `SelectFilter` | — | — |
|
|
318
437
|
|
|
319
438
|
Inherits all [`ViewProps`](https://reactnative.dev/docs/view#props).
|
|
320
439
|
|
|
@@ -361,12 +480,12 @@ Inherits all [`PressableProps`](https://reactnative.dev/docs/pressable#props).
|
|
|
361
480
|
|
|
362
481
|
### Select.Value
|
|
363
482
|
|
|
364
|
-
Displays the label from `items` that matches the selected value, or the placeholder when no value is selected.
|
|
483
|
+
Displays the label from `items` that matches the selected value, or the placeholder when no value is selected. In multiple mode it shows the selected labels joined with commas, truncated to one line. A function child receives `{ isPlaceholder, selectedItems, selectedText }`.
|
|
365
484
|
|
|
366
485
|
| Prop | Type | Default | Description |
|
|
367
486
|
| --- | --- | --- | --- |
|
|
368
487
|
| `className?` | `string` | — | — |
|
|
369
|
-
| `children?` | `ReactNode` | — |
|
|
488
|
+
| `children?` | `ReactNode \| ((state: SelectValueRenderProps) => ReactNode)` | — | Replaces the default display. A function child receives the selection state; return `null` or `undefined` to fall back to the default (placeholder or `selectedText`). |
|
|
370
489
|
| `asChild?` | `boolean` | — | — |
|
|
371
490
|
| `placeholder?` | `string` | — | — |
|
|
372
491
|
|
|
@@ -394,7 +513,7 @@ Inherits all `ForgeIconProps` (react-native-svg `SvgProps` plus `filled` and `si
|
|
|
394
513
|
|
|
395
514
|
### Select.Content
|
|
396
515
|
|
|
397
|
-
Dropdown panel that positions itself around the trigger, scrolls long option lists, and closes when dismissed.
|
|
516
|
+
Dropdown panel that positions itself around the trigger, scrolls long option lists, and closes when dismissed. Dismissing never changes the value.
|
|
398
517
|
|
|
399
518
|
| Prop | Type | Default | Description |
|
|
400
519
|
| --- | --- | --- | --- |
|
|
@@ -406,12 +525,65 @@ Dropdown panel that positions itself around the trigger, scrolls long option lis
|
|
|
406
525
|
| `crossOffset?` | `number` | — | Cross-axis adjustment from the aligned dropdown position in pixels. Defaults to 0. |
|
|
407
526
|
| `enteringAnimation?` | `EntryOrExitLayoutType` | — | Enter animation for the dropdown. Defaults to a 150ms fade. |
|
|
408
527
|
| `exitingAnimation?` | `EntryOrExitLayoutType` | — | Exit animation for the dropdown. Defaults to a 100ms fade. |
|
|
528
|
+
| `dismissAccessibilityLabel?` | `string` | `'Dismiss'` | Accessible name of the visually hidden button that lets screen-reader users close the popup on web. |
|
|
409
529
|
|
|
410
530
|
Inherits all [`ViewProps`](https://reactnative.dev/docs/view#props).
|
|
411
531
|
|
|
532
|
+
### Select.BottomSheet
|
|
533
|
+
|
|
534
|
+
Presents the options in a bottom sheet on iOS and Android, in place of `Select.Content`. Compose exactly one of `Select.Content` or `Select.BottomSheet` per select. It sorts its direct children into the same regions as `Select.Content`: `Select.Search` stays above the scrolling options and `Select.Empty` follows them. On web it renders the `Select.Content` dropdown, and `dismissAccessibilityLabel` names its hidden dismiss button. With `Select.Search`, keyboard avoidance applies to the default search input; an `asChild` input does not move the sheet.
|
|
535
|
+
|
|
536
|
+
| Prop | Type | Default | Description |
|
|
537
|
+
| --- | --- | --- | --- |
|
|
538
|
+
| `className?` | `string` | — | Additional Uniwind/Tailwind classes, merged after the sheet body's own styles. |
|
|
539
|
+
| `children?` | `ReactNode` | — | `Select.Search`, `Select.Item` elements, and `Select.Empty`, as in `Select.Content`. |
|
|
540
|
+
| `asChild?` | `boolean` | — | Render the consumer's child as the sheet body, skipping the region split. |
|
|
541
|
+
| `dismissAccessibilityLabel?` | `string` | `'Dismiss'` | Accessible name of the visually hidden dismiss button on web, where the surface is the `Select.Content` dropdown. |
|
|
542
|
+
|
|
543
|
+
Inherits all [`ViewProps`](https://reactnative.dev/docs/view#props).
|
|
544
|
+
|
|
545
|
+
| Attribute | Values | Description |
|
|
546
|
+
| ------------ | --------------------- | ------------------------------- |
|
|
547
|
+
| `data-slot` | `select-bottom-sheet` | Sheet body on native |
|
|
548
|
+
| `data-state` | `open` | Present while the sheet is open |
|
|
549
|
+
|
|
550
|
+
### Select.Search
|
|
551
|
+
|
|
552
|
+
A search input for the top of `Select.Content` or `Select.BottomSheet`, with a leading search icon. Its query filters the options through the root `filter`, and it takes focus when the dropdown opens on web. Place it as a direct child of the surface; a wrapped `Select.Search` is not recognized and renders among the options.
|
|
553
|
+
|
|
554
|
+
| Prop | Type | Default | Description |
|
|
555
|
+
| --- | --- | --- | --- |
|
|
556
|
+
| `className?` | `string` | — | Additional Uniwind/Tailwind classes, merged after the search container's own styles. |
|
|
557
|
+
| `children?` | `ReactNode` | — | The text input to render when `asChild` is set. Otherwise not rendered. |
|
|
558
|
+
| `asChild?` | `boolean` | — | Merge the search wiring onto the single child text input instead of rendering the default `Input` with a search icon. |
|
|
559
|
+
| `variant?` | `'outline' \| 'subtle'` | `'subtle'` | `Input` variant of the default search field. Ignored when `asChild` is set. |
|
|
560
|
+
| `getResultCountLabel?` | `((count: number) => string)` | — | Text the polite live region announces when the result count changes, such as `"3 results"` (the English default). When nothing matches, the `Select.Empty` text is announced instead. |
|
|
561
|
+
|
|
562
|
+
Inherits [`TextInputProps`](https://reactnative.dev/docs/textinput#props) except `value`, `defaultValue`, and `onChangeText`, which the root owns through `inputValue`, `defaultInputValue`, and `onInputChange`.
|
|
563
|
+
|
|
564
|
+
| Attribute | Values | Description |
|
|
565
|
+
| ----------- | --------------- | ---------------- |
|
|
566
|
+
| `data-slot` | `select-search` | Search container |
|
|
567
|
+
|
|
568
|
+
### Select.Empty
|
|
569
|
+
|
|
570
|
+
Content shown below the options only when no option matches the query. Plain text children are styled to match the options. Place it as a direct child of `Select.Content` or `Select.BottomSheet`.
|
|
571
|
+
|
|
572
|
+
| Prop | Type | Default | Description |
|
|
573
|
+
| --- | --- | --- | --- |
|
|
574
|
+
| `className?` | `string` | — | Additional Uniwind/Tailwind classes, merged after the empty state's own styles. |
|
|
575
|
+
| `children?` | `ReactNode` | — | — |
|
|
576
|
+
| `asChild?` | `boolean` | — | — |
|
|
577
|
+
|
|
578
|
+
Inherits all [`ViewProps`](https://reactnative.dev/docs/view#props).
|
|
579
|
+
|
|
580
|
+
| Attribute | Values | Description |
|
|
581
|
+
| ----------- | -------------- | ----------- |
|
|
582
|
+
| `data-slot` | `select-empty` | Empty state |
|
|
583
|
+
|
|
412
584
|
### Select.Item
|
|
413
585
|
|
|
414
|
-
An individual option. Its `value` must match an entry in the root `items` array. The `indicator` prop replaces the default selected-item checkmark, and `indicator={null}` suppresses it.
|
|
586
|
+
An individual option. Its `value` must match an entry in the root `items` array. Disable an option with `disabled` here or on its `items` entry. The `indicator` prop replaces the default selected-item checkmark, and `indicator={null}` suppresses it. The default checkmark is omitted when the item contains a `Select.ItemControl`.
|
|
415
587
|
|
|
416
588
|
| Prop | Type | Default | Description |
|
|
417
589
|
| --- | --- | --- | --- |
|
|
@@ -419,7 +591,7 @@ An individual option. Its `value` must match an entry in the root `items` array.
|
|
|
419
591
|
| `children?` | `ReactNode` | — | Either children or a render prop that receives a boolean reflecting whether the component is currently pressed. |
|
|
420
592
|
| `disabled?` | `boolean` | — | Whether the press behavior is disabled. |
|
|
421
593
|
| `asChild?` | `boolean` | — | — |
|
|
422
|
-
| `indicator?` | `ReactNode` | — | Replaces the default selected-item checkmark. The replacement is rendered only while the item is selected. Pass `null` to suppress the indicator. |
|
|
594
|
+
| `indicator?` | `ReactNode` | — | Replaces the default selected-item checkmark. The replacement is rendered only while the item is selected. Pass `null` to suppress the indicator. The default checkmark is omitted when the item contains a `Select.ItemControl`. |
|
|
423
595
|
| `value` | `string` | — | — |
|
|
424
596
|
| `isHovered?` | `boolean` | — | — |
|
|
425
597
|
| `isActive?` | `boolean` | — | — |
|
|
@@ -452,22 +624,49 @@ Text label rendered inside a `Select.Item`.
|
|
|
452
624
|
|
|
453
625
|
Inherits all [`TextProps`](https://reactnative.dev/docs/text#props).
|
|
454
626
|
|
|
627
|
+
### Select.ItemControl
|
|
628
|
+
|
|
629
|
+
A leading checkbox that mirrors the item's selected state. It is presentational: the option itself carries the selected state for assistive technology.
|
|
630
|
+
|
|
631
|
+
| Prop | Type | Default | Description |
|
|
632
|
+
| --- | --- | --- | --- |
|
|
633
|
+
| `className?` | `string` | — | Additional Uniwind/Tailwind classes, merged after the control's own styles. |
|
|
634
|
+
| `children?` | `ReactNode` | — | The element to render when `asChild` is set. Otherwise not rendered. |
|
|
635
|
+
| `asChild?` | `boolean` | — | Render the consumer's child element in place of the default checkbox glyph. |
|
|
636
|
+
|
|
637
|
+
Inherits all [`ViewProps`](https://reactnative.dev/docs/view#props).
|
|
638
|
+
|
|
639
|
+
| Attribute | Values | Description |
|
|
640
|
+
| -------------- | --------------------- | --------------- |
|
|
641
|
+
| `data-slot` | `select-item-control` | Checkbox glyph |
|
|
642
|
+
| `data-checked` | `true` \| `false` | Selection state |
|
|
643
|
+
|
|
455
644
|
## Anatomy
|
|
456
645
|
|
|
457
|
-
| `data-slot`
|
|
458
|
-
|
|
|
459
|
-
| `select`
|
|
460
|
-
| `select-trigger`
|
|
461
|
-
| `select-value`
|
|
462
|
-
| `select-icon`
|
|
463
|
-
| `select-content`
|
|
464
|
-
| `select-
|
|
465
|
-
| `select-
|
|
646
|
+
| `data-slot` | Element |
|
|
647
|
+
| --------------------- | ----------------------------- |
|
|
648
|
+
| `select` | Root container |
|
|
649
|
+
| `select-trigger` | Dropdown trigger |
|
|
650
|
+
| `select-value` | Selected label or placeholder |
|
|
651
|
+
| `select-icon` | Dropdown indicator |
|
|
652
|
+
| `select-content` | Dropdown panel |
|
|
653
|
+
| `select-bottom-sheet` | Sheet body (native) |
|
|
654
|
+
| `select-search` | Search input |
|
|
655
|
+
| `select-empty` | No-results message |
|
|
656
|
+
| `select-item` | Option |
|
|
657
|
+
| `select-item-label` | Option label |
|
|
658
|
+
| `select-item-control` | Option checkbox |
|
|
466
659
|
|
|
467
660
|
## Accessibility
|
|
468
661
|
|
|
469
|
-
The trigger uses `role="combobox"` with `aria-haspopup="listbox"`, reflects open, required, invalid, and read-only state, and points to the dropdown content. The content uses `role="listbox"` and each item uses `role="option"` with selected and disabled state.
|
|
662
|
+
The trigger uses `role="combobox"` with `aria-haspopup="listbox"`, reflects open, required, invalid, and read-only state, and points to the dropdown content. The content uses `role="listbox"` and each item uses `role="option"` with selected and disabled state. In multiple mode the listbox sets `aria-multiselectable="true"`, and `Select.ItemControl` is hidden from assistive technology.
|
|
470
663
|
|
|
471
664
|
Compose with `Field.Label` for a visible associated label. When no visible label is present, provide `accessibilityLabel` on `Select`.
|
|
472
665
|
|
|
473
|
-
On web,
|
|
666
|
+
With `Select.Search`, the dropdown becomes a `role="dialog"` named by the field label that contains the search input (`role="searchbox"`) and the `role="listbox"`. The trigger keeps `aria-haspopup="listbox"`. On web, focus stays in the search input while Arrow Up and Arrow Down move the highlight through the visible, enabled options, announced through `aria-activedescendant`. Enter selects and closes, or toggles in multiple mode, and Space types a space. The highlight moves to the first match each time the query changes, and a polite live region announces the number of results, or the `Select.Empty` text when nothing matches. Escape and Tab close the dropdown, keep the value, and clear the query. Inside a `Drawer`, closing returns focus to the trigger and leaves the drawer open.
|
|
667
|
+
|
|
668
|
+
On web, Enter, Space, Arrow Down, or Arrow Up opens the dropdown. Arrow keys, Home, and End move through enabled options; Enter or Space selects and closes, or toggles in multiple mode; Escape closes; and Tab closes while continuing focus navigation. Focus returns to the trigger when the dropdown closes.
|
|
669
|
+
|
|
670
|
+
Screen-reader users can close the dropdown without a keyboard. On web, a visually hidden button labelled by `dismissAccessibilityLabel` on `Select.Content` closes it. On iOS, the VoiceOver escape gesture closes it, and on Android the back button does. Closing never changes the value.
|
|
671
|
+
|
|
672
|
+
In `Select.BottomSheet` on iOS and Android, VoiceOver and TalkBack move through the search input and each option directly, with no virtual focus, and each option announces its selected state. The sheet takes the same role and name as the dropdown. The VoiceOver escape gesture, Android back, a swipe down, and the backdrop close it, keep the value, and clear the query.
|