react-native-molecules 0.5.0-beta.3 → 0.5.0-beta.30
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/components/Accordion/Accordion.tsx +2 -6
- package/components/Accordion/AccordionItem.tsx +16 -12
- package/components/Accordion/AccordionItemContent.tsx +6 -1
- package/components/Accordion/AccordionItemHeader.tsx +1 -1
- package/components/Accordion/utils.ts +6 -0
- package/components/ActivityIndicator/ActivityIndicator.tsx +6 -15
- package/components/Appbar/AppbarBase.tsx +18 -13
- package/components/Button/Button.tsx +211 -264
- package/components/Button/index.tsx +9 -3
- package/components/Button/types.ts +16 -2
- package/components/Button/utils.ts +230 -208
- package/components/Card/Card.tsx +1 -1
- package/components/Checkbox/Checkbox.tsx +125 -88
- package/components/Checkbox/CheckboxBase.ios.tsx +14 -23
- package/components/Checkbox/CheckboxBase.tsx +21 -137
- package/components/Checkbox/context.tsx +14 -0
- package/components/Checkbox/index.tsx +11 -4
- package/components/Checkbox/types.ts +63 -29
- package/components/Checkbox/utils.ts +25 -108
- package/components/Chip/Chip.tsx +41 -52
- package/components/Chip/utils.ts +3 -7
- package/components/DateField/DateField.tsx +111 -0
- package/components/DateField/index.tsx +6 -0
- package/components/{DatePickerInput/inputUtils.ts → DateField/useDateFieldState.ts} +19 -51
- package/components/DatePicker/DateCalendar.tsx +83 -0
- package/components/DatePicker/DatePickerActions.tsx +73 -0
- package/components/DatePicker/DatePickerModal.tsx +246 -0
- package/components/DatePicker/DatePickerPopover.tsx +79 -0
- package/components/DatePicker/DatePickerProvider.tsx +158 -0
- package/components/DatePicker/DatePickerTrigger.tsx +23 -0
- package/components/DatePicker/context.tsx +83 -0
- package/components/DatePicker/index.tsx +45 -0
- package/components/DatePicker/utils.ts +295 -0
- package/components/DatePickerInline/DatePickerDockedHeader.tsx +117 -0
- package/components/DatePickerInline/DatePickerInline.tsx +17 -16
- package/components/DatePickerInline/DatePickerInlineBase.tsx +11 -5
- package/components/DatePickerInline/DatePickerInlineHeader.tsx +50 -20
- package/components/DatePickerInline/Day.tsx +25 -1
- package/components/DatePickerInline/DayNames.tsx +13 -10
- package/components/DatePickerInline/DayRange.tsx +2 -4
- package/components/DatePickerInline/HeaderItem.tsx +44 -29
- package/components/DatePickerInline/Month.tsx +48 -67
- package/components/DatePickerInline/MonthPicker.tsx +80 -92
- package/components/DatePickerInline/Swiper.native.tsx +21 -4
- package/components/DatePickerInline/Swiper.tsx +169 -14
- package/components/DatePickerInline/SwiperUtils.ts +1 -1
- package/components/DatePickerInline/Week.tsx +6 -1
- package/components/DatePickerInline/YearPicker.tsx +220 -78
- package/components/DatePickerInline/dateUtils.tsx +18 -13
- package/components/DatePickerInline/store.tsx +27 -0
- package/components/DatePickerInline/types.ts +6 -2
- package/components/DatePickerInline/utils.ts +66 -29
- package/components/Divider/Divider.tsx +192 -0
- package/components/Divider/index.tsx +10 -0
- package/components/Drawer/Drawer.tsx +17 -6
- package/components/Drawer/DrawerItemGroup.tsx +3 -7
- package/components/ElementGroup/ElementGroup.tsx +1 -1
- package/components/FilePicker/FilePicker.tsx +48 -78
- package/components/FilePicker/index.tsx +2 -1
- package/components/FilePicker/utils.ts +9 -0
- package/components/HelperText/HelperText.tsx +0 -35
- package/components/Icon/iconFactory.tsx +5 -4
- package/components/Icon/index.tsx +1 -1
- package/components/Icon/types.ts +17 -6
- package/components/IconButton/IconButton.tsx +84 -84
- package/components/IconButton/index.tsx +1 -0
- package/components/IconButton/types.ts +10 -0
- package/components/IconButton/utils.ts +167 -33
- package/components/List/List.tsx +276 -0
- package/components/List/context.tsx +27 -0
- package/components/List/index.ts +8 -0
- package/components/List/types.ts +117 -0
- package/components/List/utils.ts +79 -0
- package/components/LoadingIndicator/LoadingIndicator.tsx +253 -0
- package/components/LoadingIndicator/LoadingIndicator.web.tsx +136 -0
- package/components/LoadingIndicator/index.tsx +13 -0
- package/components/LoadingIndicator/utils.ts +117 -0
- package/components/Menu/Menu.tsx +162 -39
- package/components/Menu/index.tsx +10 -7
- package/components/Menu/utils.ts +21 -70
- package/components/NavigationRail/NavigationRail.tsx +15 -9
- package/components/Popover/Popover.tsx +119 -145
- package/components/Popover/PopoverRoot.tsx +60 -0
- package/components/Popover/common.ts +54 -34
- package/components/Popover/index.ts +12 -1
- package/components/Popover/usePlatformMeasure.native.ts +90 -0
- package/components/Popover/usePlatformMeasure.ts +120 -0
- package/components/Popover/utils.ts +34 -0
- package/components/Portal/Portal.tsx +1 -2
- package/components/Radio/Radio.tsx +188 -0
- package/components/Radio/RadioBase.ios.tsx +69 -0
- package/components/Radio/RadioBase.tsx +136 -0
- package/components/Radio/context.tsx +23 -0
- package/components/Radio/index.tsx +20 -0
- package/components/Radio/types.ts +101 -0
- package/components/Radio/utils.ts +115 -0
- package/components/Rating/Rating.tsx +1 -1
- package/components/Select/Select.tsx +521 -785
- package/components/Select/context.tsx +81 -0
- package/components/Select/index.ts +26 -14
- package/components/Select/types.ts +65 -58
- package/components/Select/utils.ts +126 -0
- package/components/Slot/Slot.tsx +244 -0
- package/components/Slot/compose-refs.tsx +62 -0
- package/components/Slot/index.tsx +8 -0
- package/components/Surface/Surface.android.tsx +32 -7
- package/components/Surface/Surface.ios.tsx +34 -29
- package/components/Surface/Surface.tsx +31 -4
- package/components/Surface/utils.ts +44 -6
- package/components/Switch/Switch.ios.tsx +1 -1
- package/components/Switch/Switch.tsx +10 -3
- package/components/Tabs/TabItem.tsx +35 -58
- package/components/Tabs/TabLabel.tsx +5 -9
- package/components/Tabs/Tabs.tsx +156 -150
- package/components/Tabs/utils.ts +15 -2
- package/components/Text/textFactory.tsx +17 -5
- package/components/TextInput/TextInput.tsx +663 -579
- package/components/TextInput/index.tsx +19 -3
- package/components/TextInput/types.ts +77 -28
- package/components/TextInput/utils.ts +235 -145
- package/components/TimeField/TimeField.tsx +75 -0
- package/components/TimeField/index.tsx +6 -0
- package/components/TimeField/useTimeFieldState.ts +70 -0
- package/components/{TimePickerField/sanitizeTime.ts → TimeField/utils.ts} +77 -10
- package/components/TimePicker/AnalogClock.tsx +1 -1
- package/components/TimePicker/TimeInput.tsx +87 -42
- package/components/TimePicker/TimeInputs.tsx +138 -50
- package/components/TimePicker/TimePicker.tsx +74 -11
- package/components/TimePicker/TimePickerModal.tsx +186 -0
- package/components/TimePicker/context.tsx +17 -0
- package/components/TimePicker/index.tsx +15 -3
- package/components/TimePicker/utils.ts +93 -4
- package/components/Tooltip/Tooltip.tsx +42 -67
- package/components/Tooltip/TooltipContent.tsx +32 -5
- package/components/Tooltip/TooltipTrigger.tsx +20 -20
- package/components/Tooltip/index.tsx +1 -1
- package/components/TouchableRipple/TouchableRipple.native.tsx +83 -16
- package/components/TouchableRipple/TouchableRipple.tsx +150 -102
- package/components/TouchableRipple/rippleFromForegroundColor.ts +21 -0
- package/hocs/index.tsx +1 -1
- package/hocs/withKeyboardAccessibility.tsx +2 -3
- package/hocs/withPortal.tsx +1 -1
- package/hooks/index.tsx +2 -12
- package/hooks/useActionState.tsx +19 -8
- package/hooks/useContrastColor.ts +1 -2
- package/hooks/useFilePicker.tsx +7 -17
- package/hooks/useHandleNumberFormat.tsx +2 -2
- package/hooks/useMediaQuery.tsx +1 -2
- package/package.json +95 -111
- package/shortcuts-manager/ShortcutsManager/ShortcutsManager.tsx +6 -3
- package/shortcuts-manager/ShortcutsManager/utils.tsx +1 -1
- package/shortcuts-manager/useSetScopes/useSetScopes.tsx +1 -1
- package/shortcuts-manager/useShortcut/useShortcut.tsx +1 -1
- package/styles/shadow.ts +2 -1
- package/styles/themes/LightTheme.tsx +1 -1
- package/utils/DocumentPicker/documentPicker.ts +78 -27
- package/utils/DocumentPicker/types.ts +0 -1
- package/utils/extractSubcomponents.ts +89 -0
- package/utils/extractTextStyles.ts +1 -2
- package/utils/formatNumberWithMask/formatNumberWithMask.ts +2 -1
- package/utils/index.ts +0 -3
- package/utils/normalizeToNumberString/normalizeToNumberString.ts +1 -1
- package/components/DatePickerDocked/DatePickerDocked.tsx +0 -30
- package/components/DatePickerDocked/DatePickerDockedHeader.tsx +0 -129
- package/components/DatePickerDocked/index.tsx +0 -17
- package/components/DatePickerDocked/types.ts +0 -11
- package/components/DatePickerDocked/utils.ts +0 -157
- package/components/DatePickerInline/DatePickerContext.tsx +0 -21
- package/components/DatePickerInput/DatePickerInput.tsx +0 -139
- package/components/DatePickerInput/DatePickerInputModal.tsx +0 -48
- package/components/DatePickerInput/DatePickerInputWithoutModal.tsx +0 -77
- package/components/DatePickerInput/DateRangeInput.tsx +0 -88
- package/components/DatePickerInput/index.tsx +0 -10
- package/components/DatePickerInput/types.ts +0 -28
- package/components/DatePickerInput/utils.ts +0 -15
- package/components/DatePickerModal/AnimatedCrossView.tsx +0 -94
- package/components/DatePickerModal/CalendarEdit.tsx +0 -139
- package/components/DatePickerModal/DatePickerModal.tsx +0 -85
- package/components/DatePickerModal/DatePickerModalContent.tsx +0 -155
- package/components/DatePickerModal/DatePickerModalContentHeader.tsx +0 -213
- package/components/DatePickerModal/DatePickerModalHeader.tsx +0 -74
- package/components/DatePickerModal/DatePickerModalHeaderBackground.tsx +0 -13
- package/components/DatePickerModal/index.tsx +0 -16
- package/components/DatePickerModal/types.ts +0 -92
- package/components/DatePickerModal/utils.ts +0 -122
- package/components/DateTimePicker/DateTimePicker.tsx +0 -172
- package/components/DateTimePicker/index.tsx +0 -10
- package/components/DateTimePicker/utils.ts +0 -12
- package/components/HorizontalDivider/HorizontalDivider.tsx +0 -103
- package/components/HorizontalDivider/index.tsx +0 -9
- package/components/ListItem/ListItem.tsx +0 -136
- package/components/ListItem/ListItemDescription.tsx +0 -25
- package/components/ListItem/ListItemTitle.tsx +0 -25
- package/components/ListItem/index.tsx +0 -14
- package/components/ListItem/utils.ts +0 -115
- package/components/Menu/MenuDivider.tsx +0 -13
- package/components/Menu/MenuItem.tsx +0 -128
- package/components/Popover/Popover.native.tsx +0 -185
- package/components/RadioButton/RadioButton.tsx +0 -138
- package/components/RadioButton/RadioButtonAndroid.tsx +0 -188
- package/components/RadioButton/RadioButtonGroup.tsx +0 -98
- package/components/RadioButton/RadioButtonIOS.tsx +0 -106
- package/components/RadioButton/RadioButtonItem.tsx +0 -232
- package/components/RadioButton/index.ts +0 -22
- package/components/RadioButton/utils.ts +0 -165
- package/components/TimePickerField/TimePickerField.tsx +0 -152
- package/components/TimePickerField/index.tsx +0 -10
- package/components/TimePickerField/utils.ts +0 -94
- package/components/TimePickerModal/TimePickerModal.tsx +0 -115
- package/components/TimePickerModal/index.tsx +0 -10
- package/components/TimePickerModal/utils.ts +0 -47
- package/components/VerticalDivider/VerticalDivider.tsx +0 -100
- package/components/VerticalDivider/index.tsx +0 -9
- package/context-bridge/index.tsx +0 -87
- package/fast-context/index.tsx +0 -190
- package/hocs/typedMemo.tsx +0 -5
- package/hooks/useControlledValue.tsx +0 -68
- package/hooks/useLatest.tsx +0 -9
- package/hooks/useMergedRefs.ts +0 -14
- package/hooks/usePrevious.ts +0 -13
- package/hooks/useSearchable.tsx +0 -74
- package/hooks/useSubcomponents.tsx +0 -59
- package/hooks/useToggle.tsx +0 -24
- package/utils/color.ts +0 -22
- package/utils/compare/index.ts +0 -54
- package/utils/lodash.ts +0 -49
- package/utils/repository.ts +0 -53
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { useControlledValue, useToggle } from '@react-native-molecules/utils/hooks';
|
|
1
2
|
import {
|
|
2
|
-
|
|
3
|
+
Fragment,
|
|
3
4
|
memo,
|
|
5
|
+
type ReactNode,
|
|
4
6
|
useCallback,
|
|
5
|
-
useContext,
|
|
6
7
|
useEffect,
|
|
7
8
|
useMemo,
|
|
8
9
|
useRef,
|
|
@@ -14,315 +15,279 @@ import {
|
|
|
14
15
|
type LayoutChangeEvent,
|
|
15
16
|
Platform,
|
|
16
17
|
Pressable,
|
|
17
|
-
ScrollView,
|
|
18
18
|
View,
|
|
19
19
|
} from 'react-native';
|
|
20
|
-
import { StyleSheet } from 'react-native-unistyles';
|
|
21
20
|
|
|
22
|
-
import {
|
|
23
|
-
import { useToggle } from '../../hooks';
|
|
21
|
+
import { typedMemo } from '../../hocs';
|
|
24
22
|
import { resolveStateVariant } from '../../utils';
|
|
23
|
+
import { extractSubcomponents } from '../../utils/extractSubcomponents';
|
|
25
24
|
import { Chip } from '../Chip';
|
|
26
25
|
import { Icon } from '../Icon';
|
|
27
26
|
import { IconButton } from '../IconButton';
|
|
27
|
+
import { List } from '../List';
|
|
28
28
|
import { Popover } from '../Popover';
|
|
29
|
-
import { registerPortalContext } from '../Portal';
|
|
30
29
|
import { Text } from '../Text';
|
|
31
30
|
import { TextInput, type TextInputHandles, type TextInputProps } from '../TextInput';
|
|
31
|
+
import {
|
|
32
|
+
SelectDropdownContextProvider,
|
|
33
|
+
SelectSearchContextProvider,
|
|
34
|
+
useSelectContextValue,
|
|
35
|
+
useSelectDropdownContextValue,
|
|
36
|
+
useSelectDropdownStoreRef,
|
|
37
|
+
useSelectSearchContextValue,
|
|
38
|
+
} from './context';
|
|
32
39
|
import type {
|
|
33
40
|
DefaultItemT,
|
|
34
41
|
SelectContentProps,
|
|
35
|
-
SelectContextValue,
|
|
36
|
-
SelectDropdownContextValue,
|
|
37
42
|
SelectDropdownProps,
|
|
38
|
-
SelectGroupProps,
|
|
39
43
|
SelectOptionProps,
|
|
40
|
-
|
|
44
|
+
SelectProps,
|
|
45
|
+
SelectSearchContextValue,
|
|
41
46
|
SelectSearchInputProps,
|
|
47
|
+
SelectSearchKey,
|
|
48
|
+
SelectTriggerOutlineProps,
|
|
42
49
|
SelectTriggerProps,
|
|
43
50
|
SelectValueProps,
|
|
44
51
|
} from './types';
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
52
|
+
import {
|
|
53
|
+
collectWebSelectKeyboardOptionElements,
|
|
54
|
+
selectOutlineStyles,
|
|
55
|
+
styles,
|
|
56
|
+
triggerStyles,
|
|
57
|
+
} from './utils';
|
|
58
|
+
|
|
59
|
+
const emptyArr: unknown[] = [];
|
|
60
|
+
|
|
61
|
+
export const getSelectTriggerState = ({
|
|
62
|
+
isOpen,
|
|
63
|
+
hovered,
|
|
64
|
+
disabled,
|
|
65
|
+
error,
|
|
66
|
+
}: {
|
|
67
|
+
isOpen: boolean;
|
|
68
|
+
hovered: boolean;
|
|
69
|
+
disabled: boolean;
|
|
70
|
+
error: boolean;
|
|
71
|
+
}) =>
|
|
72
|
+
resolveStateVariant({
|
|
73
|
+
focused: isOpen,
|
|
74
|
+
hovered,
|
|
75
|
+
disabled,
|
|
76
|
+
error,
|
|
77
|
+
hoveredAndFocused: hovered && isOpen,
|
|
78
|
+
errorFocused: error && isOpen,
|
|
79
|
+
errorHovered: error && hovered,
|
|
80
|
+
errorFocusedAndHovered: error && isOpen && hovered,
|
|
81
|
+
errorDisabled: error && disabled,
|
|
82
|
+
}) as any;
|
|
83
|
+
|
|
84
|
+
export const getDisplayLabel = (item: DefaultItemT, labelKey?: string) => {
|
|
85
|
+
const itemLabelKey = typeof item.labelKey === 'string' ? item.labelKey : undefined;
|
|
86
|
+
const key = labelKey ?? itemLabelKey ?? 'label';
|
|
87
|
+
const value = item[key];
|
|
88
|
+
return value == null ? String(item.id) : String(value);
|
|
78
89
|
};
|
|
79
90
|
|
|
80
|
-
export const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
registerPortalContext([SelectContext, SelectDropdownContext]);
|
|
91
|
-
|
|
92
|
-
export const useSelectDropdownContext = () => {
|
|
93
|
-
return useContext(SelectDropdownContext);
|
|
91
|
+
export const getNested = (item: unknown, path: string): unknown => {
|
|
92
|
+
if (item == null || typeof item !== 'object') return undefined;
|
|
93
|
+
if (!path.includes('.')) return (item as Record<string, unknown>)[path];
|
|
94
|
+
let val: unknown = item;
|
|
95
|
+
for (const part of path.split('.')) {
|
|
96
|
+
if (val == null || typeof val !== 'object') return undefined;
|
|
97
|
+
val = (val as Record<string, unknown>)[part];
|
|
98
|
+
}
|
|
99
|
+
return val;
|
|
94
100
|
};
|
|
95
101
|
|
|
96
|
-
export const
|
|
97
|
-
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
export const matchesByKey = (item: unknown, key: string, lowerQuery: string): boolean =>
|
|
103
|
+
String(getNested(item, key) ?? '')
|
|
104
|
+
.toLowerCase()
|
|
105
|
+
.includes(lowerQuery);
|
|
106
|
+
|
|
107
|
+
export const applySearch = <T extends object>(
|
|
108
|
+
items: T[],
|
|
109
|
+
searchKey: SelectSearchKey<T> | undefined,
|
|
110
|
+
query: string,
|
|
111
|
+
): T[] => {
|
|
112
|
+
if (!query) return items;
|
|
113
|
+
if (typeof searchKey === 'function') {
|
|
114
|
+
return items.filter(item => searchKey(item, query));
|
|
115
|
+
}
|
|
116
|
+
const keys = Array.isArray(searchKey) ? searchKey : [searchKey || 'label'];
|
|
117
|
+
const lowerQuery = query.toLowerCase();
|
|
118
|
+
return items.filter(item => keys.some(key => matchesByKey(item, key, lowerQuery)));
|
|
101
119
|
};
|
|
102
120
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const handleSearchQueryChange = useCallback(
|
|
129
|
-
(query: string) => {
|
|
130
|
-
setSearchQuery(query);
|
|
131
|
-
onSearchChange?.(query);
|
|
132
|
-
},
|
|
133
|
-
[onSearchChange],
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
// Default hideSelected to multiple (true for multi-select, false for single select)
|
|
137
|
-
const hideSelected = hideSelectedProp !== undefined ? hideSelectedProp : multiple;
|
|
121
|
+
export const SelectDropdownProvider = memo(
|
|
122
|
+
({
|
|
123
|
+
children,
|
|
124
|
+
isOpen: isOpenProp,
|
|
125
|
+
onClose: onCloseProp,
|
|
126
|
+
}: {
|
|
127
|
+
children: ReactNode;
|
|
128
|
+
isOpen?: boolean;
|
|
129
|
+
onClose?: () => void;
|
|
130
|
+
}) => {
|
|
131
|
+
const { state: isOpen, handleOpen, handleClose } = useToggle(false);
|
|
132
|
+
const triggerRef = useRef<View>(null);
|
|
133
|
+
const [triggerLayout, setTriggerLayout] = useState<{
|
|
134
|
+
width: number;
|
|
135
|
+
height: number;
|
|
136
|
+
} | null>(null);
|
|
137
|
+
const isControlled = isOpenProp !== undefined;
|
|
138
|
+
|
|
139
|
+
const onClose = useCallback(() => {
|
|
140
|
+
if (isControlled) {
|
|
141
|
+
onCloseProp?.();
|
|
142
|
+
} else {
|
|
143
|
+
handleClose();
|
|
144
|
+
}
|
|
145
|
+
}, [isControlled, onCloseProp, handleClose]);
|
|
138
146
|
|
|
139
|
-
|
|
140
|
-
|
|
147
|
+
const onOpen = useCallback(() => {
|
|
148
|
+
if (!isControlled) {
|
|
149
|
+
handleOpen();
|
|
150
|
+
}
|
|
151
|
+
}, [handleOpen, isControlled]);
|
|
152
|
+
|
|
153
|
+
const contextValue = useMemo(
|
|
154
|
+
() => ({
|
|
155
|
+
isOpen: isControlled ? isOpenProp! : isOpen,
|
|
156
|
+
onClose,
|
|
157
|
+
onOpen,
|
|
158
|
+
triggerRef: triggerRef as React.RefObject<View>,
|
|
159
|
+
triggerLayout,
|
|
160
|
+
setTriggerLayout,
|
|
161
|
+
}),
|
|
162
|
+
[isControlled, isOpenProp, isOpen, onClose, onOpen, triggerLayout],
|
|
163
|
+
);
|
|
141
164
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const singleValue = value as Option['id'] | null;
|
|
150
|
-
return singleValue !== item.id;
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
}
|
|
165
|
+
return (
|
|
166
|
+
<SelectDropdownContextProvider value={contextValue}>
|
|
167
|
+
{children}
|
|
168
|
+
</SelectDropdownContextProvider>
|
|
169
|
+
);
|
|
170
|
+
},
|
|
171
|
+
);
|
|
154
172
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
173
|
+
export const SelectRoot = typedMemo(
|
|
174
|
+
<Option extends DefaultItemT = DefaultItemT>({
|
|
175
|
+
children,
|
|
176
|
+
options = emptyArr as Option[],
|
|
177
|
+
searchKey,
|
|
178
|
+
searchQuery: searchQueryProp,
|
|
179
|
+
defaultSearchQuery,
|
|
180
|
+
onSearchChange,
|
|
181
|
+
searchMode = 'client',
|
|
182
|
+
getItemId,
|
|
183
|
+
...listProps
|
|
184
|
+
}: SelectProps<Option>) => {
|
|
185
|
+
const [searchQuery, setSearchQuery] = useControlledValue<string>({
|
|
186
|
+
value: searchQueryProp,
|
|
187
|
+
defaultValue: defaultSearchQuery ?? '',
|
|
188
|
+
onChange: onSearchChange,
|
|
189
|
+
});
|
|
166
190
|
|
|
167
|
-
|
|
168
|
-
|
|
191
|
+
const getOptionId = useMemo(
|
|
192
|
+
() => (getItemId ?? ((item: Option) => item.id)) as (item: Option) => string | number,
|
|
193
|
+
[getItemId],
|
|
194
|
+
);
|
|
169
195
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (!currentValue.find(v => v === item.id)) {
|
|
175
|
-
onValueChange([...currentValue, item.id] as Option['id'][], item);
|
|
176
|
-
}
|
|
177
|
-
} else {
|
|
178
|
-
onValueChange(item.id, item);
|
|
179
|
-
}
|
|
180
|
-
},
|
|
181
|
-
[multiple, value, onValueChange],
|
|
182
|
-
);
|
|
196
|
+
const filteredOptions = useMemo(() => {
|
|
197
|
+
if (searchMode === 'external') return options;
|
|
198
|
+
return applySearch(options, searchKey, searchQuery);
|
|
199
|
+
}, [options, searchKey, searchMode, searchQuery]);
|
|
183
200
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
onValueChange(currentValue.filter(v => v !== item.id) as Option['id'][], item);
|
|
189
|
-
} else {
|
|
190
|
-
onValueChange(null, item);
|
|
201
|
+
const optionById = useMemo(() => {
|
|
202
|
+
const map = new Map<string | number, Option>();
|
|
203
|
+
for (const option of options) {
|
|
204
|
+
map.set(getOptionId(option), option);
|
|
191
205
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
options,
|
|
206
|
-
|
|
207
|
-
setSearchQuery: handleSearchQueryChange,
|
|
208
|
-
filteredOptions,
|
|
209
|
-
}),
|
|
210
|
-
[
|
|
211
|
-
value,
|
|
212
|
-
multiple,
|
|
213
|
-
onAdd,
|
|
214
|
-
onRemove,
|
|
215
|
-
disabled,
|
|
216
|
-
error,
|
|
217
|
-
labelKey,
|
|
218
|
-
options,
|
|
219
|
-
searchQuery,
|
|
220
|
-
handleSearchQueryChange,
|
|
221
|
-
filteredOptions,
|
|
222
|
-
],
|
|
223
|
-
);
|
|
224
|
-
|
|
225
|
-
return (
|
|
226
|
-
<SelectContext.Provider value={contextValue as unknown as SelectContextValue<DefaultItemT>}>
|
|
227
|
-
{children}
|
|
228
|
-
</SelectContext.Provider>
|
|
229
|
-
);
|
|
230
|
-
};
|
|
206
|
+
return map;
|
|
207
|
+
}, [getOptionId, options]);
|
|
208
|
+
|
|
209
|
+
const searchContextValue = useMemo(
|
|
210
|
+
() =>
|
|
211
|
+
({
|
|
212
|
+
searchQuery,
|
|
213
|
+
setSearchQuery,
|
|
214
|
+
allOptions: options,
|
|
215
|
+
options: filteredOptions,
|
|
216
|
+
optionById,
|
|
217
|
+
getOptionId,
|
|
218
|
+
} as unknown as SelectSearchContextValue<DefaultItemT>),
|
|
219
|
+
[filteredOptions, getOptionId, optionById, options, searchQuery, setSearchQuery],
|
|
220
|
+
);
|
|
231
221
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}) => {
|
|
242
|
-
const { state: isOpen, handleOpen, handleClose } = useToggle(false);
|
|
243
|
-
const triggerRef = useRef<View>(null);
|
|
244
|
-
const contentRef = useRef<any>(null);
|
|
245
|
-
const [triggerLayout, setTriggerLayout] = useState<{ width: number; height: number } | null>(
|
|
246
|
-
null,
|
|
247
|
-
);
|
|
248
|
-
const isControlled = isOpenProp !== undefined;
|
|
222
|
+
return (
|
|
223
|
+
<SelectSearchContextProvider value={searchContextValue}>
|
|
224
|
+
<List {...listProps}>
|
|
225
|
+
<SelectDropdownProvider>{children}</SelectDropdownProvider>
|
|
226
|
+
</List>
|
|
227
|
+
</SelectSearchContextProvider>
|
|
228
|
+
);
|
|
229
|
+
},
|
|
230
|
+
);
|
|
249
231
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}
|
|
256
|
-
|
|
232
|
+
export const SelectContent = typedMemo(
|
|
233
|
+
<Option extends DefaultItemT = DefaultItemT>({
|
|
234
|
+
children,
|
|
235
|
+
...rest
|
|
236
|
+
}: SelectContentProps<Option>) => {
|
|
237
|
+
const { options, getOptionId } = useSelectSearchContextValue(state => ({
|
|
238
|
+
options: state.options as Option[],
|
|
239
|
+
getOptionId: state.getOptionId as (item: Option) => string | number,
|
|
240
|
+
}));
|
|
241
|
+
const isSelectedId = useSelectContextValue(state => state.isSelectedId);
|
|
257
242
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
handleOpen();
|
|
243
|
+
if (typeof children !== 'function') {
|
|
244
|
+
return <List.Content {...rest}>{children}</List.Content>;
|
|
261
245
|
}
|
|
262
|
-
}, [handleOpen, isControlled]);
|
|
263
|
-
|
|
264
|
-
const contextValue = useMemo(
|
|
265
|
-
() => ({
|
|
266
|
-
isOpen: isControlled ? isOpenProp! : isOpen,
|
|
267
|
-
onClose,
|
|
268
|
-
onOpen,
|
|
269
|
-
triggerRef: triggerRef as React.RefObject<View>,
|
|
270
|
-
contentRef,
|
|
271
|
-
triggerLayout,
|
|
272
|
-
setTriggerLayout,
|
|
273
|
-
}),
|
|
274
|
-
[isControlled, isOpenProp, isOpen, onClose, onOpen, triggerLayout],
|
|
275
|
-
);
|
|
276
|
-
|
|
277
|
-
return (
|
|
278
|
-
<SelectDropdownContext.Provider value={contextValue}>
|
|
279
|
-
{children}
|
|
280
|
-
</SelectDropdownContext.Provider>
|
|
281
|
-
);
|
|
282
|
-
};
|
|
283
246
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
247
|
+
return (
|
|
248
|
+
<List.Content {...rest}>
|
|
249
|
+
{options.map(item => (
|
|
250
|
+
<Fragment key={String(getOptionId(item))}>
|
|
251
|
+
{children(item, isSelectedId(getOptionId(item)))}
|
|
252
|
+
</Fragment>
|
|
253
|
+
))}
|
|
254
|
+
</List.Content>
|
|
255
|
+
);
|
|
256
|
+
},
|
|
257
|
+
);
|
|
295
258
|
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
const { onOpen, isOpen, triggerRef, setTriggerLayout } = useSelectDropdownContextValue(
|
|
259
|
+
export const SelectTrigger = memo(({ children, style, ...rest }: SelectTriggerProps) => {
|
|
260
|
+
const { isOpen, onOpen, onClose, triggerRef, setTriggerLayout } = useSelectDropdownContextValue(
|
|
299
261
|
state => ({
|
|
300
|
-
onOpen: state.onOpen,
|
|
301
262
|
isOpen: state.isOpen,
|
|
263
|
+
onOpen: state.onOpen,
|
|
264
|
+
onClose: state.onClose,
|
|
302
265
|
triggerRef: state.triggerRef,
|
|
303
266
|
setTriggerLayout: state.setTriggerLayout,
|
|
304
267
|
}),
|
|
305
268
|
);
|
|
269
|
+
const setSelectDropdownContext = useSelectDropdownStoreRef().set;
|
|
306
270
|
|
|
307
271
|
const { disabled, error } = useSelectContextValue(state => ({
|
|
308
272
|
disabled: state.disabled,
|
|
309
273
|
error: state.error,
|
|
310
274
|
}));
|
|
311
275
|
|
|
312
|
-
const
|
|
276
|
+
const [hovered, setHovered] = useState(false);
|
|
277
|
+
|
|
278
|
+
const { Select_TriggerOutline, rest: restChildren } = extractSubcomponents({
|
|
279
|
+
children,
|
|
280
|
+
allowedChildren: [{ name: 'Select_TriggerOutline', allowMultiple: false }] as const,
|
|
281
|
+
includeRest: true,
|
|
282
|
+
});
|
|
313
283
|
|
|
314
284
|
triggerStyles.useVariants({
|
|
315
|
-
state:
|
|
316
|
-
|
|
285
|
+
state: getSelectTriggerState({
|
|
286
|
+
isOpen,
|
|
317
287
|
hovered,
|
|
318
288
|
disabled: !!disabled,
|
|
319
289
|
error: !!error,
|
|
320
|
-
|
|
321
|
-
errorFocused: !!error && isOpen,
|
|
322
|
-
errorHovered: !!error && hovered,
|
|
323
|
-
errorFocusedAndHovered: !!error && isOpen && hovered,
|
|
324
|
-
errorDisabled: !!error && !!disabled,
|
|
325
|
-
}) as any,
|
|
290
|
+
}),
|
|
326
291
|
});
|
|
327
292
|
|
|
328
293
|
const handleLayout = useCallback(
|
|
@@ -334,180 +299,240 @@ const SelectTrigger = ({ children, style, ...rest }: SelectTriggerProps) => {
|
|
|
334
299
|
);
|
|
335
300
|
|
|
336
301
|
const handlePress = useCallback(() => {
|
|
337
|
-
if (
|
|
302
|
+
if (disabled) return;
|
|
303
|
+
if (!isOpen) {
|
|
338
304
|
onOpen();
|
|
305
|
+
} else {
|
|
306
|
+
onClose();
|
|
339
307
|
}
|
|
340
|
-
}, [isOpen, onOpen, disabled]);
|
|
308
|
+
}, [isOpen, onOpen, onClose, disabled]);
|
|
309
|
+
|
|
310
|
+
const handleHoverIn = useCallback(() => {
|
|
311
|
+
setHovered(true);
|
|
312
|
+
setSelectDropdownContext(() => ({ triggerHovered: true }));
|
|
313
|
+
}, [setSelectDropdownContext]);
|
|
314
|
+
|
|
315
|
+
const handleHoverOut = useCallback(() => {
|
|
316
|
+
setHovered(false);
|
|
317
|
+
setSelectDropdownContext(() => ({ triggerHovered: false }));
|
|
318
|
+
}, [setSelectDropdownContext]);
|
|
319
|
+
|
|
320
|
+
const outlineElement =
|
|
321
|
+
Select_TriggerOutline.length > 0 ? Select_TriggerOutline : <SelectTriggerOutline />;
|
|
341
322
|
|
|
342
323
|
return (
|
|
343
324
|
<Pressable
|
|
344
325
|
ref={triggerRef}
|
|
345
326
|
onPress={handlePress}
|
|
346
327
|
onLayout={handleLayout}
|
|
328
|
+
onHoverIn={handleHoverIn}
|
|
329
|
+
onHoverOut={handleHoverOut}
|
|
347
330
|
style={[triggerStyles.trigger, style]}
|
|
348
331
|
accessibilityRole="combobox"
|
|
349
332
|
accessibilityState={{ expanded: isOpen, disabled: !!disabled }}
|
|
350
333
|
disabled={disabled}
|
|
351
334
|
{...rest}>
|
|
352
|
-
{
|
|
335
|
+
{restChildren}
|
|
353
336
|
<Icon
|
|
354
337
|
name={isOpen ? 'chevron-up' : 'chevron-down'}
|
|
355
338
|
size={20}
|
|
356
339
|
style={triggerStyles.triggerIcon}
|
|
357
340
|
/>
|
|
358
|
-
|
|
341
|
+
{outlineElement}
|
|
359
342
|
</Pressable>
|
|
360
343
|
);
|
|
361
|
-
};
|
|
344
|
+
});
|
|
362
345
|
|
|
363
346
|
SelectTrigger.displayName = 'Select_Trigger';
|
|
364
347
|
|
|
365
|
-
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
348
|
+
export const SelectTriggerOutline = memo(({ style }: SelectTriggerOutlineProps) => {
|
|
349
|
+
const { isOpen, triggerHovered } = useSelectDropdownContextValue(state => ({
|
|
350
|
+
isOpen: state.isOpen,
|
|
351
|
+
triggerHovered: state.triggerHovered,
|
|
352
|
+
}));
|
|
353
|
+
const { disabled, error } = useSelectContextValue(state => ({
|
|
354
|
+
disabled: state.disabled,
|
|
355
|
+
error: state.error,
|
|
373
356
|
}));
|
|
357
|
+
selectOutlineStyles.useVariants({
|
|
358
|
+
state: getSelectTriggerState({
|
|
359
|
+
isOpen,
|
|
360
|
+
hovered: !!triggerHovered,
|
|
361
|
+
disabled: !!disabled,
|
|
362
|
+
error: !!error,
|
|
363
|
+
}),
|
|
364
|
+
});
|
|
374
365
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
if (item === null || item === undefined) return null;
|
|
378
|
-
const id = typeof item === 'object' ? item.id : item;
|
|
379
|
-
const found = options.find(o => o.id === id);
|
|
380
|
-
return found || item;
|
|
381
|
-
};
|
|
366
|
+
return <View pointerEvents="none" style={[selectOutlineStyles.outline, style]} />;
|
|
367
|
+
});
|
|
382
368
|
|
|
383
|
-
|
|
384
|
-
return (Array.isArray(value) ? value : []).map(resolve).filter(Boolean);
|
|
385
|
-
}
|
|
386
|
-
return resolve(value);
|
|
387
|
-
}, [value, multiple, options]);
|
|
369
|
+
SelectTriggerOutline.displayName = 'Select_TriggerOutline';
|
|
388
370
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
371
|
+
export const SelectValue = memo(
|
|
372
|
+
({ placeholder, labelKey, renderValue, style, ...rest }: SelectValueProps) => {
|
|
373
|
+
const { value, multiple, onRemove } = useSelectContextValue(state => ({
|
|
374
|
+
value: state.value,
|
|
375
|
+
multiple: state.multiple,
|
|
376
|
+
onRemove: state.onRemove,
|
|
377
|
+
}));
|
|
378
|
+
const { optionById } = useSelectSearchContextValue(state => ({
|
|
379
|
+
optionById: state.optionById,
|
|
380
|
+
}));
|
|
392
381
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
382
|
+
const resolvedValue = useMemo(() => {
|
|
383
|
+
const resolve = (id: unknown) => {
|
|
384
|
+
if (id === null || id === undefined) return null;
|
|
385
|
+
const found = optionById.get(id as string | number);
|
|
386
|
+
return found || { id: id as string | number };
|
|
387
|
+
};
|
|
396
388
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
return
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
|
|
389
|
+
if (multiple) {
|
|
390
|
+
return (Array.isArray(value) ? value : []).map(resolve).filter(Boolean);
|
|
391
|
+
}
|
|
392
|
+
return resolve(value);
|
|
393
|
+
}, [optionById, value, multiple]);
|
|
394
|
+
|
|
395
|
+
const displayValue = useMemo(() => {
|
|
396
|
+
if (!resolvedValue) return placeholder || '';
|
|
397
|
+
if (multiple && (resolvedValue as any[]).length === 0) return placeholder || '';
|
|
398
|
+
|
|
399
|
+
if (renderValue) {
|
|
400
|
+
return renderValue(resolvedValue as DefaultItemT | DefaultItemT[] | null);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (multiple) {
|
|
404
|
+
const values = resolvedValue as DefaultItemT[];
|
|
405
|
+
// For multi-select, show chips
|
|
406
|
+
return values.map(item => getDisplayLabel(item, labelKey)).join(', ');
|
|
407
|
+
} else {
|
|
408
|
+
const singleValue = resolvedValue as DefaultItemT;
|
|
409
|
+
return getDisplayLabel(singleValue, labelKey);
|
|
410
|
+
}
|
|
411
|
+
}, [resolvedValue, multiple, labelKey, placeholder, renderValue]);
|
|
412
|
+
|
|
413
|
+
if (multiple && Array.isArray(resolvedValue) && resolvedValue.length > 0) {
|
|
414
|
+
// Render chips for multi-select
|
|
415
|
+
return (
|
|
416
|
+
<View style={[styles.chipContainer, style]} {...rest}>
|
|
417
|
+
{(resolvedValue as DefaultItemT[]).map(item => (
|
|
418
|
+
<SelectValueItem
|
|
419
|
+
key={item.id || String(item)}
|
|
420
|
+
item={item}
|
|
421
|
+
onRemoveItem={onRemove}
|
|
422
|
+
/>
|
|
423
|
+
))}
|
|
424
|
+
</View>
|
|
425
|
+
);
|
|
404
426
|
}
|
|
405
|
-
}, [resolvedValue, multiple, labelKey, placeholder, renderValue]);
|
|
406
427
|
|
|
407
|
-
if (multiple && Array.isArray(resolvedValue) && resolvedValue.length > 0) {
|
|
408
|
-
// Render chips for multi-select
|
|
409
428
|
return (
|
|
410
|
-
<
|
|
411
|
-
{
|
|
412
|
-
|
|
413
|
-
key={item.id || String(item)}
|
|
414
|
-
label={item[labelKey || 'label'] || String(item.id || item)}
|
|
415
|
-
size="sm"
|
|
416
|
-
selected
|
|
417
|
-
left={<></>}
|
|
418
|
-
onClose={() => onRemove(item)}
|
|
419
|
-
/>
|
|
420
|
-
))}
|
|
421
|
-
</View>
|
|
429
|
+
<Text style={[styles.valueText, style]} selectable={false} {...rest}>
|
|
430
|
+
{displayValue}
|
|
431
|
+
</Text>
|
|
422
432
|
);
|
|
423
|
-
}
|
|
433
|
+
},
|
|
434
|
+
);
|
|
424
435
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
436
|
+
const SelectValueItem = typedMemo(
|
|
437
|
+
({
|
|
438
|
+
item,
|
|
439
|
+
onRemoveItem,
|
|
440
|
+
}: {
|
|
441
|
+
item: DefaultItemT;
|
|
442
|
+
onRemoveItem: (item: DefaultItemT) => void;
|
|
443
|
+
}) => {
|
|
444
|
+
const onRemove = useCallback(() => {
|
|
445
|
+
onRemoveItem(item);
|
|
446
|
+
}, [item, onRemoveItem]);
|
|
447
|
+
|
|
448
|
+
return (
|
|
449
|
+
<Chip.Input
|
|
450
|
+
label={getDisplayLabel(item)}
|
|
451
|
+
size="sm"
|
|
452
|
+
selected
|
|
453
|
+
left={<></>}
|
|
454
|
+
onClose={onRemove}
|
|
455
|
+
/>
|
|
456
|
+
);
|
|
457
|
+
},
|
|
458
|
+
);
|
|
431
459
|
|
|
432
460
|
SelectValue.displayName = 'Select_Value';
|
|
433
461
|
|
|
434
462
|
// Select.Dropdown - popover with keyboard navigation
|
|
435
|
-
const SelectDropdown = (
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
463
|
+
export const SelectDropdown = memo(
|
|
464
|
+
({
|
|
465
|
+
children,
|
|
466
|
+
WrapperComponent,
|
|
467
|
+
wrapperComponentProps,
|
|
468
|
+
enableKeyboardNavigation = true,
|
|
469
|
+
style: popoverStyleProp,
|
|
470
|
+
...popoverProps
|
|
471
|
+
}: SelectDropdownProps & { enableKeyboardNavigation?: boolean }) => {
|
|
472
|
+
const { isOpen, onClose, triggerRef, triggerLayout } = useSelectDropdownContextValue(
|
|
473
|
+
state => ({
|
|
474
|
+
isOpen: state.isOpen,
|
|
475
|
+
onClose: state.onClose,
|
|
476
|
+
triggerRef: state.triggerRef,
|
|
477
|
+
triggerLayout: state.triggerLayout,
|
|
478
|
+
}),
|
|
479
|
+
);
|
|
449
480
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
481
|
+
const popoverStyle = useMemo(() => {
|
|
482
|
+
const baseStyle = popoverStyleProp ? [popoverStyleProp] : [];
|
|
483
|
+
if (triggerLayout) {
|
|
484
|
+
return [{ width: triggerLayout.width }, ...baseStyle];
|
|
485
|
+
}
|
|
486
|
+
return baseStyle;
|
|
487
|
+
}, [triggerLayout, popoverStyleProp]);
|
|
488
|
+
|
|
489
|
+
if (!triggerLayout) return null;
|
|
457
490
|
|
|
458
|
-
|
|
491
|
+
if (WrapperComponent) {
|
|
492
|
+
return (
|
|
493
|
+
<WrapperComponent isOpen={isOpen} onClose={onClose} {...wrapperComponentProps}>
|
|
494
|
+
{enableKeyboardNavigation && Platform.OS === 'web' ? (
|
|
495
|
+
<KeyboardNavigationWrapper>{children}</KeyboardNavigationWrapper>
|
|
496
|
+
) : (
|
|
497
|
+
children
|
|
498
|
+
)}
|
|
499
|
+
</WrapperComponent>
|
|
500
|
+
);
|
|
501
|
+
}
|
|
459
502
|
|
|
460
|
-
if (WrapperComponent) {
|
|
461
503
|
return (
|
|
462
|
-
<
|
|
504
|
+
<Popover
|
|
505
|
+
triggerRef={triggerRef as React.RefObject<View>}
|
|
506
|
+
isOpen={isOpen}
|
|
507
|
+
onClose={onClose}
|
|
508
|
+
style={popoverStyle}
|
|
509
|
+
triggerDimensions={triggerLayout}
|
|
510
|
+
{...popoverProps}>
|
|
463
511
|
{enableKeyboardNavigation && Platform.OS === 'web' ? (
|
|
464
512
|
<KeyboardNavigationWrapper>{children}</KeyboardNavigationWrapper>
|
|
465
513
|
) : (
|
|
466
514
|
children
|
|
467
515
|
)}
|
|
468
|
-
</
|
|
516
|
+
</Popover>
|
|
469
517
|
);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
return (
|
|
473
|
-
<Popover
|
|
474
|
-
triggerRef={triggerRef as React.RefObject<View>}
|
|
475
|
-
isOpen={isOpen}
|
|
476
|
-
onClose={onClose}
|
|
477
|
-
style={popoverStyle}
|
|
478
|
-
triggerDimensions={triggerLayout}
|
|
479
|
-
{...popoverProps}>
|
|
480
|
-
{enableKeyboardNavigation && Platform.OS === 'web' ? (
|
|
481
|
-
<KeyboardNavigationWrapper>{children}</KeyboardNavigationWrapper>
|
|
482
|
-
) : (
|
|
483
|
-
children
|
|
484
|
-
)}
|
|
485
|
-
</Popover>
|
|
486
|
-
);
|
|
487
|
-
};
|
|
518
|
+
},
|
|
519
|
+
);
|
|
488
520
|
|
|
489
|
-
// Keyboard navigation wrapper for web
|
|
490
|
-
|
|
491
|
-
|
|
521
|
+
// Keyboard navigation wrapper for web. Captures its own DOM ref via a `display: contents`
|
|
522
|
+
// wrapper so the keyboard navigator can query options without needing the dropdown content
|
|
523
|
+
// itself to plumb a contentRef.
|
|
524
|
+
const KeyboardNavigationWrapper = memo(({ children }: { children: React.ReactNode }) => {
|
|
525
|
+
const { onClose, isOpen } = useSelectDropdownContextValue(state => ({
|
|
492
526
|
onClose: state.onClose,
|
|
493
|
-
contentRef: state.contentRef,
|
|
494
527
|
isOpen: state.isOpen,
|
|
495
528
|
}));
|
|
529
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
496
530
|
|
|
497
531
|
const handleKeyDown = useCallback(
|
|
498
532
|
(e: globalThis.KeyboardEvent) => {
|
|
499
|
-
if (!
|
|
500
|
-
|
|
501
|
-
// Find all focusable options
|
|
502
|
-
// We assume options have role="option" and are descendants of the contentRef
|
|
503
|
-
// On React Native Web, refs often point to the host node (div)
|
|
504
|
-
const container = contentRef.current as HTMLElement;
|
|
505
|
-
if (!container || !container.querySelectorAll) return;
|
|
506
|
-
|
|
507
|
-
const options = Array.from(
|
|
508
|
-
container.querySelectorAll('[role="option"]:not([disabled])'),
|
|
509
|
-
) as HTMLElement[];
|
|
533
|
+
if (!containerRef.current) return;
|
|
510
534
|
|
|
535
|
+
const options = collectWebSelectKeyboardOptionElements(containerRef.current);
|
|
511
536
|
if (options.length === 0) return;
|
|
512
537
|
|
|
513
538
|
const currentIndex = options.findIndex(el => el === document.activeElement);
|
|
@@ -533,202 +558,108 @@ const KeyboardNavigationWrapper = ({ children }: { children: React.ReactNode })
|
|
|
533
558
|
break;
|
|
534
559
|
case 'Enter':
|
|
535
560
|
e.preventDefault();
|
|
561
|
+
e.stopImmediatePropagation();
|
|
536
562
|
if (currentIndex !== -1) {
|
|
537
|
-
|
|
563
|
+
// Store reference to the focused element before triggering click
|
|
564
|
+
// to prevent issues with DOM updates during the click handler
|
|
565
|
+
const focusedOption = options[currentIndex];
|
|
566
|
+
if (focusedOption) {
|
|
567
|
+
focusedOption.click();
|
|
568
|
+
}
|
|
538
569
|
}
|
|
539
570
|
break;
|
|
540
571
|
case 'Escape':
|
|
541
572
|
e.preventDefault();
|
|
542
573
|
onClose();
|
|
543
|
-
// Return focus to trigger? This should be handled by the caller/Popover usually.
|
|
544
574
|
break;
|
|
545
575
|
}
|
|
546
576
|
},
|
|
547
|
-
[
|
|
577
|
+
[onClose],
|
|
548
578
|
);
|
|
549
579
|
|
|
550
580
|
useEffect(() => {
|
|
551
|
-
if (Platform.OS
|
|
552
|
-
const controller = new AbortController();
|
|
553
|
-
// We attach listener to the window or the container?
|
|
554
|
-
// If we attach to container, it needs focus to receive keys.
|
|
555
|
-
// Popovers usually trap focus.
|
|
556
|
-
// Let's attach to window to be safe, but only when open (which this component implies).
|
|
557
|
-
// Actually, best practice is to attach to the container if it captures focus.
|
|
558
|
-
// But SelectDropdown usually renders in a Portal.
|
|
559
|
-
// Let's attach to window but check if the event target is inside our content.
|
|
560
|
-
// Or rely on the fact that if an option is focused, the keydown bubbles up.
|
|
561
|
-
// If nothing is focused, where do keys go? Body.
|
|
562
|
-
const listener = (e: KeyboardEvent) => {
|
|
563
|
-
// Only handle navigation keys when dropdown is open
|
|
564
|
-
if (!isOpen) return;
|
|
565
|
-
|
|
566
|
-
// For arrow keys, Enter, and Escape, allow navigation regardless of focus location
|
|
567
|
-
// This ensures keyboard navigation works even when focus is still on the trigger
|
|
568
|
-
const isNavigationKey = ['ArrowDown', 'ArrowUp', 'Enter', 'Escape'].includes(e.key);
|
|
569
|
-
|
|
570
|
-
if (isNavigationKey) {
|
|
571
|
-
handleKeyDown(e);
|
|
572
|
-
return;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
// For other keys, only handle if focus is within the dropdown
|
|
576
|
-
const contentEl = contentRef?.current as HTMLElement | null;
|
|
577
|
-
const dropdownContainer = contentEl?.parentElement ?? contentEl;
|
|
578
|
-
const targetNode = e.target as Node;
|
|
579
|
-
|
|
580
|
-
const isWithinDropdown =
|
|
581
|
-
!!dropdownContainer &&
|
|
582
|
-
(dropdownContainer === targetNode || dropdownContainer.contains(targetNode));
|
|
583
|
-
|
|
584
|
-
if (isWithinDropdown || e.target === document.body) {
|
|
585
|
-
handleKeyDown(e);
|
|
586
|
-
}
|
|
587
|
-
};
|
|
588
|
-
|
|
589
|
-
window.addEventListener('keydown', listener, {
|
|
590
|
-
capture: true,
|
|
591
|
-
signal: controller.signal,
|
|
592
|
-
});
|
|
593
|
-
|
|
594
|
-
return () => {
|
|
595
|
-
controller.abort();
|
|
596
|
-
};
|
|
597
|
-
}
|
|
598
|
-
return undefined;
|
|
599
|
-
}, [handleKeyDown, contentRef, isOpen]);
|
|
581
|
+
if (Platform.OS !== 'web') return undefined;
|
|
600
582
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
SelectDropdown.displayName = 'Select_Dropdown';
|
|
583
|
+
const controller = new AbortController();
|
|
584
|
+
const listener = (e: KeyboardEvent) => {
|
|
585
|
+
if (!isOpen) return;
|
|
605
586
|
|
|
606
|
-
//
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
}: SelectContentProps) => {
|
|
614
|
-
const { contentRef } = useSelectDropdownContextValue(state => ({
|
|
615
|
-
contentRef: state.contentRef,
|
|
616
|
-
}));
|
|
617
|
-
|
|
618
|
-
const { filteredOptions, value, multiple, searchQuery, options } = useSelectContextValue(
|
|
619
|
-
state => ({
|
|
620
|
-
filteredOptions: state.filteredOptions,
|
|
621
|
-
value: state.value,
|
|
622
|
-
multiple: state.multiple,
|
|
623
|
-
searchQuery: state.searchQuery,
|
|
624
|
-
options: state.options,
|
|
625
|
-
}),
|
|
626
|
-
);
|
|
587
|
+
// Navigation keys are handled regardless of focus location so keyboard nav works
|
|
588
|
+
// even while focus is still on the trigger.
|
|
589
|
+
const isNavigationKey = ['ArrowDown', 'ArrowUp', 'Enter', 'Escape'].includes(e.key);
|
|
590
|
+
if (isNavigationKey) {
|
|
591
|
+
handleKeyDown(e);
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
627
594
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
595
|
+
// Other keys: only handle if focus is inside the dropdown.
|
|
596
|
+
const container = containerRef.current;
|
|
597
|
+
const targetNode = e.target as Node;
|
|
598
|
+
const isWithinDropdown =
|
|
599
|
+
!!container && (container === targetNode || container.contains(targetNode));
|
|
600
|
+
if (isWithinDropdown || e.target === document.body) {
|
|
601
|
+
handleKeyDown(e);
|
|
602
|
+
}
|
|
603
|
+
};
|
|
633
604
|
|
|
634
|
-
|
|
605
|
+
window.addEventListener('keydown', listener, {
|
|
606
|
+
capture: true,
|
|
607
|
+
signal: controller.signal,
|
|
635
608
|
});
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
const defaultEmptyState = useMemo(() => {
|
|
639
|
-
const hasSearchQuery = searchQuery && searchQuery.trim().length > 0;
|
|
640
|
-
const hasNoOptions = options.length === 0;
|
|
641
|
-
|
|
642
|
-
if (hasNoOptions) {
|
|
643
|
-
return (
|
|
644
|
-
<View style={styles.emptyState}>
|
|
645
|
-
<Text style={styles.emptyStateText}>No options available</Text>
|
|
646
|
-
</View>
|
|
647
|
-
);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
if (hasSearchQuery) {
|
|
651
|
-
return (
|
|
652
|
-
<View style={styles.emptyState}>
|
|
653
|
-
<Text style={styles.emptyStateText}>No results found</Text>
|
|
654
|
-
</View>
|
|
655
|
-
);
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
return (
|
|
659
|
-
<View style={styles.emptyState}>
|
|
660
|
-
<Text style={styles.emptyStateText}>No options</Text>
|
|
661
|
-
</View>
|
|
662
|
-
);
|
|
663
|
-
}, [searchQuery, options.length]);
|
|
609
|
+
return () => controller.abort();
|
|
610
|
+
}, [handleKeyDown, isOpen]);
|
|
664
611
|
|
|
665
612
|
return (
|
|
666
|
-
<
|
|
667
|
-
{filteredOptions.length === 0 ? emptyState ?? defaultEmptyState : content}
|
|
668
|
-
</ContainerComponent>
|
|
669
|
-
);
|
|
670
|
-
};
|
|
671
|
-
|
|
672
|
-
SelectContent.displayName = 'Select_Content';
|
|
673
|
-
|
|
674
|
-
// Select.Group - groups items with label
|
|
675
|
-
const SelectGroup = memo(({ children, label, style, ...rest }: SelectGroupProps) => {
|
|
676
|
-
return (
|
|
677
|
-
<View style={style} {...rest}>
|
|
678
|
-
{label && <Text style={styles.groupLabel}>{label}</Text>}
|
|
613
|
+
<div ref={containerRef} style={{ display: 'contents' }}>
|
|
679
614
|
{children}
|
|
680
|
-
</
|
|
615
|
+
</div>
|
|
681
616
|
);
|
|
682
617
|
});
|
|
683
618
|
|
|
684
|
-
|
|
619
|
+
SelectDropdown.displayName = 'Select_Dropdown';
|
|
685
620
|
|
|
686
621
|
// Select.Item - select item that uses context
|
|
687
|
-
const SelectOption = memo(
|
|
622
|
+
export const SelectOption = memo(
|
|
688
623
|
<Option extends DefaultItemT = DefaultItemT>({
|
|
689
624
|
value,
|
|
690
625
|
children,
|
|
691
|
-
renderItem,
|
|
692
626
|
onPress,
|
|
693
627
|
style,
|
|
694
628
|
disabled: optionDisabledProp = false,
|
|
695
629
|
...rest
|
|
696
630
|
}: SelectOptionProps<Option>) => {
|
|
697
631
|
const {
|
|
698
|
-
value: selectionValue,
|
|
699
632
|
multiple,
|
|
700
633
|
onAdd,
|
|
701
634
|
onRemove,
|
|
702
635
|
disabled: selectDisabled,
|
|
703
|
-
|
|
704
|
-
|
|
636
|
+
isSelectedId,
|
|
637
|
+
} = useSelectContextValue(state => ({
|
|
705
638
|
multiple: state.multiple,
|
|
706
639
|
onAdd: state.onAdd,
|
|
707
640
|
onRemove: state.onRemove,
|
|
708
641
|
disabled: state.disabled,
|
|
642
|
+
isSelectedId: state.isSelectedId,
|
|
709
643
|
}));
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
644
|
+
const { allOptions, getOptionId } = useSelectSearchContextValue(state => ({
|
|
645
|
+
allOptions: state.allOptions,
|
|
646
|
+
getOptionId: state.getOptionId,
|
|
713
647
|
}));
|
|
714
648
|
|
|
715
649
|
const option = useMemo(() => {
|
|
650
|
+
const found = allOptions.find(i => getOptionId(i as Option) === value);
|
|
651
|
+
if (found) return found as Option;
|
|
716
652
|
return {
|
|
717
653
|
id: value,
|
|
718
|
-
...(typeof children === 'string' ? { label: children } : {}),
|
|
719
654
|
...(optionDisabledProp ? { selectable: false } : {}),
|
|
720
655
|
} as Option;
|
|
721
|
-
}, [
|
|
656
|
+
}, [allOptions, getOptionId, optionDisabledProp, value]);
|
|
722
657
|
|
|
723
|
-
const isSelected =
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
const singleValue = selectionValue as any;
|
|
729
|
-
return (singleValue?.id ?? singleValue) === option.id || false;
|
|
730
|
-
}
|
|
731
|
-
}, [selectionValue, multiple, option.id]);
|
|
658
|
+
const isSelected = isSelectedId(value);
|
|
659
|
+
|
|
660
|
+
const { onClose } = useSelectDropdownContextValue(state => ({
|
|
661
|
+
onClose: state.onClose,
|
|
662
|
+
}));
|
|
732
663
|
|
|
733
664
|
const isOptionDisabled = Boolean(
|
|
734
665
|
selectDisabled || optionDisabledProp || option.selectable === false,
|
|
@@ -737,10 +668,7 @@ const SelectOption = memo(
|
|
|
737
668
|
const handlePress = useCallback(
|
|
738
669
|
(event: GestureResponderEvent) => {
|
|
739
670
|
if (isOptionDisabled) return;
|
|
740
|
-
|
|
741
|
-
if (onPress) {
|
|
742
|
-
onPress(option, event);
|
|
743
|
-
}
|
|
671
|
+
onPress?.(option, event);
|
|
744
672
|
|
|
745
673
|
if (isSelected) {
|
|
746
674
|
onRemove(option);
|
|
@@ -756,283 +684,91 @@ const SelectOption = memo(
|
|
|
756
684
|
[isOptionDisabled, option, isSelected, onPress, onAdd, onRemove, multiple, onClose],
|
|
757
685
|
);
|
|
758
686
|
|
|
759
|
-
const content = useMemo(() => {
|
|
760
|
-
if (typeof children === 'string') {
|
|
761
|
-
return <Text style={isOptionDisabled && styles.itemDisabledText}>{children}</Text>;
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
if (children) return children;
|
|
765
|
-
|
|
766
|
-
return (
|
|
767
|
-
<Text style={isOptionDisabled && styles.itemDisabledText}>
|
|
768
|
-
{option.label || String(option.id)}
|
|
769
|
-
</Text>
|
|
770
|
-
);
|
|
771
|
-
}, [children, option.id, option.label, isOptionDisabled]);
|
|
772
|
-
|
|
773
|
-
const accessibilityProps = {
|
|
774
|
-
accessibilityRole: 'button' as AccessibilityRole, // Fallback for native
|
|
775
|
-
accessibilityState: { selected: isSelected, disabled: isOptionDisabled },
|
|
776
|
-
...Platform.select({
|
|
777
|
-
web: {
|
|
778
|
-
accessibilityRole: 'option' as AccessibilityRole,
|
|
779
|
-
tabIndex: -1 as 0 | -1 | undefined,
|
|
780
|
-
// Use a dataset attribute to help the keyboard navigator find this
|
|
781
|
-
'data-option-id': String(option.id),
|
|
782
|
-
},
|
|
783
|
-
}),
|
|
784
|
-
};
|
|
785
|
-
|
|
786
|
-
if (renderItem) {
|
|
787
|
-
return (
|
|
788
|
-
<Pressable
|
|
789
|
-
onPress={handlePress}
|
|
790
|
-
disabled={isOptionDisabled}
|
|
791
|
-
style={[isOptionDisabled && styles.itemDisabled, style]}
|
|
792
|
-
{...accessibilityProps}
|
|
793
|
-
{...rest}>
|
|
794
|
-
{renderItem(option, isSelected)}
|
|
795
|
-
</Pressable>
|
|
796
|
-
);
|
|
797
|
-
}
|
|
798
|
-
|
|
799
687
|
return (
|
|
800
|
-
<
|
|
688
|
+
<List.Item
|
|
689
|
+
{...rest}
|
|
690
|
+
style={style}
|
|
691
|
+
value={value}
|
|
692
|
+
shouldToggleOnPress={false}
|
|
801
693
|
onPress={handlePress}
|
|
802
694
|
disabled={isOptionDisabled}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
695
|
+
accessibilityState={{ selected: isSelected, disabled: isOptionDisabled }}
|
|
696
|
+
{...(Platform.OS === 'web'
|
|
697
|
+
? {
|
|
698
|
+
// Force role="option" on web — the keyboard navigator finds rows by
|
|
699
|
+
// [role="option"], so callers must not override these.
|
|
700
|
+
accessibilityRole: 'option' as AccessibilityRole,
|
|
701
|
+
role: 'option',
|
|
702
|
+
tabIndex: -1 as 0 | -1 | undefined,
|
|
703
|
+
'data-molecules-select-option': '',
|
|
704
|
+
'data-option-id': String(option.id),
|
|
705
|
+
onKeyDown: (e: React.KeyboardEvent) => {
|
|
706
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
707
|
+
e.preventDefault();
|
|
708
|
+
e.stopPropagation();
|
|
709
|
+
}
|
|
710
|
+
},
|
|
711
|
+
}
|
|
712
|
+
: { accessibilityRole: 'button' as AccessibilityRole })}>
|
|
713
|
+
{children}
|
|
714
|
+
</List.Item>
|
|
813
715
|
);
|
|
814
716
|
},
|
|
815
717
|
);
|
|
816
718
|
|
|
817
719
|
SelectOption.displayName = 'Select_Option';
|
|
818
720
|
|
|
819
|
-
|
|
820
|
-
const
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
setSearchQuery: state.setSearchQuery,
|
|
825
|
-
}));
|
|
826
|
-
const textInputRef = useRef<TextInputHandles>(null);
|
|
827
|
-
|
|
828
|
-
const handleChangeText = useCallback(
|
|
829
|
-
(text: string) => {
|
|
830
|
-
setSearchQuery(text);
|
|
831
|
-
onQueryChange?.(text);
|
|
832
|
-
textInputProps.onChangeText?.(text);
|
|
833
|
-
},
|
|
834
|
-
[onQueryChange, setSearchQuery, textInputProps],
|
|
835
|
-
);
|
|
836
|
-
|
|
837
|
-
const inputProps = {
|
|
838
|
-
...textInputProps,
|
|
839
|
-
value: textInputProps.value !== undefined ? textInputProps.value : searchQuery,
|
|
840
|
-
onChangeText: handleChangeText,
|
|
841
|
-
placeholder: textInputProps.placeholder || 'Search...',
|
|
842
|
-
inputStyle: styles.searchInputInput,
|
|
843
|
-
} as TextInputProps;
|
|
844
|
-
|
|
845
|
-
useEffect(() => {
|
|
846
|
-
if (Platform.OS !== 'web') return;
|
|
847
|
-
if (!autoFocus || !textInputRef.current) {
|
|
848
|
-
return;
|
|
849
|
-
}
|
|
721
|
+
export const SelectSearchInput = memo(({ children, ...textInputProps }: SelectSearchInputProps) => {
|
|
722
|
+
const { searchQuery, setSearchQuery } = useSelectSearchContextValue(state => ({
|
|
723
|
+
searchQuery: state.searchQuery,
|
|
724
|
+
setSearchQuery: state.setSearchQuery,
|
|
725
|
+
}));
|
|
850
726
|
|
|
851
|
-
|
|
852
|
-
focus?: (options?: { preventScroll?: boolean }) => void;
|
|
853
|
-
};
|
|
727
|
+
const textInputRef = useRef<TextInputHandles>(null);
|
|
854
728
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
window.scrollTo(scrollX, scrollY);
|
|
862
|
-
}
|
|
863
|
-
};
|
|
729
|
+
const handleChangeText = useCallback(
|
|
730
|
+
(text: string) => {
|
|
731
|
+
setSearchQuery(text);
|
|
732
|
+
},
|
|
733
|
+
[setSearchQuery],
|
|
734
|
+
);
|
|
864
735
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
736
|
+
const inputProps = {
|
|
737
|
+
...textInputProps,
|
|
738
|
+
value: searchQuery,
|
|
739
|
+
onChangeText: handleChangeText,
|
|
740
|
+
placeholder: textInputProps.placeholder || 'Search...',
|
|
741
|
+
inputStyle: styles.searchInputInput,
|
|
742
|
+
} as TextInputProps;
|
|
868
743
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
autoFocus={Platform.OS !== 'web' && autoFocus}
|
|
873
|
-
style={styles.searchInput}
|
|
874
|
-
left={
|
|
875
|
-
<Icon onPress={() => textInputRef.current?.focus()} name="magnify" size={20} />
|
|
876
|
-
}
|
|
877
|
-
right={
|
|
878
|
-
searchQuery ? (
|
|
879
|
-
<IconButton name="close" size={20} onPress={() => setSearchQuery('')} />
|
|
880
|
-
) : undefined
|
|
881
|
-
}
|
|
882
|
-
size="sm"
|
|
883
|
-
variant="outlined"
|
|
884
|
-
{...inputProps}
|
|
885
|
-
/>
|
|
886
|
-
);
|
|
887
|
-
},
|
|
888
|
-
);
|
|
744
|
+
const onPressLeftIcon = useCallback(() => {
|
|
745
|
+
textInputRef.current?.focus();
|
|
746
|
+
}, []);
|
|
889
747
|
|
|
890
|
-
|
|
748
|
+
const onClearSearchQuery = useCallback(() => {
|
|
749
|
+
handleChangeText('');
|
|
750
|
+
}, [handleChangeText]);
|
|
891
751
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
752
|
+
return (
|
|
753
|
+
<TextInput
|
|
754
|
+
ref={textInputRef}
|
|
755
|
+
style={styles.searchInput}
|
|
756
|
+
size="sm"
|
|
757
|
+
variant="outlined"
|
|
758
|
+
{...inputProps}>
|
|
759
|
+
<TextInput.Left>
|
|
760
|
+
<Icon onPress={onPressLeftIcon} name="magnify" size={20} />
|
|
761
|
+
</TextInput.Left>
|
|
762
|
+
{searchQuery ? (
|
|
763
|
+
<TextInput.Right>
|
|
764
|
+
<IconButton name="close" size={20} onPress={onClearSearchQuery} />
|
|
765
|
+
</TextInput.Right>
|
|
766
|
+
) : null}
|
|
767
|
+
{children}
|
|
768
|
+
</TextInput>
|
|
769
|
+
);
|
|
901
770
|
});
|
|
902
771
|
|
|
903
|
-
|
|
904
|
-
trigger: {
|
|
905
|
-
borderRadius: theme.shapes.corner.extraSmall,
|
|
906
|
-
paddingHorizontal: theme.spacings['3'],
|
|
907
|
-
paddingVertical: theme.spacings['2'],
|
|
908
|
-
minHeight: 56,
|
|
909
|
-
flexDirection: 'row',
|
|
910
|
-
alignItems: 'center',
|
|
911
|
-
justifyContent: 'space-between',
|
|
912
|
-
width: '100%',
|
|
913
|
-
variants: {
|
|
914
|
-
state: {
|
|
915
|
-
disabled: {
|
|
916
|
-
opacity: 0.38,
|
|
917
|
-
backgroundColor: theme.colors.surfaceVariant,
|
|
918
|
-
},
|
|
919
|
-
errorDisabled: {
|
|
920
|
-
opacity: 0.38,
|
|
921
|
-
},
|
|
922
|
-
},
|
|
923
|
-
},
|
|
924
|
-
},
|
|
925
|
-
outline: {
|
|
926
|
-
position: 'absolute',
|
|
927
|
-
top: 0,
|
|
928
|
-
left: 0,
|
|
929
|
-
right: 0,
|
|
930
|
-
bottom: 0,
|
|
931
|
-
borderRadius: theme.shapes.corner.extraSmall,
|
|
932
|
-
borderWidth: 1,
|
|
933
|
-
borderColor: theme.colors.outline,
|
|
934
|
-
pointerEvents: 'none',
|
|
935
|
-
variants: {
|
|
936
|
-
state: {
|
|
937
|
-
focused: {
|
|
938
|
-
borderWidth: 2,
|
|
939
|
-
borderColor: theme.colors.primary,
|
|
940
|
-
},
|
|
941
|
-
hovered: {
|
|
942
|
-
borderColor: theme.colors.onSurface,
|
|
943
|
-
},
|
|
944
|
-
hoveredAndFocused: {
|
|
945
|
-
borderWidth: 2,
|
|
946
|
-
borderColor: theme.colors.primary,
|
|
947
|
-
},
|
|
948
|
-
disabled: {
|
|
949
|
-
borderColor: theme.colors.onSurface,
|
|
950
|
-
},
|
|
951
|
-
error: {
|
|
952
|
-
borderColor: theme.colors.error,
|
|
953
|
-
},
|
|
954
|
-
errorFocused: {
|
|
955
|
-
borderWidth: 2,
|
|
956
|
-
borderColor: theme.colors.error,
|
|
957
|
-
},
|
|
958
|
-
errorHovered: {
|
|
959
|
-
borderColor: theme.colors.onErrorContainer,
|
|
960
|
-
},
|
|
961
|
-
errorFocusedAndHovered: {
|
|
962
|
-
borderWidth: 2,
|
|
963
|
-
borderColor: theme.colors.error,
|
|
964
|
-
},
|
|
965
|
-
errorDisabled: {
|
|
966
|
-
borderColor: theme.colors.error,
|
|
967
|
-
},
|
|
968
|
-
},
|
|
969
|
-
},
|
|
970
|
-
},
|
|
971
|
-
triggerIcon: {
|
|
972
|
-
marginLeft: theme.spacings['2'],
|
|
973
|
-
color: theme.colors.onSurfaceVariant,
|
|
974
|
-
},
|
|
975
|
-
}));
|
|
976
|
-
|
|
977
|
-
const styles = StyleSheet.create(theme => ({
|
|
978
|
-
chipContainer: {
|
|
979
|
-
flexDirection: 'row',
|
|
980
|
-
flexWrap: 'wrap',
|
|
981
|
-
gap: 6,
|
|
982
|
-
maxWidth: '90%',
|
|
983
|
-
},
|
|
984
|
-
groupLabel: {
|
|
985
|
-
paddingHorizontal: theme.spacings['4'],
|
|
986
|
-
paddingVertical: theme.spacings['2'],
|
|
987
|
-
fontWeight: '600',
|
|
988
|
-
color: theme.colors.onSurface,
|
|
989
|
-
},
|
|
990
|
-
item: {
|
|
991
|
-
paddingHorizontal: theme.spacings['4'],
|
|
992
|
-
paddingVertical: theme.spacings['3'],
|
|
993
|
-
backgroundColor: 'transparent',
|
|
994
|
-
|
|
995
|
-
_web: {
|
|
996
|
-
cursor: 'pointer',
|
|
997
|
-
outlineStyle: 'none',
|
|
998
|
-
_hover: {
|
|
999
|
-
backgroundColor: theme.colors.stateLayer.hover.primary,
|
|
1000
|
-
},
|
|
1001
|
-
_focus: {
|
|
1002
|
-
backgroundColor: theme.colors.stateLayer.hover.primary,
|
|
1003
|
-
},
|
|
1004
|
-
},
|
|
1005
|
-
},
|
|
1006
|
-
itemSelected: {
|
|
1007
|
-
backgroundColor: theme.colors.stateLayer.hover.primary,
|
|
1008
|
-
},
|
|
1009
|
-
itemDisabled: {
|
|
1010
|
-
opacity: 0.38,
|
|
1011
|
-
_web: {
|
|
1012
|
-
cursor: 'not-allowed',
|
|
1013
|
-
},
|
|
1014
|
-
},
|
|
1015
|
-
itemDisabledText: {
|
|
1016
|
-
color: theme.colors.onSurfaceVariant,
|
|
1017
|
-
},
|
|
1018
|
-
searchInput: {
|
|
1019
|
-
marginHorizontal: theme.spacings['2'],
|
|
1020
|
-
marginVertical: theme.spacings['3'],
|
|
1021
|
-
},
|
|
1022
|
-
searchInputInput: {
|
|
1023
|
-
height: 42,
|
|
1024
|
-
},
|
|
1025
|
-
emptyState: {
|
|
1026
|
-
paddingHorizontal: theme.spacings['4'],
|
|
1027
|
-
paddingVertical: theme.spacings['6'],
|
|
1028
|
-
alignItems: 'center',
|
|
1029
|
-
justifyContent: 'center',
|
|
1030
|
-
},
|
|
1031
|
-
emptyStateText: {
|
|
1032
|
-
color: theme.colors.onSurfaceVariant,
|
|
1033
|
-
fontSize: 14,
|
|
1034
|
-
},
|
|
1035
|
-
}));
|
|
772
|
+
SelectSearchInput.displayName = 'Select_SearchInput';
|
|
1036
773
|
|
|
1037
|
-
export default
|
|
1038
|
-
export { SelectDropdownProvider, SelectProvider };
|
|
774
|
+
export default SelectRoot;
|