@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
|
@@ -1,10 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
Children,
|
|
3
|
+
cloneElement,
|
|
4
|
+
createContext,
|
|
5
|
+
forwardRef,
|
|
6
|
+
isValidElement,
|
|
7
|
+
useContext,
|
|
8
|
+
useMemo,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from 'react';
|
|
11
|
+
import { Platform, Text, type TextInput, type TextProps, View, type ViewProps } from 'react-native';
|
|
12
|
+
import { SafeAreaInsetsContext } from 'react-native-safe-area-context';
|
|
13
|
+
import { Check, KeyboardArrowDown, KeyboardArrowUp, Search } from '@cdx-ui/icons';
|
|
4
14
|
import {
|
|
5
15
|
SelectContent as SelectContentPrimitive,
|
|
16
|
+
SelectEmpty as SelectEmptyPrimitive,
|
|
6
17
|
SelectItem as SelectItemPrimitive,
|
|
18
|
+
SelectListbox as SelectListboxPrimitive,
|
|
7
19
|
SelectRoot as SelectRootPrimitive,
|
|
20
|
+
SelectProvider,
|
|
21
|
+
SelectSearch as SelectSearchPrimitive,
|
|
22
|
+
SelectSheetContent as SelectSheetContentPrimitive,
|
|
8
23
|
SelectTrigger as SelectTriggerPrimitive,
|
|
9
24
|
Slot,
|
|
10
25
|
dataAttributes,
|
|
@@ -12,35 +27,75 @@ import {
|
|
|
12
27
|
useSelectContext,
|
|
13
28
|
useSelectItemContext,
|
|
14
29
|
type ISelectContentProps,
|
|
30
|
+
type ISelectEmptyProps,
|
|
15
31
|
type ISelectItemLabelProps,
|
|
16
32
|
type ISelectItemProps,
|
|
17
33
|
type ISelectProps,
|
|
34
|
+
type ISelectSearchProps,
|
|
18
35
|
type ISelectTriggerProps,
|
|
19
36
|
type ISelectValueProps,
|
|
37
|
+
type SelectItemData,
|
|
38
|
+
type SelectValueRenderProps,
|
|
20
39
|
} from '@cdx-ui/primitives';
|
|
21
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
cn,
|
|
42
|
+
FormControlContext,
|
|
43
|
+
ParentContext,
|
|
44
|
+
useParentContext,
|
|
45
|
+
useStyleContext,
|
|
46
|
+
type FormControlContextValue,
|
|
47
|
+
} from '@cdx-ui/utils';
|
|
48
|
+
import { BottomSheet } from '../BottomSheet';
|
|
49
|
+
import { checkboxIconVariants, checkboxIndicatorVariants } from '../Checkbox/styles';
|
|
22
50
|
import { Icon, IconProps } from '../Icon';
|
|
51
|
+
import { Input, type InputProps } from '../Input';
|
|
23
52
|
import { Menu } from '../Menu';
|
|
24
|
-
import { menuItemVariants } from '../Menu/styles';
|
|
53
|
+
import { menuItemControlVariants, menuItemVariants } from '../Menu/styles';
|
|
54
|
+
import { PickerBottomSheetSurface, PickerSheetTextInput } from '../picker/surfaces';
|
|
25
55
|
import { positionToDataIcon, type IconSlotPosition } from '../../utils/positionToDataIcon';
|
|
26
56
|
import {
|
|
27
57
|
type SelectVariantProps,
|
|
58
|
+
selectBottomSheetListboxVariants,
|
|
59
|
+
selectBottomSheetVariants,
|
|
60
|
+
selectEmptyTextVariants,
|
|
61
|
+
selectEmptyVariants,
|
|
28
62
|
selectIconVariants,
|
|
63
|
+
selectItemVariants,
|
|
64
|
+
selectSearchListboxVariants,
|
|
65
|
+
selectSearchVariants,
|
|
29
66
|
selectTriggerVariants,
|
|
30
67
|
selectValueVariants,
|
|
31
68
|
} from './styles';
|
|
32
69
|
|
|
70
|
+
export type {
|
|
71
|
+
SelectFilter,
|
|
72
|
+
SelectItemData,
|
|
73
|
+
SelectType,
|
|
74
|
+
SelectValueRenderProps,
|
|
75
|
+
} from '@cdx-ui/primitives';
|
|
76
|
+
|
|
33
77
|
const SCOPE = 'SELECT';
|
|
34
78
|
|
|
35
79
|
const useSelectStyleContext = () => useStyleContext(SCOPE) as SelectVariantProps;
|
|
36
80
|
|
|
81
|
+
const IS_WEB = Platform.OS === 'web';
|
|
82
|
+
|
|
83
|
+
/** `true` inside the native `Select.BottomSheet`, where `Select.Search` uses the sheet's input. */
|
|
84
|
+
const SelectSheetContext = createContext(false);
|
|
85
|
+
|
|
37
86
|
// =============================================================================
|
|
38
87
|
// STYLED ROOT
|
|
39
88
|
// =============================================================================
|
|
40
89
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
90
|
+
/**
|
|
91
|
+
* `type` narrows `value` / `defaultValue` / `onValueChange` to `string` (single, the default)
|
|
92
|
+
* or `string[]` (multiple).
|
|
93
|
+
*/
|
|
94
|
+
export type SelectProps = ViewProps &
|
|
95
|
+
SelectVariantProps &
|
|
96
|
+
ISelectProps & {
|
|
97
|
+
className?: string;
|
|
98
|
+
};
|
|
44
99
|
|
|
45
100
|
const SelectRoot = forwardRef<View, SelectProps>(
|
|
46
101
|
({ variant = 'outline', size = 'default', className, children, style, ...props }, ref) => {
|
|
@@ -147,19 +202,32 @@ SelectTrigger.displayName = 'Select.Trigger';
|
|
|
147
202
|
// STYLED VALUE (styled-only)
|
|
148
203
|
// =============================================================================
|
|
149
204
|
|
|
150
|
-
export interface SelectValueProps extends TextProps, ISelectValueProps {
|
|
205
|
+
export interface SelectValueProps extends Omit<TextProps, 'children'>, ISelectValueProps {
|
|
151
206
|
asChild?: boolean;
|
|
152
207
|
className?: string;
|
|
153
208
|
}
|
|
154
209
|
|
|
210
|
+
/**
|
|
211
|
+
* `Intl.ListFormat` is not available on Hermes, so labels are joined with a fixed separator
|
|
212
|
+
* to keep the summary identical on iOS, Android, and web.
|
|
213
|
+
*/
|
|
214
|
+
const SELECTED_TEXT_SEPARATOR = ', ';
|
|
215
|
+
|
|
155
216
|
const SelectValue = forwardRef<Text, SelectValueProps>(
|
|
156
217
|
({ asChild, className, style, placeholder, children, ...props }, ref) => {
|
|
157
218
|
const { size } = useSelectStyleContext();
|
|
158
|
-
const {
|
|
219
|
+
const { type, items, selectedValues, invalid } = useSelectContext();
|
|
220
|
+
|
|
221
|
+
const selectedItems = useMemo<SelectItemData[]>(
|
|
222
|
+
() => items.filter((item) => selectedValues.includes(item.value)),
|
|
223
|
+
[items, selectedValues],
|
|
224
|
+
);
|
|
225
|
+
const isPlaceholder = !selectedValues.some((selected) => selected !== '');
|
|
226
|
+
const selectedText = selectedItems.map((item) => item.label).join(SELECTED_TEXT_SEPARATOR);
|
|
159
227
|
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
const displayText =
|
|
228
|
+
const renderState: SelectValueRenderProps = { isPlaceholder, selectedItems, selectedText };
|
|
229
|
+
const customDisplay = typeof children === 'function' ? children(renderState) : children;
|
|
230
|
+
const displayText = isPlaceholder ? placeholder : selectedText;
|
|
163
231
|
|
|
164
232
|
const computedClassName = cn(selectValueVariants({ size }), className);
|
|
165
233
|
const Comp = asChild ? Slot : Text;
|
|
@@ -169,17 +237,18 @@ const SelectValue = forwardRef<Text, SelectValueProps>(
|
|
|
169
237
|
ref={ref}
|
|
170
238
|
className={computedClassName}
|
|
171
239
|
style={style}
|
|
240
|
+
numberOfLines={type === 'multiple' ? 1 : undefined}
|
|
172
241
|
{...props}
|
|
173
242
|
{...mergeDataAttributes(
|
|
174
243
|
props,
|
|
175
244
|
dataAttributes({
|
|
176
245
|
slot: 'select-value',
|
|
177
|
-
placeholder:
|
|
246
|
+
placeholder: isPlaceholder,
|
|
178
247
|
invalid,
|
|
179
248
|
}),
|
|
180
249
|
)}
|
|
181
250
|
>
|
|
182
|
-
{
|
|
251
|
+
{customDisplay ?? displayText}
|
|
183
252
|
</Comp>
|
|
184
253
|
);
|
|
185
254
|
},
|
|
@@ -231,6 +300,114 @@ const SelectIcon = ({ asChild, className, style, as, position, ...props }: Selec
|
|
|
231
300
|
|
|
232
301
|
SelectIcon.displayName = 'Select.Icon';
|
|
233
302
|
|
|
303
|
+
// =============================================================================
|
|
304
|
+
// STYLED SEARCH — composes Input inside the popup
|
|
305
|
+
// =============================================================================
|
|
306
|
+
|
|
307
|
+
export interface SelectSearchProps extends ISelectSearchProps {
|
|
308
|
+
/**
|
|
309
|
+
* Merge the search wiring onto the single child text input instead of rendering the default
|
|
310
|
+
* `Input` with a search icon.
|
|
311
|
+
*/
|
|
312
|
+
asChild?: boolean;
|
|
313
|
+
/** Additional Uniwind/Tailwind classes, merged after the search container's own styles. */
|
|
314
|
+
className?: string;
|
|
315
|
+
/**
|
|
316
|
+
* `Input` variant of the default search field. Ignored when `asChild` is set.
|
|
317
|
+
* @default 'subtle'
|
|
318
|
+
*/
|
|
319
|
+
variant?: InputProps['variant'];
|
|
320
|
+
/** The text input to render when `asChild` is set. Otherwise not rendered. */
|
|
321
|
+
children?: ReactNode;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* The search input sits inside the select's `Field` in the React tree but is not the field's
|
|
326
|
+
* control, so it must not pick up the field's id, label, validation, or label-press focus.
|
|
327
|
+
*/
|
|
328
|
+
const ISOLATED_FIELD: Partial<FormControlContextValue> = {};
|
|
329
|
+
|
|
330
|
+
const SelectSearch = forwardRef<TextInput, SelectSearchProps>(
|
|
331
|
+
({ asChild, className, style, variant = 'subtle', children, ...props }, ref) => {
|
|
332
|
+
const { size } = useSelectStyleContext();
|
|
333
|
+
const inSheet = useContext(SelectSheetContext);
|
|
334
|
+
const slotData = dataAttributes({ slot: 'select-search' });
|
|
335
|
+
|
|
336
|
+
if (asChild) {
|
|
337
|
+
return (
|
|
338
|
+
<FormControlContext.Provider value={ISOLATED_FIELD}>
|
|
339
|
+
<SelectSearchPrimitive
|
|
340
|
+
ref={ref}
|
|
341
|
+
asChild
|
|
342
|
+
className={className}
|
|
343
|
+
style={style}
|
|
344
|
+
{...props}
|
|
345
|
+
{...mergeDataAttributes(props, slotData)}
|
|
346
|
+
>
|
|
347
|
+
{children}
|
|
348
|
+
</SelectSearchPrimitive>
|
|
349
|
+
</FormControlContext.Provider>
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return (
|
|
354
|
+
<FormControlContext.Provider value={ISOLATED_FIELD}>
|
|
355
|
+
<View className={cn(selectSearchVariants(), className)} style={style} {...slotData}>
|
|
356
|
+
<Input variant={variant} size={size ?? 'default'}>
|
|
357
|
+
<Input.Icon as={Search} />
|
|
358
|
+
<SelectSearchPrimitive ref={ref} asChild {...props}>
|
|
359
|
+
{inSheet ? (
|
|
360
|
+
<Input.Field asChild>
|
|
361
|
+
<PickerSheetTextInput />
|
|
362
|
+
</Input.Field>
|
|
363
|
+
) : (
|
|
364
|
+
<Input.Field />
|
|
365
|
+
)}
|
|
366
|
+
</SelectSearchPrimitive>
|
|
367
|
+
</Input>
|
|
368
|
+
</View>
|
|
369
|
+
</FormControlContext.Provider>
|
|
370
|
+
);
|
|
371
|
+
},
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
SelectSearch.displayName = 'Select.Search';
|
|
375
|
+
|
|
376
|
+
// =============================================================================
|
|
377
|
+
// STYLED EMPTY — shown when no option matches the search
|
|
378
|
+
// =============================================================================
|
|
379
|
+
|
|
380
|
+
export interface SelectEmptyProps extends ISelectEmptyProps {
|
|
381
|
+
asChild?: boolean;
|
|
382
|
+
/** Additional Uniwind/Tailwind classes, merged after the empty state's own styles. */
|
|
383
|
+
className?: string;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const SelectEmpty = forwardRef<View, SelectEmptyProps>(
|
|
387
|
+
({ asChild, className, style, children, ...props }, ref) => {
|
|
388
|
+
const { size } = useSelectStyleContext();
|
|
389
|
+
|
|
390
|
+
return (
|
|
391
|
+
<SelectEmptyPrimitive
|
|
392
|
+
ref={ref}
|
|
393
|
+
asChild={asChild}
|
|
394
|
+
className={cn(selectEmptyVariants(), className)}
|
|
395
|
+
style={style}
|
|
396
|
+
{...props}
|
|
397
|
+
{...mergeDataAttributes(props, dataAttributes({ slot: 'select-empty' }))}
|
|
398
|
+
>
|
|
399
|
+
{!asChild && typeof children === 'string' ? (
|
|
400
|
+
<Text className={selectEmptyTextVariants({ size })}>{children}</Text>
|
|
401
|
+
) : (
|
|
402
|
+
children
|
|
403
|
+
)}
|
|
404
|
+
</SelectEmptyPrimitive>
|
|
405
|
+
);
|
|
406
|
+
},
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
SelectEmpty.displayName = 'Select.Empty';
|
|
410
|
+
|
|
234
411
|
// =============================================================================
|
|
235
412
|
// STYLED CONTENT — composes Menu surface (listbox semantics stay on primitive)
|
|
236
413
|
// =============================================================================
|
|
@@ -240,8 +417,30 @@ export interface SelectContentProps extends ViewProps, ISelectContentProps {
|
|
|
240
417
|
className?: string;
|
|
241
418
|
}
|
|
242
419
|
|
|
420
|
+
const isSlot = (child: ReactNode, slot: unknown) => isValidElement(child) && child.type === slot;
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Sorts direct children into regions: `Select.Search` above a scrollable listbox of the
|
|
424
|
+
* remaining children, and `Select.Empty` below it. `Select.Search` and `Select.Empty` must be
|
|
425
|
+
* direct children: a `memo()`, HOC, or custom wrapper is not recognized and renders among the
|
|
426
|
+
* options. A second `Select.Search` or `Select.Empty` is dropped.
|
|
427
|
+
*/
|
|
428
|
+
function partitionPopupChildren(children: ReactNode) {
|
|
429
|
+
const childArray = Children.toArray(children);
|
|
430
|
+
return {
|
|
431
|
+
search: childArray.find((child) => isSlot(child, SelectSearch)),
|
|
432
|
+
empty: childArray.find((child) => isSlot(child, SelectEmpty)),
|
|
433
|
+
options: childArray.filter(
|
|
434
|
+
(child) => !isSlot(child, SelectSearch) && !isSlot(child, SelectEmpty),
|
|
435
|
+
),
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** Without `asChild`, direct children are sorted into regions (see `partitionPopupChildren`). */
|
|
243
440
|
const SelectContent = forwardRef<View, SelectContentProps>(
|
|
244
441
|
({ asChild, className, style, children, ...props }, ref) => {
|
|
442
|
+
const slotData = mergeDataAttributes(props, dataAttributes({ slot: 'select-content' }));
|
|
443
|
+
|
|
245
444
|
if (asChild) {
|
|
246
445
|
return (
|
|
247
446
|
<SelectContentPrimitive
|
|
@@ -250,24 +449,33 @@ const SelectContent = forwardRef<View, SelectContentProps>(
|
|
|
250
449
|
className={className}
|
|
251
450
|
style={style}
|
|
252
451
|
{...props}
|
|
253
|
-
{...
|
|
452
|
+
{...slotData}
|
|
254
453
|
>
|
|
255
454
|
{children}
|
|
256
455
|
</SelectContentPrimitive>
|
|
257
456
|
);
|
|
258
457
|
}
|
|
259
458
|
|
|
459
|
+
const { search, empty, options } = partitionPopupChildren(children);
|
|
460
|
+
|
|
260
461
|
return (
|
|
261
|
-
<SelectContentPrimitive
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
462
|
+
<SelectContentPrimitive ref={ref} asChild style={style} {...props} {...slotData}>
|
|
463
|
+
{search ? (
|
|
464
|
+
<Menu.Content asSurface className={className}>
|
|
465
|
+
{search}
|
|
466
|
+
<SelectListboxPrimitive asChild>
|
|
467
|
+
<Menu.Content asSurface scrollable className={selectSearchListboxVariants()}>
|
|
468
|
+
{options}
|
|
469
|
+
</Menu.Content>
|
|
470
|
+
</SelectListboxPrimitive>
|
|
471
|
+
{empty}
|
|
472
|
+
</Menu.Content>
|
|
473
|
+
) : (
|
|
474
|
+
<Menu.Content asSurface scrollable className={className}>
|
|
475
|
+
{options}
|
|
476
|
+
{empty}
|
|
477
|
+
</Menu.Content>
|
|
478
|
+
)}
|
|
271
479
|
</SelectContentPrimitive>
|
|
272
480
|
);
|
|
273
481
|
},
|
|
@@ -275,6 +483,168 @@ const SelectContent = forwardRef<View, SelectContentProps>(
|
|
|
275
483
|
|
|
276
484
|
SelectContent.displayName = 'Select.Content';
|
|
277
485
|
|
|
486
|
+
// =============================================================================
|
|
487
|
+
// STYLED BOTTOM SHEET — native surface in place of Select.Content
|
|
488
|
+
// =============================================================================
|
|
489
|
+
|
|
490
|
+
export interface SelectBottomSheetProps extends ViewProps {
|
|
491
|
+
/** Render the consumer's child as the sheet body, skipping the region split. */
|
|
492
|
+
asChild?: boolean;
|
|
493
|
+
/** Additional Uniwind/Tailwind classes, merged after the sheet body's own styles. */
|
|
494
|
+
className?: string;
|
|
495
|
+
/**
|
|
496
|
+
* Accessible name of the visually hidden dismiss button on web, where the surface is the
|
|
497
|
+
* `Select.Content` dropdown.
|
|
498
|
+
* @default 'Dismiss'
|
|
499
|
+
*/
|
|
500
|
+
dismissAccessibilityLabel?: string;
|
|
501
|
+
/** `Select.Search`, `Select.Item` elements, and `Select.Empty`, as in `Select.Content`. */
|
|
502
|
+
children?: ReactNode;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const SAFE_AREA_FALLBACK = { bottom: 0 };
|
|
506
|
+
|
|
507
|
+
const SelectNativeBottomSheet = forwardRef<View, SelectBottomSheetProps>(
|
|
508
|
+
(
|
|
509
|
+
{
|
|
510
|
+
asChild,
|
|
511
|
+
className,
|
|
512
|
+
children,
|
|
513
|
+
dismissAccessibilityLabel: _dismissAccessibilityLabel,
|
|
514
|
+
...props
|
|
515
|
+
},
|
|
516
|
+
ref,
|
|
517
|
+
) => {
|
|
518
|
+
const ctx = useSelectContext();
|
|
519
|
+
const insets = useContext(SafeAreaInsetsContext) ?? SAFE_AREA_FALLBACK;
|
|
520
|
+
const slotData = mergeDataAttributes(props, dataAttributes({ slot: 'select-bottom-sheet' }));
|
|
521
|
+
|
|
522
|
+
const { search, empty, options } = partitionPopupChildren(children);
|
|
523
|
+
|
|
524
|
+
// The sheet renders in the BottomSheet.Provider portal, outside this tree, so the select
|
|
525
|
+
// context is provided again inside it.
|
|
526
|
+
return (
|
|
527
|
+
<PickerBottomSheetSurface
|
|
528
|
+
open={ctx.open && !ctx.native}
|
|
529
|
+
setOpen={ctx.setOpen}
|
|
530
|
+
fill
|
|
531
|
+
// gorhom makes the sheet one accessibility element by default, which hides the rows
|
|
532
|
+
// from VoiceOver.
|
|
533
|
+
accessible={false}
|
|
534
|
+
keyboardBehavior="extend"
|
|
535
|
+
keyboardBlurBehavior="restore"
|
|
536
|
+
>
|
|
537
|
+
<SelectProvider value={ctx}>
|
|
538
|
+
<SelectSheetContext.Provider value>
|
|
539
|
+
{asChild ? (
|
|
540
|
+
<SelectSheetContentPrimitive ref={ref} asChild {...props} {...slotData}>
|
|
541
|
+
{children}
|
|
542
|
+
</SelectSheetContentPrimitive>
|
|
543
|
+
) : (
|
|
544
|
+
<SelectSheetContentPrimitive
|
|
545
|
+
ref={ref}
|
|
546
|
+
className={cn(selectBottomSheetVariants(), className)}
|
|
547
|
+
{...props}
|
|
548
|
+
{...slotData}
|
|
549
|
+
>
|
|
550
|
+
{search}
|
|
551
|
+
<BottomSheet.ScrollView
|
|
552
|
+
keyboardShouldPersistTaps="handled"
|
|
553
|
+
keyboardDismissMode="on-drag"
|
|
554
|
+
automaticallyAdjustKeyboardInsets
|
|
555
|
+
contentContainerStyle={{ paddingBottom: insets.bottom }}
|
|
556
|
+
>
|
|
557
|
+
<SelectListboxPrimitive className={selectBottomSheetListboxVariants()}>
|
|
558
|
+
{options}
|
|
559
|
+
</SelectListboxPrimitive>
|
|
560
|
+
{empty}
|
|
561
|
+
</BottomSheet.ScrollView>
|
|
562
|
+
</SelectSheetContentPrimitive>
|
|
563
|
+
)}
|
|
564
|
+
</SelectSheetContext.Provider>
|
|
565
|
+
</SelectProvider>
|
|
566
|
+
</PickerBottomSheetSurface>
|
|
567
|
+
);
|
|
568
|
+
},
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Presents the options in a bottom sheet on iOS and Android, in place of `Select.Content`. The
|
|
573
|
+
* sheet keeps `Select.Search` and the results above the soft keyboard, and closes on swipe
|
|
574
|
+
* down, backdrop press, Android back, and the VoiceOver escape gesture without changing the
|
|
575
|
+
* value. On web it renders the `Select.Content` dropdown. Requires `BottomSheet.Provider`.
|
|
576
|
+
*/
|
|
577
|
+
const SelectBottomSheet = forwardRef<View, SelectBottomSheetProps>((props, ref) =>
|
|
578
|
+
IS_WEB ? (
|
|
579
|
+
<SelectContent ref={ref} {...props} />
|
|
580
|
+
) : (
|
|
581
|
+
<SelectNativeBottomSheet ref={ref} {...props} />
|
|
582
|
+
),
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
SelectBottomSheet.displayName = 'Select.BottomSheet';
|
|
586
|
+
|
|
587
|
+
// =============================================================================
|
|
588
|
+
// STYLED ITEM CONTROL — Menu checkbox chrome driven by item selection
|
|
589
|
+
// =============================================================================
|
|
590
|
+
|
|
591
|
+
export interface SelectItemControlProps extends ViewProps {
|
|
592
|
+
/** Render the consumer's child element in place of the default checkbox glyph. */
|
|
593
|
+
asChild?: boolean;
|
|
594
|
+
/** Additional Uniwind/Tailwind classes, merged after the control's own styles. */
|
|
595
|
+
className?: string;
|
|
596
|
+
/** The element to render when `asChild` is set. Otherwise not rendered. */
|
|
597
|
+
children?: ReactNode;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const SelectItemControl = ({
|
|
601
|
+
asChild,
|
|
602
|
+
className,
|
|
603
|
+
style,
|
|
604
|
+
children,
|
|
605
|
+
...props
|
|
606
|
+
}: SelectItemControlProps) => {
|
|
607
|
+
const { checked } = useSelectItemContext();
|
|
608
|
+
const fieldState = checked ? 'checked' : 'idle';
|
|
609
|
+
|
|
610
|
+
const hiddenProps = {
|
|
611
|
+
className: cn(menuItemControlVariants(), 'pointer-events-none', className),
|
|
612
|
+
style,
|
|
613
|
+
accessible: false,
|
|
614
|
+
accessibilityElementsHidden: true,
|
|
615
|
+
importantForAccessibility: 'no-hide-descendants' as const,
|
|
616
|
+
'aria-hidden': true,
|
|
617
|
+
...props,
|
|
618
|
+
...mergeDataAttributes(props, dataAttributes({ slot: 'select-item-control', checked })),
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
if (asChild) {
|
|
622
|
+
return <Slot {...hiddenProps}>{children}</Slot>;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return (
|
|
626
|
+
<View {...hiddenProps}>
|
|
627
|
+
<View className={checkboxIndicatorVariants()} {...dataAttributes({ checked, fieldState })}>
|
|
628
|
+
{checked ? (
|
|
629
|
+
<Icon
|
|
630
|
+
as={Check}
|
|
631
|
+
className={checkboxIconVariants()}
|
|
632
|
+
accessibilityElementsHidden
|
|
633
|
+
aria-hidden
|
|
634
|
+
/>
|
|
635
|
+
) : null}
|
|
636
|
+
</View>
|
|
637
|
+
</View>
|
|
638
|
+
);
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
SelectItemControl.displayName = 'Select.ItemControl';
|
|
642
|
+
|
|
643
|
+
const hasItemControl = (children: ReactNode): boolean =>
|
|
644
|
+
Children.toArray(children).some(
|
|
645
|
+
(child) => isValidElement(child) && child.type === SelectItemControl,
|
|
646
|
+
);
|
|
647
|
+
|
|
278
648
|
// =============================================================================
|
|
279
649
|
// STYLED ITEM — composes Menu item visuals (option semantics stay on primitive)
|
|
280
650
|
// =============================================================================
|
|
@@ -284,6 +654,7 @@ export interface SelectItemProps extends ISelectItemProps {
|
|
|
284
654
|
/**
|
|
285
655
|
* Replaces the default selected-item checkmark. The replacement is rendered
|
|
286
656
|
* only while the item is selected. Pass `null` to suppress the indicator.
|
|
657
|
+
* The default checkmark is omitted when the item contains a `Select.ItemControl`.
|
|
287
658
|
*/
|
|
288
659
|
indicator?: ReactNode;
|
|
289
660
|
className?: string;
|
|
@@ -293,17 +664,20 @@ export interface SelectItemProps extends ISelectItemProps {
|
|
|
293
664
|
const SelectItem = forwardRef<View, SelectItemProps>(
|
|
294
665
|
({ asChild, indicator, className, children, style, value: itemValue, ...props }, ref) => {
|
|
295
666
|
const { size } = useSelectStyleContext();
|
|
296
|
-
const {
|
|
297
|
-
const checked =
|
|
667
|
+
const { isSelected } = useSelectContext();
|
|
668
|
+
const checked = isSelected(itemValue);
|
|
298
669
|
|
|
299
670
|
const computedClassName = cn(
|
|
300
671
|
menuItemVariants(),
|
|
672
|
+
selectItemVariants(),
|
|
301
673
|
size === 'small' ? 'px-2.5 py-1.5' : undefined,
|
|
302
674
|
className,
|
|
303
675
|
);
|
|
304
676
|
const selectedIndicator =
|
|
305
677
|
indicator === undefined ? (
|
|
306
|
-
|
|
678
|
+
hasItemControl(children) ? null : (
|
|
679
|
+
<Menu.ItemIndicator isChecked={checked} />
|
|
680
|
+
)
|
|
307
681
|
) : checked ? (
|
|
308
682
|
indicator
|
|
309
683
|
) : null;
|
|
@@ -385,8 +759,12 @@ type SelectCompoundComponent = typeof SelectRoot & {
|
|
|
385
759
|
Value: typeof SelectValue;
|
|
386
760
|
Icon: typeof SelectIcon;
|
|
387
761
|
Content: typeof SelectContent;
|
|
762
|
+
BottomSheet: typeof SelectBottomSheet;
|
|
763
|
+
Search: typeof SelectSearch;
|
|
764
|
+
Empty: typeof SelectEmpty;
|
|
388
765
|
Item: typeof SelectItem;
|
|
389
766
|
ItemLabel: typeof SelectItemLabel;
|
|
767
|
+
ItemControl: typeof SelectItemControl;
|
|
390
768
|
};
|
|
391
769
|
|
|
392
770
|
export const Select = Object.assign(SelectRoot, {
|
|
@@ -394,6 +772,10 @@ export const Select = Object.assign(SelectRoot, {
|
|
|
394
772
|
Value: SelectValue,
|
|
395
773
|
Icon: SelectIcon,
|
|
396
774
|
Content: SelectContent,
|
|
775
|
+
BottomSheet: SelectBottomSheet,
|
|
776
|
+
Search: SelectSearch,
|
|
777
|
+
Empty: SelectEmpty,
|
|
397
778
|
Item: SelectItem,
|
|
398
779
|
ItemLabel: SelectItemLabel,
|
|
780
|
+
ItemControl: SelectItemControl,
|
|
399
781
|
}) as SelectCompoundComponent;
|
|
@@ -119,4 +119,40 @@ export const selectIconVariants = cva(
|
|
|
119
119
|
},
|
|
120
120
|
);
|
|
121
121
|
|
|
122
|
+
// ── Item ────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
// Web keyboard navigation keeps DOM focus on the popup or the search input (virtual focus), so the
|
|
125
|
+
// highlighted option is painted from `data-highlighted` instead of a focus ring.
|
|
126
|
+
export const selectItemVariants = cva(['web:data-[highlighted=true]:bg-surface-action-tint-hover']);
|
|
127
|
+
|
|
128
|
+
// ── Search ──────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
export const selectSearchVariants = cva(['mb-1']);
|
|
131
|
+
|
|
132
|
+
export const selectSearchListboxVariants = cva([
|
|
133
|
+
'rounded-none border-0 bg-transparent p-0 shadow-none',
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
// ── Bottom sheet (native) ───────────────────────────────────
|
|
137
|
+
|
|
138
|
+
export const selectBottomSheetVariants = cva(['flex-1 px-3']);
|
|
139
|
+
|
|
140
|
+
export const selectBottomSheetListboxVariants = cva(['py-1']);
|
|
141
|
+
|
|
142
|
+
// ── Empty ───────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
export const selectEmptyVariants = cva(['px-3 py-2']);
|
|
145
|
+
|
|
146
|
+
export const selectEmptyTextVariants = cva(['text-start text-content-secondary'], {
|
|
147
|
+
variants: {
|
|
148
|
+
size: {
|
|
149
|
+
default: 'text-[0.875rem]',
|
|
150
|
+
small: 'text-[0.75rem]',
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
defaultVariants: {
|
|
154
|
+
size: 'default',
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
122
158
|
export type SelectVariantProps = VariantProps<typeof selectTriggerVariants>;
|
package/src/components/index.ts
CHANGED
|
@@ -34,6 +34,8 @@ export * from './Field';
|
|
|
34
34
|
export * from './Form';
|
|
35
35
|
export * from './FullScreenDialog';
|
|
36
36
|
export * from './Image';
|
|
37
|
+
export * from './Insight';
|
|
38
|
+
export * from './InsightStack';
|
|
37
39
|
export * from './InsuranceDisclosure';
|
|
38
40
|
export * from './Input';
|
|
39
41
|
export * from './OtpInput';
|