jp-composter 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +997 -0
- package/dist/index.d.ts +997 -0
- package/dist/index.js +36837 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +36778 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +66 -0
- package/src/SliceUI/IconMoon.tsx +33 -0
- package/src/SliceUI/assets/Anatomy diagram copy.svg +19 -0
- package/src/SliceUI/assets/Anatomy diagram.svg +19 -0
- package/src/SliceUI/assets/Anatomycheck.svg +15 -0
- package/src/SliceUI/assets/Anatomyinput.svg +32 -0
- package/src/SliceUI/assets/Checkbox.jpg +0 -0
- package/src/SliceUI/assets/Diagram copy.svg +15 -0
- package/src/SliceUI/assets/Diagram.jpg +0 -0
- package/src/SliceUI/assets/Diagram.svg +15 -0
- package/src/SliceUI/assets/Frame 5 copy.png +0 -0
- package/src/SliceUI/assets/Frame 5.png +0 -0
- package/src/SliceUI/assets/Frame 65.png +0 -0
- package/src/SliceUI/assets/Frame_65.png +0 -0
- package/src/SliceUI/assets/Icon copy.svg +3 -0
- package/src/SliceUI/assets/Icon.svg +3 -0
- package/src/SliceUI/assets/Icon_Bridging copy.svg +39 -0
- package/src/SliceUI/assets/Icon_Bridging.svg +39 -0
- package/src/SliceUI/assets/Icon_Consistent copy.svg +39 -0
- package/src/SliceUI/assets/Icon_Consistent.svg +39 -0
- package/src/SliceUI/assets/Icon_Plug copy.svg +38 -0
- package/src/SliceUI/assets/Icon_Plug.svg +38 -0
- package/src/SliceUI/assets/Icon_Reusable copy.svg +39 -0
- package/src/SliceUI/assets/Icon_Reusable.svg +39 -0
- package/src/SliceUI/assets/Layer_1.png +0 -0
- package/src/SliceUI/assets/accessibility.png +0 -0
- package/src/SliceUI/assets/accessibility.svg +1 -0
- package/src/SliceUI/assets/addon-library.png +0 -0
- package/src/SliceUI/assets/assets.png +0 -0
- package/src/SliceUI/assets/avif-test-image.avif +0 -0
- package/src/SliceUI/assets/bridging.svg +13 -0
- package/src/SliceUI/assets/consistent.svg +11 -0
- package/src/SliceUI/assets/context.png +0 -0
- package/src/SliceUI/assets/discord.svg +1 -0
- package/src/SliceUI/assets/docs.png +0 -0
- package/src/SliceUI/assets/figma-plugin.png +0 -0
- package/src/SliceUI/assets/github.svg +1 -0
- package/src/SliceUI/assets/resources/Anatomy diagram.svg +19 -0
- package/src/SliceUI/assets/resources/Anatomycheck.svg +15 -0
- package/src/SliceUI/assets/resources/Anatomyinput.svg +32 -0
- package/src/SliceUI/assets/resources/Diagram.svg +15 -0
- package/src/SliceUI/assets/resources/Frame 5.png +0 -0
- package/src/SliceUI/assets/resources/Frame 65.png +0 -0
- package/src/SliceUI/assets/resources/Icon.svg +3 -0
- package/src/SliceUI/assets/resources/Icon_Bridging.svg +39 -0
- package/src/SliceUI/assets/resources/Icon_Consistent.svg +39 -0
- package/src/SliceUI/assets/resources/Icon_Plug.svg +38 -0
- package/src/SliceUI/assets/resources/Icon_Reusable.svg +39 -0
- package/src/SliceUI/assets/resources/fonts/FontIcon.json +150 -0
- package/src/SliceUI/assets/resources/fonts/Lato-Black.ttf +0 -0
- package/src/SliceUI/assets/resources/fonts/Lato-Bold.ttf +0 -0
- package/src/SliceUI/assets/resources/fonts/Lato-Heavy.ttf +0 -0
- package/src/SliceUI/assets/resources/fonts/Lato-Medium.ttf +0 -0
- package/src/SliceUI/assets/resources/fonts/Lato-Regular.ttf +0 -0
- package/src/SliceUI/assets/resources/fonts/Lato.woff2 +0 -0
- package/src/SliceUI/assets/resources/fonts/icomoon.eot +0 -0
- package/src/SliceUI/assets/resources/fonts/icomoon.svg +601 -0
- package/src/SliceUI/assets/resources/fonts/icomoon.ttf +0 -0
- package/src/SliceUI/assets/resources/fonts/icomoon.woff +0 -0
- package/src/SliceUI/assets/resources/fonts/selection.json +1 -0
- package/src/SliceUI/assets/share.png +0 -0
- package/src/SliceUI/assets/styling.png +0 -0
- package/src/SliceUI/assets/testing.png +0 -0
- package/src/SliceUI/assets/theming.png +0 -0
- package/src/SliceUI/assets/tutorials.svg +1 -0
- package/src/SliceUI/assets/youtube.svg +1 -0
- package/src/SliceUI/automation/helper.ts +29 -0
- package/src/SliceUI/avatar/Avatar.tsx +237 -0
- package/src/SliceUI/avatar/Token.ts +116 -0
- package/src/SliceUI/avatar/Type.ts +36 -0
- package/src/SliceUI/avatar/helper.ts +53 -0
- package/src/SliceUI/badge/Badge.tsx +308 -0
- package/src/SliceUI/badge/Token.ts +202 -0
- package/src/SliceUI/badge/Type.ts +46 -0
- package/src/SliceUI/badge/helper.ts +39 -0
- package/src/SliceUI/button/Button.tsx +243 -0
- package/src/SliceUI/button/Token.ts +138 -0
- package/src/SliceUI/button/Type.ts +34 -0
- package/src/SliceUI/button/helper.ts +125 -0
- package/src/SliceUI/checkbox/Checkbox.tsx +176 -0
- package/src/SliceUI/checkbox/Token.ts +128 -0
- package/src/SliceUI/checkbox/Type.ts +35 -0
- package/src/SliceUI/chip/Chip.tsx +290 -0
- package/src/SliceUI/chip/Token.ts +151 -0
- package/src/SliceUI/chip/Type.ts +43 -0
- package/src/SliceUI/chip/helper.ts +40 -0
- package/src/SliceUI/colors/Pallete.ts +151 -0
- package/src/SliceUI/colors/Token.ts +110 -0
- package/src/SliceUI/colors/Type.ts +56 -0
- package/src/SliceUI/contextProvider/context.tsx +108 -0
- package/src/SliceUI/divider/Divider.tsx +109 -0
- package/src/SliceUI/divider/Token.ts +18 -0
- package/src/SliceUI/divider/Type.ts +26 -0
- package/src/SliceUI/icon/CustomIcon.ts +4 -0
- package/src/SliceUI/icon/IcoMoonIcon.tsx +11 -0
- package/src/SliceUI/icon/Icon.tsx +38 -0
- package/src/SliceUI/icon/Token.ts +14 -0
- package/src/SliceUI/icon/Type.ts +13 -0
- package/src/SliceUI/icon/selection.json +1 -0
- package/src/SliceUI/input/Input.tsx +573 -0
- package/src/SliceUI/input/ToDo.md +99 -0
- package/src/SliceUI/input/Token.ts +372 -0
- package/src/SliceUI/input/Type.ts +109 -0
- package/src/SliceUI/input/components/InputPortal.tsx +211 -0
- package/src/SliceUI/input/components/NativeBottomSheet.tsx +296 -0
- package/src/SliceUI/input/components/SelectChip.tsx +185 -0
- package/src/SliceUI/input/components/SelectList.tsx +173 -0
- package/src/SliceUI/input/components/SelectListItem.tsx +377 -0
- package/src/SliceUI/input/components/SelectScrollbarStyle.ts +44 -0
- package/src/SliceUI/input/hooks/useCustomScrollbar.ts +17 -0
- package/src/SliceUI/input/hooks/useInputState.ts +41 -0
- package/src/SliceUI/input/hooks/useLabelAnimation.ts +132 -0
- package/src/SliceUI/input/hooks/useOutsideClick.ts +38 -0
- package/src/SliceUI/input/hooks/useSelectLogic.ts +338 -0
- package/src/SliceUI/input/utils/inputUtils.ts +120 -0
- package/src/SliceUI/input/utils/selectUtils.ts +85 -0
- package/src/SliceUI/input/utils/styleUtils.ts +50 -0
- package/src/SliceUI/input/variants/CurrencyInput/CurrencyInput.tsx +16 -0
- package/src/SliceUI/input/variants/CurrencyInput/NativeCurrencyInput.tsx +181 -0
- package/src/SliceUI/input/variants/CurrencyInput/WebCurrencyInput.tsx +163 -0
- package/src/SliceUI/input/variants/CurrencyInput/types.ts +17 -0
- package/src/SliceUI/input/variants/PhoneInput/NativePhoneInput.tsx +189 -0
- package/src/SliceUI/input/variants/PhoneInput/PhoneInput.tsx +16 -0
- package/src/SliceUI/input/variants/PhoneInput/WebPhoneInput.tsx +291 -0
- package/src/SliceUI/input/variants/PhoneInput/types.ts +22 -0
- package/src/SliceUI/input/variants/SelectInput/SelectInput.tsx +407 -0
- package/src/SliceUI/input/variants/SelectInput/types.ts +34 -0
- package/src/SliceUI/input/variants/TextInput.tsx +68 -0
- package/src/SliceUI/layout/Box.tsx +38 -0
- package/src/SliceUI/layout/Center.tsx +38 -0
- package/src/SliceUI/layout/Divider.tsx +37 -0
- package/src/SliceUI/layout/Grid.tsx +75 -0
- package/src/SliceUI/layout/PageContainer.tsx +60 -0
- package/src/SliceUI/layout/ScrollContainer.tsx +72 -0
- package/src/SliceUI/layout/Spacer.tsx +54 -0
- package/src/SliceUI/layout/Stack.tsx +97 -0
- package/src/SliceUI/layout/StickyHeader.tsx +71 -0
- package/src/SliceUI/radio/RadioButton.tsx +130 -0
- package/src/SliceUI/radio/Token.ts +197 -0
- package/src/SliceUI/radio/Type.ts +35 -0
- package/src/SliceUI/react-native.config.js +3 -0
- package/src/SliceUI/responsive/Type.ts +7 -0
- package/src/SliceUI/responsive/helper.ts +53 -0
- package/src/SliceUI/switch/Switch.tsx +119 -0
- package/src/SliceUI/switch/Token.ts +205 -0
- package/src/SliceUI/switch/Type.ts +26 -0
- package/src/SliceUI/tab/TabItem.tsx +204 -0
- package/src/SliceUI/tab/Tabs.tsx +110 -0
- package/src/SliceUI/tab/Token.ts +282 -0
- package/src/SliceUI/tab/Type.ts +66 -0
- package/src/SliceUI/tab/helper.ts +53 -0
- package/src/SliceUI/table/Table.tsx +388 -0
- package/src/SliceUI/table/TableCell.tsx +158 -0
- package/src/SliceUI/table/TableFooter.tsx +353 -0
- package/src/SliceUI/table/TableHeader.tsx +247 -0
- package/src/SliceUI/table/TableRow.tsx +218 -0
- package/src/SliceUI/table/Token.ts +252 -0
- package/src/SliceUI/table/Type.ts +213 -0
- package/src/SliceUI/table/helper.ts +376 -0
- package/src/SliceUI/table/index.ts +53 -0
- package/src/SliceUI/theme/dummyColors.tsx +7 -0
- package/src/SliceUI/theme/theme.ts +107 -0
- package/src/SliceUI/typography/BaseTypographyToken.ts +62 -0
- package/src/SliceUI/typography/FoundationToken.ts +48 -0
- package/src/SliceUI/typography/Token.ts +228 -0
- package/src/SliceUI/typography/Type.ts +20 -0
- package/src/SliceUI/typography/Typography.tsx +99 -0
- package/src/SliceUI/values/BorderRadius.ts +17 -0
- package/src/SliceUI/values/BorderWidth.ts +7 -0
- package/src/SliceUI/values/Dimension.ts +35 -0
- package/src/SliceUI/values/IconSizes.ts +13 -0
- package/src/SliceUI/values/Spacing.ts +22 -0
- package/src/declarations.d.ts +8 -0
- package/src/index.tsx +119 -0
- package/src/stories/Colors.mdx +1418 -0
- package/src/stories/Dimensions.mdx +60 -0
- package/src/stories/GetStarted.mdx +90 -0
- package/src/stories/Introduction.mdx +136 -0
- package/src/stories/Shape.mdx +126 -0
- package/src/stories/Spacing.mdx +104 -0
- package/src/stories/Typography.mdx +454 -0
- package/src/stories/Utils.mdx +277 -0
- package/src/stories/story-components/AddIcon.js +13 -0
- package/src/stories/story-components/RectangleWithBox.jsx +51 -0
- package/src/stories/story-components/RoundedRectangle.jsx +18 -0
- package/src/stories/story-components/RoundedWithWhiteInside.jsx +33 -0
- package/src/stories/story-components/WhiteRoundedRectangle.jsx +107 -0
- package/src/stories/story-components/svgPaths.js +126 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
|
2
|
+
import type { SelectOption, ExtendedInputVariant } from '../Type';
|
|
3
|
+
import { toggleOption, filterAndRankBySearch } from '../utils/selectUtils';
|
|
4
|
+
|
|
5
|
+
interface UseSelectLogicParams {
|
|
6
|
+
variant?: ExtendedInputVariant;
|
|
7
|
+
value?: string | string[];
|
|
8
|
+
options: SelectOption[];
|
|
9
|
+
searchable?: boolean;
|
|
10
|
+
normalizedMultiple?: boolean;
|
|
11
|
+
onChangeText?: ((value: string) => void) | ((value: string | string[]) => void);
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
focusInput?: () => void;
|
|
14
|
+
maxAllowed?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function useStableState<T>(initialValue: T): [T, (value: T | ((prev: T) => T)) => void] {
|
|
18
|
+
const [state, setState] = useState<T>(initialValue);
|
|
19
|
+
const setStable = useCallback((value: T | ((prev: T) => T)) => {
|
|
20
|
+
setState(value);
|
|
21
|
+
}, []);
|
|
22
|
+
return [state, setStable];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const useSelectLogic = ({
|
|
26
|
+
variant,
|
|
27
|
+
value,
|
|
28
|
+
options,
|
|
29
|
+
searchable = false,
|
|
30
|
+
normalizedMultiple,
|
|
31
|
+
onChangeText,
|
|
32
|
+
disabled = false,
|
|
33
|
+
focusInput,
|
|
34
|
+
maxAllowed,
|
|
35
|
+
}: UseSelectLogicParams) => {
|
|
36
|
+
const [selectDropdownOpen, setSelectDropdownOpen] = useStableState(false);
|
|
37
|
+
const [searchValue, setSearchValue] = useStableState('');
|
|
38
|
+
const [debouncedSearchValue, setDebouncedSearchValue] = useStableState('');
|
|
39
|
+
|
|
40
|
+
const optionRefs = useRef<Map<number, HTMLElement>>(new Map());
|
|
41
|
+
const prevSelectDropdownOpen = useRef(selectDropdownOpen);
|
|
42
|
+
const searchValueRef = useRef(searchValue);
|
|
43
|
+
const filteredOptionsRef = useRef<SelectOption[]>(options);
|
|
44
|
+
|
|
45
|
+
const suppressNextOpenRef = useRef(false);
|
|
46
|
+
|
|
47
|
+
const selectedValues = useMemo(() => {
|
|
48
|
+
if (normalizedMultiple) {
|
|
49
|
+
return Array.isArray(value) ? value : [];
|
|
50
|
+
}
|
|
51
|
+
return value && typeof value === 'string' ? [value] : [];
|
|
52
|
+
}, [normalizedMultiple, value]);
|
|
53
|
+
|
|
54
|
+
const filteredOptions = useMemo(() => {
|
|
55
|
+
const base = options;
|
|
56
|
+
|
|
57
|
+
if (!searchable || !debouncedSearchValue.trim()) {
|
|
58
|
+
return base;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return filterAndRankBySearch(base, debouncedSearchValue, o => o.label);
|
|
62
|
+
}, [options, searchable, debouncedSearchValue]);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
filteredOptionsRef.current = filteredOptions;
|
|
66
|
+
}, [filteredOptions]);
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
prevSelectDropdownOpen.current = selectDropdownOpen;
|
|
70
|
+
}, [selectDropdownOpen]);
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
const t = setTimeout(() => {
|
|
74
|
+
setDebouncedSearchValue(searchValue);
|
|
75
|
+
}, 250);
|
|
76
|
+
return () => clearTimeout(t);
|
|
77
|
+
}, [searchValue, setDebouncedSearchValue]);
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
searchValueRef.current = searchValue;
|
|
81
|
+
}, [searchValue]);
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (selectDropdownOpen) return;
|
|
85
|
+
if (searchValue !== '') {
|
|
86
|
+
setSearchValue('');
|
|
87
|
+
}
|
|
88
|
+
}, [selectDropdownOpen, searchValue, setSearchValue]);
|
|
89
|
+
|
|
90
|
+
const handleCheckboxToggle = useCallback((value: string, checked: boolean) => {
|
|
91
|
+
if (!normalizedMultiple) return;
|
|
92
|
+
if (disabled) return;
|
|
93
|
+
|
|
94
|
+
if (checked && maxAllowed !== undefined && selectedValues.length >= maxAllowed) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const next = checked
|
|
99
|
+
? [...selectedValues, value]
|
|
100
|
+
: selectedValues.filter(v => v !== value);
|
|
101
|
+
|
|
102
|
+
(onChangeText as any)?.(next);
|
|
103
|
+
setSearchValue('');
|
|
104
|
+
}, [normalizedMultiple, disabled, selectedValues, onChangeText, setSearchValue, maxAllowed]);
|
|
105
|
+
|
|
106
|
+
const handleSelectKeyDown = useCallback(
|
|
107
|
+
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
108
|
+
if (variant !== 'select') return;
|
|
109
|
+
if (disabled) return;
|
|
110
|
+
|
|
111
|
+
e.stopPropagation();
|
|
112
|
+
|
|
113
|
+
switch (e.key) {
|
|
114
|
+
case 'Enter': {
|
|
115
|
+
e.preventDefault();
|
|
116
|
+
|
|
117
|
+
if (!selectDropdownOpen) {
|
|
118
|
+
setSelectDropdownOpen(true);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let didSelect = false;
|
|
123
|
+
|
|
124
|
+
if (searchValueRef.current) {
|
|
125
|
+
const searchBase = filterAndRankBySearch(options, searchValueRef.current, o => o.label);
|
|
126
|
+
const exactMatch = searchBase.find(
|
|
127
|
+
o => o.label.toLowerCase() === searchValueRef.current.toLowerCase(),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
if (exactMatch) {
|
|
131
|
+
(onChangeText as any)?.(
|
|
132
|
+
normalizedMultiple
|
|
133
|
+
? toggleOption(selectedValues, exactMatch.value)
|
|
134
|
+
: exactMatch.value,
|
|
135
|
+
);
|
|
136
|
+
didSelect = true;
|
|
137
|
+
} else if (searchBase.length === 1) {
|
|
138
|
+
const only = searchBase[0];
|
|
139
|
+
(onChangeText as any)?.(
|
|
140
|
+
normalizedMultiple
|
|
141
|
+
? toggleOption(selectedValues, only.value)
|
|
142
|
+
: only.value,
|
|
143
|
+
);
|
|
144
|
+
didSelect = true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!didSelect) {
|
|
149
|
+
const focusedEntry = Array.from(optionRefs.current.entries())
|
|
150
|
+
.find(([, el]) => el === (document.activeElement as HTMLElement));
|
|
151
|
+
if (!focusedEntry) return;
|
|
152
|
+
const [focusedIndex] = focusedEntry;
|
|
153
|
+
const option = filteredOptionsRef.current[focusedIndex];
|
|
154
|
+
if (!option) return;
|
|
155
|
+
|
|
156
|
+
(onChangeText as any)?.(
|
|
157
|
+
normalizedMultiple
|
|
158
|
+
? toggleOption(selectedValues, option.value)
|
|
159
|
+
: option.value,
|
|
160
|
+
);
|
|
161
|
+
didSelect = true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (didSelect) {
|
|
165
|
+
setSearchValue('');
|
|
166
|
+
if (!normalizedMultiple) {
|
|
167
|
+
setSelectDropdownOpen(false);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
suppressNextOpenRef.current = true;
|
|
171
|
+
focusInput?.();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
case ' ': {
|
|
177
|
+
if (!selectDropdownOpen) {
|
|
178
|
+
e.preventDefault();
|
|
179
|
+
setSelectDropdownOpen(true);
|
|
180
|
+
}
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
case 'Backspace': {
|
|
184
|
+
if (!searchValueRef.current) {
|
|
185
|
+
if (normalizedMultiple && selectedValues.length > 0) {
|
|
186
|
+
e.preventDefault();
|
|
187
|
+
const next = selectedValues.slice(0, -1);
|
|
188
|
+
(onChangeText as any)?.(next);
|
|
189
|
+
} else if (!normalizedMultiple && value && typeof value === 'string') {
|
|
190
|
+
e.preventDefault();
|
|
191
|
+
(onChangeText as any)?.('');
|
|
192
|
+
if (searchable && !selectDropdownOpen) {
|
|
193
|
+
setSelectDropdownOpen(true);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
case 'ArrowDown': {
|
|
201
|
+
e.preventDefault();
|
|
202
|
+
|
|
203
|
+
if (!selectDropdownOpen) {
|
|
204
|
+
setSelectDropdownOpen(true);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const entries = Array.from(optionRefs.current.entries()).sort(
|
|
209
|
+
(a, b) => a[0] - b[0],
|
|
210
|
+
);
|
|
211
|
+
const currentIdx = entries.findIndex(([, el]) => el === (document.activeElement as HTMLElement));
|
|
212
|
+
const nextIdx = currentIdx < 0 ? 0 : Math.min(currentIdx + 1, entries.length - 1);
|
|
213
|
+
const nextEl = entries[nextIdx]?.[1];
|
|
214
|
+
if (nextEl) {
|
|
215
|
+
nextEl.focus();
|
|
216
|
+
nextEl.scrollIntoView({ block: 'nearest' });
|
|
217
|
+
}
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
case 'ArrowUp': {
|
|
222
|
+
e.preventDefault();
|
|
223
|
+
if (!selectDropdownOpen) return;
|
|
224
|
+
|
|
225
|
+
const entries = Array.from(optionRefs.current.entries()).sort(
|
|
226
|
+
(a, b) => a[0] - b[0],
|
|
227
|
+
);
|
|
228
|
+
const currentIdx = entries.findIndex(([, el]) => el === (document.activeElement as HTMLElement));
|
|
229
|
+
const prevIdx = currentIdx < 0 ? 0 : Math.max(currentIdx - 1, 0);
|
|
230
|
+
const prevEl = entries[prevIdx]?.[1];
|
|
231
|
+
if (prevEl) {
|
|
232
|
+
prevEl.focus();
|
|
233
|
+
prevEl.scrollIntoView({ block: 'nearest' });
|
|
234
|
+
}
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
case 'Escape': {
|
|
239
|
+
e.preventDefault();
|
|
240
|
+
setSelectDropdownOpen(false);
|
|
241
|
+
(e.currentTarget as HTMLElement).blur();
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
[
|
|
248
|
+
variant,
|
|
249
|
+
selectDropdownOpen,
|
|
250
|
+
normalizedMultiple,
|
|
251
|
+
selectedValues,
|
|
252
|
+
options,
|
|
253
|
+
onChangeText,
|
|
254
|
+
disabled,
|
|
255
|
+
value,
|
|
256
|
+
searchable,
|
|
257
|
+
focusInput,
|
|
258
|
+
setSelectDropdownOpen,
|
|
259
|
+
setSearchValue,
|
|
260
|
+
optionRefs,
|
|
261
|
+
],
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const handleOptionSelect = useCallback(
|
|
265
|
+
(optionValue: string) => {
|
|
266
|
+
if (disabled) return;
|
|
267
|
+
|
|
268
|
+
if (normalizedMultiple) {
|
|
269
|
+
// MULTI SELECT
|
|
270
|
+
if (
|
|
271
|
+
!selectedValues.includes(optionValue) &&
|
|
272
|
+
maxAllowed !== undefined &&
|
|
273
|
+
selectedValues.length >= maxAllowed
|
|
274
|
+
) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const next = toggleOption(selectedValues, optionValue);
|
|
279
|
+
(onChangeText as any)?.(next);
|
|
280
|
+
|
|
281
|
+
// Clear search but DO NOT refocus the input.
|
|
282
|
+
// Refocusing triggers onFocus -> onOpen -> dropdown flicker.
|
|
283
|
+
setSearchValue('');
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// SINGLE SELECT
|
|
288
|
+
if (value === optionValue) {
|
|
289
|
+
(onChangeText as any)?.('');
|
|
290
|
+
} else {
|
|
291
|
+
(onChangeText as any)?.(optionValue);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
setSearchValue('');
|
|
295
|
+
setSelectDropdownOpen(false);
|
|
296
|
+
|
|
297
|
+
// Single select should refocus, but we must suppress the next onFocus
|
|
298
|
+
// from re-opening the dropdown (onFocus={onOpen} on the search TextInput).
|
|
299
|
+
suppressNextOpenRef.current = true;
|
|
300
|
+
focusInput?.();
|
|
301
|
+
},
|
|
302
|
+
[
|
|
303
|
+
normalizedMultiple,
|
|
304
|
+
selectedValues,
|
|
305
|
+
onChangeText,
|
|
306
|
+
disabled,
|
|
307
|
+
focusInput,
|
|
308
|
+
value,
|
|
309
|
+
maxAllowed,
|
|
310
|
+
setSearchValue,
|
|
311
|
+
setSelectDropdownOpen,
|
|
312
|
+
],
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
const registerOptionRef = useCallback((index: number, element: HTMLElement | null) => {
|
|
318
|
+
if (element) {
|
|
319
|
+
optionRefs.current.set(index, element);
|
|
320
|
+
} else {
|
|
321
|
+
optionRefs.current.delete(index);
|
|
322
|
+
}
|
|
323
|
+
}, []);
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
selectDropdownOpen,
|
|
327
|
+
setSelectDropdownOpen,
|
|
328
|
+
searchValue,
|
|
329
|
+
setSearchValue,
|
|
330
|
+
filteredOptions,
|
|
331
|
+
selectedValues,
|
|
332
|
+
handleSelectKeyDown,
|
|
333
|
+
handleOptionSelect,
|
|
334
|
+
handleCheckboxToggle,
|
|
335
|
+
registerOptionRef,
|
|
336
|
+
suppressNextOpenRef,
|
|
337
|
+
};
|
|
338
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
import type { ExtendedInputVariant, SelectListType, SelectOption } from '../Type';
|
|
3
|
+
|
|
4
|
+
export const isWeb = Platform.OS === 'web';
|
|
5
|
+
|
|
6
|
+
export const currencySymbols: Record<string, string> = {
|
|
7
|
+
USD: '$',
|
|
8
|
+
EUR: '€',
|
|
9
|
+
INR: '₹',
|
|
10
|
+
JPY: '¥',
|
|
11
|
+
GBP: '£',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const availableCurrencies = [
|
|
15
|
+
'USD',
|
|
16
|
+
'EUR',
|
|
17
|
+
'INR',
|
|
18
|
+
'JPY',
|
|
19
|
+
'GBP',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export const labelLeftOffsetBySize: Record<string, number> = {
|
|
23
|
+
small: 36,
|
|
24
|
+
medium: 40,
|
|
25
|
+
large: 48,
|
|
26
|
+
xlarge: 56,
|
|
27
|
+
xxlarge: 64,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const getNormalizedVariant = (
|
|
31
|
+
variant?: ExtendedInputVariant,
|
|
32
|
+
): ExtendedInputVariant | 'select' => {
|
|
33
|
+
return variant === 'checkbox' ? 'select' : variant ?? 'outlined';
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const getNormalizedMultiple = (
|
|
37
|
+
variant?: ExtendedInputVariant,
|
|
38
|
+
multiple?: boolean,
|
|
39
|
+
): boolean | undefined => {
|
|
40
|
+
return variant === 'checkbox' ? true : multiple;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const getNormalizedListType = (
|
|
44
|
+
variant?: ExtendedInputVariant,
|
|
45
|
+
listType?: SelectListType,
|
|
46
|
+
): SelectListType => {
|
|
47
|
+
return variant === 'checkbox' ? 'checkbox' : listType ?? 'default';
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const isSelectVariant = (
|
|
51
|
+
variant?: ExtendedInputVariant,
|
|
52
|
+
): boolean => {
|
|
53
|
+
return (
|
|
54
|
+
variant === 'select' ||
|
|
55
|
+
variant === 'checkbox'
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const isMultiSelect = (
|
|
60
|
+
normalizedMultiple?: boolean,
|
|
61
|
+
): boolean => {
|
|
62
|
+
return normalizedMultiple === true;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
interface GetDisplayValueParams {
|
|
66
|
+
variant?: ExtendedInputVariant;
|
|
67
|
+
value?: string | string[];
|
|
68
|
+
selectedValues: string[];
|
|
69
|
+
options: SelectOption[];
|
|
70
|
+
labelText?: string;
|
|
71
|
+
placeholder?: string;
|
|
72
|
+
normalizedMultiple?: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const getDisplayValue = ({
|
|
76
|
+
variant,
|
|
77
|
+
value,
|
|
78
|
+
selectedValues,
|
|
79
|
+
options,
|
|
80
|
+
labelText,
|
|
81
|
+
placeholder,
|
|
82
|
+
normalizedMultiple,
|
|
83
|
+
}: GetDisplayValueParams): string => {
|
|
84
|
+
if (variant !== 'select') {
|
|
85
|
+
return typeof value === 'string'
|
|
86
|
+
? value || (!labelText ? placeholder ?? '' : '')
|
|
87
|
+
: '';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!normalizedMultiple) {
|
|
91
|
+
const stringValue = typeof value === 'string' ? value : '';
|
|
92
|
+
return (
|
|
93
|
+
options.find(o => o.value === stringValue)?.label ??
|
|
94
|
+
(!labelText ? placeholder ?? '' : '')
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (selectedValues.length === 0) {
|
|
99
|
+
return !labelText ? placeholder ?? '' : '';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (selectedValues.length === 1) {
|
|
103
|
+
return (
|
|
104
|
+
options.find(o => o.value === selectedValues[0])?.label ?? ''
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (selectedValues.length === 2) {
|
|
109
|
+
return selectedValues
|
|
110
|
+
.map(v => options.find(o => o.value === v)?.label)
|
|
111
|
+
.filter(Boolean)
|
|
112
|
+
.join(', ');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const first =
|
|
116
|
+
options.find(o => o.value === selectedValues[0])?.label ?? '';
|
|
117
|
+
|
|
118
|
+
return `${first} +${selectedValues.length - 1}`;
|
|
119
|
+
};
|
|
120
|
+
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { SelectOption } from '../Type';
|
|
2
|
+
|
|
3
|
+
export const optionExists = (
|
|
4
|
+
selectedValues: string[],
|
|
5
|
+
value: string,
|
|
6
|
+
): boolean => {
|
|
7
|
+
return selectedValues.includes(value);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const toggleOption = (
|
|
11
|
+
selectedValues: string[],
|
|
12
|
+
value: string,
|
|
13
|
+
): string[] => {
|
|
14
|
+
const exists = selectedValues.includes(value);
|
|
15
|
+
|
|
16
|
+
return exists
|
|
17
|
+
? selectedValues.filter(v => v !== value)
|
|
18
|
+
: [...selectedValues, value];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const removeOption = (
|
|
22
|
+
selectedValues: string[],
|
|
23
|
+
value: string,
|
|
24
|
+
): string[] => {
|
|
25
|
+
return selectedValues.filter(v => v !== value);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const removeLastSelected = (
|
|
29
|
+
selectedValues: string[],
|
|
30
|
+
): string[] => {
|
|
31
|
+
if (selectedValues.length === 0) return selectedValues;
|
|
32
|
+
|
|
33
|
+
return selectedValues.slice(0, -1);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function filterAndRankBySearch<T>(
|
|
37
|
+
items: T[],
|
|
38
|
+
query: string,
|
|
39
|
+
getLabel: (item: T) => string,
|
|
40
|
+
): T[] {
|
|
41
|
+
const q = query.trim().toLowerCase();
|
|
42
|
+
if (!q) return items;
|
|
43
|
+
|
|
44
|
+
const scored = items
|
|
45
|
+
.map((item, index) => {
|
|
46
|
+
const label = getLabel(item);
|
|
47
|
+
const lowerLabel = label.toLowerCase();
|
|
48
|
+
const tokens = lowerLabel.split(/\s+/);
|
|
49
|
+
let score = 0;
|
|
50
|
+
|
|
51
|
+
if (lowerLabel === q) {
|
|
52
|
+
score = 1000;
|
|
53
|
+
} else if (lowerLabel.startsWith(q + ' ') || lowerLabel.startsWith(q)) {
|
|
54
|
+
score = 500;
|
|
55
|
+
} else {
|
|
56
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
57
|
+
const token = tokens[i];
|
|
58
|
+
if (token === q) {
|
|
59
|
+
score = Math.max(score, 100 - i * 10);
|
|
60
|
+
} else if (token.startsWith(q)) {
|
|
61
|
+
score = Math.max(score, 80 - i * 10);
|
|
62
|
+
} else if (token.includes(q)) {
|
|
63
|
+
score = Math.max(score, 60 - i * 10);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (score === 0 && lowerLabel.includes(q)) {
|
|
68
|
+
score = 40;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return score > 0 ? { item, score, index } : null;
|
|
73
|
+
})
|
|
74
|
+
.filter(Boolean) as { item: T; score: number; index: number }[];
|
|
75
|
+
|
|
76
|
+
return scored
|
|
77
|
+
.sort((a, b) => {
|
|
78
|
+
if (b.score !== a.score) {
|
|
79
|
+
return b.score - a.score;
|
|
80
|
+
}
|
|
81
|
+
return a.index - b.index;
|
|
82
|
+
})
|
|
83
|
+
.map(entry => entry.item);
|
|
84
|
+
}
|
|
85
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ExtendedTheme } from '../../colors/Type';
|
|
2
|
+
import type { InputVariantType } from '../Type';
|
|
3
|
+
|
|
4
|
+
export const getInputBackgroundColor = (
|
|
5
|
+
variant: InputVariantType,
|
|
6
|
+
state: 'default' | 'hover' | 'focused',
|
|
7
|
+
theme: ExtendedTheme
|
|
8
|
+
): string => {
|
|
9
|
+
if (variant === 'filled') {
|
|
10
|
+
return theme.colors.colorBackgroundLight;
|
|
11
|
+
}
|
|
12
|
+
return 'transparent';
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const getInputBorderColor = (
|
|
16
|
+
variant: InputVariantType,
|
|
17
|
+
state: 'default' | 'hover' | 'focused' | 'error',
|
|
18
|
+
theme: ExtendedTheme
|
|
19
|
+
): string => {
|
|
20
|
+
if (state === 'error') {
|
|
21
|
+
return theme.colors.colorBorderNegative;
|
|
22
|
+
}
|
|
23
|
+
if (state === 'focused') {
|
|
24
|
+
return theme.colors.colorBorderAccent;
|
|
25
|
+
}
|
|
26
|
+
if (state === 'hover') {
|
|
27
|
+
return theme.colors.colorBorderMedium;
|
|
28
|
+
}
|
|
29
|
+
return theme.colors.colorBorderSubtle;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const getInputTextColor = (
|
|
33
|
+
variant: InputVariantType,
|
|
34
|
+
state: 'default' | 'disabled',
|
|
35
|
+
theme: ExtendedTheme
|
|
36
|
+
): string => {
|
|
37
|
+
if (state === 'disabled') {
|
|
38
|
+
return theme.colors.colorForegroundTertiary;
|
|
39
|
+
}
|
|
40
|
+
return theme.colors.colorForegroundPrimary;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const formatPhoneNumber = (value: string): string => {
|
|
44
|
+
const cleaned = value.replace(/\D/g, '');
|
|
45
|
+
const match = cleaned.match(/^(\d{3})(\d{3})(\d{4})$/);
|
|
46
|
+
if (match) {
|
|
47
|
+
return `${match[1]}-${match[2]}-${match[3]}`;
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// CurrencyInput/CurrencyInput.tsx
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { isWeb } from '../../utils/inputUtils';
|
|
4
|
+
import type { CurrencyInputProps } from './types';
|
|
5
|
+
import NativeCurrencyInput from './NativeCurrencyInput';
|
|
6
|
+
import WebCurrencyInput from './WebCurrencyInput';
|
|
7
|
+
|
|
8
|
+
const CurrencyInput = (props: CurrencyInputProps) => {
|
|
9
|
+
if (isWeb) {
|
|
10
|
+
return <WebCurrencyInput {...props} />;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return <NativeCurrencyInput {...props} />;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default CurrencyInput;
|