pentatrion-design 0.0.1
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/.storybook/main.ts +33 -0
- package/.storybook/manager.ts +8 -0
- package/.storybook/preview-head.html +2 -0
- package/.storybook/preview.ts +26 -0
- package/.storybook/public/bg-map.jpg +0 -0
- package/.storybook/public/favicon-32x32.png +0 -0
- package/.storybook/public/favicon.svg +28 -0
- package/.storybook/public/movies.json +2153 -0
- package/.storybook/public/population.json +1178 -0
- package/.storybook/public/town-74.geojson +8995 -0
- package/.storybook/public/town-74.json +4062 -0
- package/.storybook/theme.ts +14 -0
- package/Makefile +14 -0
- package/README.md +40 -0
- package/TODO.md +5 -0
- package/dist/index.d.ts +1110 -0
- package/dist/index.js +3707 -0
- package/dist/redux/index.d.ts +43 -0
- package/dist/redux/index.js +583 -0
- package/dist/types.d-DlHa_IaW.d.ts +14 -0
- package/images/logo-shadow.svg +173 -0
- package/images/logo.svg +76 -0
- package/package.json +85 -0
- package/postcss.config.js +7 -0
- package/src/components/autocomplete/Autocomplete.stories.tsx +89 -0
- package/src/components/autocomplete/Autocomplete.tsx +316 -0
- package/src/components/autocomplete/AutocompleteOption.tsx +35 -0
- package/src/components/autocomplete/LazyAutocomplete.tsx +130 -0
- package/src/components/autocomplete/SimpleAutocomplete.tsx +74 -0
- package/src/components/autocomplete/index.ts +6 -0
- package/src/components/autocomplete/useAutocompleteContext.ts +20 -0
- package/src/components/autocomplete/util.ts +19 -0
- package/src/components/badge/Badge.stories.tsx +72 -0
- package/src/components/badge/Badge.tsx +70 -0
- package/src/components/button/Button.stories.tsx +189 -0
- package/src/components/button/Button.test.tsx +20 -0
- package/src/components/button/Button.tsx +200 -0
- package/src/components/button/ButtonGroup.stories.tsx +46 -0
- package/src/components/button/ButtonGroup.tsx +29 -0
- package/src/components/button/index.ts +2 -0
- package/src/components/card/Card.stories.tsx +21 -0
- package/src/components/card/config.ts +4 -0
- package/src/components/card/index.ts +1 -0
- package/src/components/code/Code.tsx +15 -0
- package/src/components/context-menu/ContextMenu.stories.tsx +22 -0
- package/src/components/context-menu/ContextMenu.tsx +175 -0
- package/src/components/context-menu/ContextMenuItem.tsx +34 -0
- package/src/components/context-menu/index.ts +2 -0
- package/src/components/dialog/Dialog.stories.tsx +270 -0
- package/src/components/dialog/Dialog.tsx +46 -0
- package/src/components/dialog/index.ts +1 -0
- package/src/components/dialog/util.ts +29 -0
- package/src/components/dropdown-menu/DropdownMenu.stories.tsx +19 -0
- package/src/components/dropdown-menu/DropdownMenu.tsx +12 -0
- package/src/components/dropdown-menu/DropdownMenuContent.tsx +46 -0
- package/src/components/dropdown-menu/DropdownMenuItem.tsx +33 -0
- package/src/components/dropdown-menu/DropdownMenuTrigger.tsx +42 -0
- package/src/components/dropdown-menu/index.ts +7 -0
- package/src/components/dropdown-menu/interface.d.ts +13 -0
- package/src/components/dropdown-menu/useDropdownMenu.tsx +115 -0
- package/src/components/dropdown-menu/useDropdownMenuContext.ts +16 -0
- package/src/components/dropdown-menu-nested/DropdownMenuNested.stories.tsx +33 -0
- package/src/components/dropdown-menu-nested/DropdownMenuNested.tsx +19 -0
- package/src/components/dropdown-menu-nested/MenuContext.ts +15 -0
- package/src/components/dropdown-menu-nested/MenuItem.tsx +43 -0
- package/src/components/dropdown-menu-nested/MenuItemWithChildren.tsx +204 -0
- package/src/components/dropdown-menu-nested/index.ts +3 -0
- package/src/components/flash/Flash.stories.tsx +34 -0
- package/src/components/flash/Flash.tsx +24 -0
- package/src/components/highlight/Highlight.stories.tsx +128 -0
- package/src/components/highlight/Highlight.tsx +29 -0
- package/src/components/href/Href.stories.tsx +55 -0
- package/src/components/href/Href.tsx +18 -0
- package/src/components/input/Checkbox.stories.tsx +52 -0
- package/src/components/input/Checkbox.tsx +41 -0
- package/src/components/input/Input.stories.tsx +103 -0
- package/src/components/input/Input.test.tsx +10 -0
- package/src/components/input/Input.tsx +60 -0
- package/src/components/input/RadioGroup.stories.tsx +76 -0
- package/src/components/input/RadioGroup.tsx +84 -0
- package/src/components/input/Toggle.stories.tsx +29 -0
- package/src/components/input/Toggle.tsx +35 -0
- package/src/components/input/index.ts +4 -0
- package/src/components/input-field/InputField.stories.tsx +227 -0
- package/src/components/input-field/InputField.tsx +62 -0
- package/src/components/loader/Loader.stories.tsx +37 -0
- package/src/components/loader/Loader.tsx +54 -0
- package/src/components/modal/Components.tsx +58 -0
- package/src/components/modal/Modal.stories.tsx +113 -0
- package/src/components/modal/Modal.tsx +13 -0
- package/src/components/modal/ModalContent.tsx +46 -0
- package/src/components/modal/ModalTrigger.tsx +41 -0
- package/src/components/modal/index.ts +7 -0
- package/src/components/modal/interface.d.ts +10 -0
- package/src/components/modal/useModal.ts +48 -0
- package/src/components/modal/useModalContext.ts +16 -0
- package/src/components/notification/Notification.stories.tsx +51 -0
- package/src/components/notification/NotificationsContext.ts +79 -0
- package/src/components/notification/NotificationsProvider.tsx +58 -0
- package/src/components/notification/index.ts +2 -0
- package/src/components/popover/Components.tsx +45 -0
- package/src/components/popover/Popover.stories.tsx +46 -0
- package/src/components/popover/Popover.test.tsx +21 -0
- package/src/components/popover/Popover.tsx +12 -0
- package/src/components/popover/PopoverContent.tsx +46 -0
- package/src/components/popover/PopoverTrigger.tsx +42 -0
- package/src/components/popover/index.ts +7 -0
- package/src/components/popover/interface.d.ts +13 -0
- package/src/components/popover/usePopover.tsx +80 -0
- package/src/components/popover/usePopoverContext.ts +16 -0
- package/src/components/portal/Portal.tsx +12 -0
- package/src/components/quick-settings/QuickSettings.stories.tsx +182 -0
- package/src/components/resize-area/ResizeArea.stories.css +40 -0
- package/src/components/resize-area/ResizeArea.stories.tsx +30 -0
- package/src/components/resize-area/ResizeArea.tsx +79 -0
- package/src/components/scroll/Scroll.stories.tsx +81 -0
- package/src/components/scroll/Scroll.tsx +111 -0
- package/src/components/select/Select.stories.tsx +248 -0
- package/src/components/select/Select.tsx +362 -0
- package/src/components/select/SelectOption.tsx +29 -0
- package/src/components/select/SelectSelection.tsx +13 -0
- package/src/components/select/index.ts +5 -0
- package/src/components/select/interface.d.ts +36 -0
- package/src/components/select/useSelectContext.ts +19 -0
- package/src/components/select/util.ts +12 -0
- package/src/components/snack/Snack.stories.tsx +33 -0
- package/src/components/snack/Snack.tsx +47 -0
- package/src/components/sortable/Sortable.stories.tsx +98 -0
- package/src/components/sortable/Sortable.tsx +37 -0
- package/src/components/sortable/index.ts +2 -0
- package/src/components/sortable/types.d.ts +12 -0
- package/src/components/steps/Step.tsx +41 -0
- package/src/components/steps/Steps.stories.tsx +141 -0
- package/src/components/steps/Steps.tsx +49 -0
- package/src/components/steps/index.ts +3 -0
- package/src/components/steps/util.ts +3 -0
- package/src/components/table/Table.stories.tsx +36 -0
- package/src/components/table/Table.tsx +13 -0
- package/src/components/table/TableBody.tsx +9 -0
- package/src/components/table/TableCell.tsx +16 -0
- package/src/components/table/TableFooter.tsx +9 -0
- package/src/components/table/TableHeader.tsx +14 -0
- package/src/components/table/TableHeaderCell.tsx +14 -0
- package/src/components/table/TableRow.tsx +14 -0
- package/src/components/table/index.ts +7 -0
- package/src/components/tabs/Tabs.stories.tsx +66 -0
- package/src/components/tabs/Tabs.tsx +80 -0
- package/src/components/tooltip/SimpleTooltip.tsx +17 -0
- package/src/components/tooltip/Tooltip.stories.tsx +129 -0
- package/src/components/tooltip/Tooltip.test.tsx +18 -0
- package/src/components/tooltip/Tooltip.tsx +13 -0
- package/src/components/tooltip/TooltipContent.tsx +41 -0
- package/src/components/tooltip/TooltipTrigger.tsx +36 -0
- package/src/components/tooltip/index.ts +6 -0
- package/src/components/tooltip/interface.d.ts +15 -0
- package/src/components/tooltip/useTooltip.ts +87 -0
- package/src/components/tooltip/useTooltipContext.ts +16 -0
- package/src/env.d.ts +27 -0
- package/src/hooks/index.ts +13 -0
- package/src/hooks/useCombinedRefs.test.tsx +42 -0
- package/src/hooks/useCombinedRefs.ts +21 -0
- package/src/hooks/useCopyToClipboard.ts +28 -0
- package/src/hooks/useDebounce.ts +59 -0
- package/src/hooks/useEventCallback.ts +13 -0
- package/src/hooks/useEventListener.ts +86 -0
- package/src/hooks/useFetch.tsx +21 -0
- package/src/hooks/useIsClosing.ts +25 -0
- package/src/hooks/useIsMounted.tsx +16 -0
- package/src/hooks/useIsomorphicLayoutEffect.ts +4 -0
- package/src/hooks/useNotifications.tsx +13 -0
- package/src/hooks/useOnClickOutside.ts +35 -0
- package/src/hooks/usePrevious.ts +9 -0
- package/src/hooks/useRipple.test.tsx +52 -0
- package/src/hooks/useRipple.tsx +79 -0
- package/src/index.css +14 -0
- package/src/index.ts +39 -0
- package/src/lib/arrUtil.ts +8 -0
- package/src/lib/error.ts +18 -0
- package/src/lib/fetch.test.ts +107 -0
- package/src/lib/fetch.ts +115 -0
- package/src/lib/index.ts +3 -0
- package/src/lib/tailwindVariants.ts +35 -0
- package/src/redux/index.ts +1 -0
- package/src/redux/notification/components/NotificationConsumer.tsx +52 -0
- package/src/redux/notification/errorCatcherMiddleware.ts +17 -0
- package/src/redux/notification/hooks/useNotification.ts +26 -0
- package/src/redux/notification/index.ts +5 -0
- package/src/redux/notification/notificationSlice.ts +68 -0
- package/src/styles/Animation.stories.tsx +43 -0
- package/src/styles/Shadow.stories.tsx +43 -0
- package/src/styles/Themes.stories.tsx +31 -0
- package/src/styles/Typography.stories.tsx +101 -0
- package/src/tailwind/base.css +188 -0
- package/src/tailwind/components-input-outline.css +62 -0
- package/src/tailwind/components-resize-area.css +112 -0
- package/src/tailwind/components-step.css +161 -0
- package/src/tailwind/components.css +51 -0
- package/src/tailwind/keyframes.ts +69 -0
- package/src/tailwind/pentatrionTw.ts +147 -0
- package/src/tailwind/utilities-dialog.css +103 -0
- package/src/tailwind/utilities.css +3 -0
- package/src/types.d.ts +12 -0
- package/tailwind.config.js +18 -0
- package/tests/README.md +32 -0
- package/tests/setup.ts +1 -0
- package/tsconfig.json +14 -0
- package/tsup.config.ts +23 -0
- package/vite.config.ts +24 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { Meta } from "@storybook/react";
|
|
2
|
+
|
|
3
|
+
import { useListItem } from "@floating-ui/react";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
|
|
6
|
+
import clsx from "clsx";
|
|
7
|
+
import { action } from "@storybook/addon-actions";
|
|
8
|
+
|
|
9
|
+
import { SelectValue } from "./Select";
|
|
10
|
+
import { useSelect, Select, SelectSelectionProps, Option } from ".";
|
|
11
|
+
|
|
12
|
+
const onChangeAction = action("onChange");
|
|
13
|
+
|
|
14
|
+
const meta = {
|
|
15
|
+
title: "Components/Select",
|
|
16
|
+
component: Select,
|
|
17
|
+
} satisfies Meta<typeof Select>;
|
|
18
|
+
export default meta;
|
|
19
|
+
|
|
20
|
+
const departments = [
|
|
21
|
+
{ value: "38", label: "Isère" },
|
|
22
|
+
{ value: "74", label: "Haute-Savoie" },
|
|
23
|
+
{ value: "73", label: "Savoie" },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const townsByDepartment = {
|
|
27
|
+
"38": [
|
|
28
|
+
{ value: "grenoble", label: "Grenoble" },
|
|
29
|
+
{ value: "meylan", label: "Meylan" },
|
|
30
|
+
{ value: "voreppe", label: "Voreppe" },
|
|
31
|
+
],
|
|
32
|
+
"73": [
|
|
33
|
+
{ value: "chambery", label: "Chambéry" },
|
|
34
|
+
{ value: "albertville", label: "Albertville" },
|
|
35
|
+
],
|
|
36
|
+
"74": [
|
|
37
|
+
{ value: "annecy", label: "Annecy" },
|
|
38
|
+
{ value: "annemasse", label: "Annemasse" },
|
|
39
|
+
{ value: "avoriaz", label: "Avoriaz" },
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const options = [
|
|
44
|
+
{ value: "abbeville", label: "Abbeville" },
|
|
45
|
+
{ value: "agde", label: "Agde" },
|
|
46
|
+
{ value: "agen", label: "Agen" },
|
|
47
|
+
{ value: "aixenprovence", label: "Aix-en-Provence" },
|
|
48
|
+
{ value: "ajaccio", label: "Ajaccio" },
|
|
49
|
+
{ value: "albi", label: "Albi" },
|
|
50
|
+
// { value: "alencon", label: "Alençon" },
|
|
51
|
+
// { value: "amiens", label: "Amiens" },
|
|
52
|
+
// { value: "angers", label: "Angers" },
|
|
53
|
+
// { value: "angouleme", label: "Angoulême" },
|
|
54
|
+
// { value: "annonay", label: "Annonay" },
|
|
55
|
+
// { value: "antibes", label: "Antibes" },
|
|
56
|
+
// { value: "arcachon", label: "Arcachon" },
|
|
57
|
+
// { value: "arles", label: "Arles" },
|
|
58
|
+
// { value: "arras", label: "Arras" },
|
|
59
|
+
// { value: "asnieres-sur-seine", label: "Asnières-sur-Seine" },
|
|
60
|
+
// { value: "aubagne", label: "Aubagne" },
|
|
61
|
+
// { value: "aubervilliers", label: "Aubervilliers" },
|
|
62
|
+
// { value: "aulnay-sous-bois", label: "Aulnay-sous-Bois" },
|
|
63
|
+
// { value: "avignon", label: "Avignon" },
|
|
64
|
+
// { value: "avranches", label: "Avranches" },
|
|
65
|
+
// { value: "avoriaz", label: "Avoriaz" },
|
|
66
|
+
// { value: "avray", label: "Avray" },
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
export const Basic = () => {
|
|
70
|
+
const [value, setValue] = useState<SelectValue>(null);
|
|
71
|
+
return (
|
|
72
|
+
<div className="grid gap-2 grid-cols-1">
|
|
73
|
+
<Select
|
|
74
|
+
searchable={false}
|
|
75
|
+
placeholder="Select your town..."
|
|
76
|
+
options={options}
|
|
77
|
+
value={value}
|
|
78
|
+
onChange={(o) => {
|
|
79
|
+
onChangeAction(o);
|
|
80
|
+
setValue(o.target.value);
|
|
81
|
+
}}
|
|
82
|
+
></Select>
|
|
83
|
+
<Select
|
|
84
|
+
variant="ghost"
|
|
85
|
+
searchable={false}
|
|
86
|
+
placeholder="Select your town..."
|
|
87
|
+
options={options}
|
|
88
|
+
value={value}
|
|
89
|
+
onChange={(o) => {
|
|
90
|
+
onChangeAction(o);
|
|
91
|
+
setValue(o.target.value);
|
|
92
|
+
}}
|
|
93
|
+
></Select>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const NotRequired = () => {
|
|
99
|
+
const [value, setValue] = useState<SelectValue>(null);
|
|
100
|
+
return (
|
|
101
|
+
<Select
|
|
102
|
+
required={false}
|
|
103
|
+
placeholder="Select your town..."
|
|
104
|
+
options={options}
|
|
105
|
+
value={value}
|
|
106
|
+
onChange={(o) => {
|
|
107
|
+
onChangeAction(o);
|
|
108
|
+
setValue(o.target.value);
|
|
109
|
+
}}
|
|
110
|
+
></Select>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const Searchable = () => {
|
|
115
|
+
const [value, setValue] = useState<SelectValue>(null);
|
|
116
|
+
return (
|
|
117
|
+
<Select
|
|
118
|
+
searchable={true}
|
|
119
|
+
placeholder="Select your town..."
|
|
120
|
+
options={options}
|
|
121
|
+
value={value}
|
|
122
|
+
onChange={(o) => {
|
|
123
|
+
onChangeAction(o);
|
|
124
|
+
setValue(o.target.value);
|
|
125
|
+
}}
|
|
126
|
+
></Select>
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// export const Multiple = () => {
|
|
131
|
+
// const [values, setValues] = useState<string[]>([]);
|
|
132
|
+
// return (
|
|
133
|
+
// <Select
|
|
134
|
+
// multiple={true}
|
|
135
|
+
// placeholder="Select your towns..."
|
|
136
|
+
// options={options}
|
|
137
|
+
// value={values}
|
|
138
|
+
// onChange={(options) => {
|
|
139
|
+
// onChangeAction(options);
|
|
140
|
+
// setValues(options.map((o) => o.value));
|
|
141
|
+
// }}
|
|
142
|
+
// ></Select>
|
|
143
|
+
// );
|
|
144
|
+
// };
|
|
145
|
+
|
|
146
|
+
function isDepartment(department: number | string | null): department is "38" | "73" | "74" {
|
|
147
|
+
return department !== null && ["38", "73", "74"].indexOf(department.toString()) !== -1;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export const Dynamic = () => {
|
|
151
|
+
const [town, setTown] = useState<SelectValue>(null);
|
|
152
|
+
const [department, setDepartment] = useState<SelectValue>(null);
|
|
153
|
+
return (
|
|
154
|
+
<>
|
|
155
|
+
<div className="flex flex-col gap-2">
|
|
156
|
+
<Select
|
|
157
|
+
required={false}
|
|
158
|
+
placeholder="Select your department..."
|
|
159
|
+
options={departments}
|
|
160
|
+
value={department}
|
|
161
|
+
onChange={(o) => {
|
|
162
|
+
onChangeAction(o);
|
|
163
|
+
setDepartment(o.target.value);
|
|
164
|
+
setTown(null);
|
|
165
|
+
}}
|
|
166
|
+
></Select>
|
|
167
|
+
{isDepartment(department) && (
|
|
168
|
+
<Select
|
|
169
|
+
required={false}
|
|
170
|
+
placeholder="Select your town..."
|
|
171
|
+
options={townsByDepartment[department] ?? []}
|
|
172
|
+
value={town}
|
|
173
|
+
onChange={(o) => {
|
|
174
|
+
onChangeAction(o);
|
|
175
|
+
setTown(o.target.value);
|
|
176
|
+
}}
|
|
177
|
+
></Select>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
</>
|
|
181
|
+
);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
type StarOption = Option & {
|
|
185
|
+
icon: string;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const stars: StarOption[] = [
|
|
189
|
+
{ value: "empty", label: "Empty", icon: "fe-star-empty" },
|
|
190
|
+
{ value: "half", label: "Half", icon: "fe-star-half" },
|
|
191
|
+
{ value: "fill", label: "Fill", icon: "fe-star" },
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
function SelectOptionComponent({ icon, label }: StarOption) {
|
|
195
|
+
const { activeIndex, selectedIndex, getItemProps, handleSelect } = useSelect();
|
|
196
|
+
|
|
197
|
+
const { ref, index } = useListItem({ label });
|
|
198
|
+
const isActive = activeIndex === index;
|
|
199
|
+
const isSelected = selectedIndex === index;
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<button
|
|
203
|
+
className={clsx("option", isSelected && "bg-gray-2", isActive && "bg-gray-1")}
|
|
204
|
+
ref={ref}
|
|
205
|
+
role="option"
|
|
206
|
+
aria-selected={isActive && isSelected}
|
|
207
|
+
tabIndex={isActive ? 0 : -1}
|
|
208
|
+
{...getItemProps({
|
|
209
|
+
onClick: () => handleSelect(index),
|
|
210
|
+
})}
|
|
211
|
+
>
|
|
212
|
+
<i className={icon}></i> {label}
|
|
213
|
+
</button>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function SelectSelectionComponent({ label, icon }: SelectSelectionProps<StarOption>) {
|
|
218
|
+
return label ? (
|
|
219
|
+
<span>
|
|
220
|
+
<i className={icon}></i>
|
|
221
|
+
</span>
|
|
222
|
+
) : (
|
|
223
|
+
<span>?</span>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export const CustomRenderer = () => {
|
|
228
|
+
const [value, setValue] = useState<SelectValue>("fill");
|
|
229
|
+
return (
|
|
230
|
+
<>
|
|
231
|
+
<Select
|
|
232
|
+
showArrow={false}
|
|
233
|
+
selectionClassName="ml-auto"
|
|
234
|
+
width="37px"
|
|
235
|
+
placement="bottom-end"
|
|
236
|
+
searchable={false}
|
|
237
|
+
options={stars}
|
|
238
|
+
value={value}
|
|
239
|
+
onChange={(o) => {
|
|
240
|
+
onChangeAction(o);
|
|
241
|
+
setValue(o.target.value);
|
|
242
|
+
}}
|
|
243
|
+
selectSelectionComponent={SelectSelectionComponent}
|
|
244
|
+
selectOptionComponent={SelectOptionComponent}
|
|
245
|
+
></Select>
|
|
246
|
+
</>
|
|
247
|
+
);
|
|
248
|
+
};
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChangeEvent,
|
|
3
|
+
ReactElement,
|
|
4
|
+
ReactNode,
|
|
5
|
+
Ref,
|
|
6
|
+
forwardRef,
|
|
7
|
+
useCallback,
|
|
8
|
+
useEffect,
|
|
9
|
+
useMemo,
|
|
10
|
+
useRef,
|
|
11
|
+
useState,
|
|
12
|
+
} from "react";
|
|
13
|
+
import {
|
|
14
|
+
FloatingFocusManager,
|
|
15
|
+
FloatingList,
|
|
16
|
+
FloatingPortal,
|
|
17
|
+
Placement,
|
|
18
|
+
autoUpdate,
|
|
19
|
+
flip,
|
|
20
|
+
offset,
|
|
21
|
+
size,
|
|
22
|
+
useClick,
|
|
23
|
+
useDismiss,
|
|
24
|
+
useFloating,
|
|
25
|
+
useInteractions,
|
|
26
|
+
useListNavigation,
|
|
27
|
+
useMergeRefs,
|
|
28
|
+
useRole,
|
|
29
|
+
useTypeahead,
|
|
30
|
+
} from "@floating-ui/react";
|
|
31
|
+
import { SelectContext } from "./useSelectContext.ts";
|
|
32
|
+
import SelectSelection, { SelectSelectionProps } from "./SelectSelection.tsx";
|
|
33
|
+
|
|
34
|
+
import type { Option } from "./interface";
|
|
35
|
+
import clsx from "clsx";
|
|
36
|
+
import { Input } from "../input";
|
|
37
|
+
import { Button } from "../button";
|
|
38
|
+
import { useEventCallback } from "~/hooks";
|
|
39
|
+
import { Dialog } from "../dialog";
|
|
40
|
+
|
|
41
|
+
export type SelectValue = number | string | null;
|
|
42
|
+
type ChangeEventLike = {
|
|
43
|
+
target: {
|
|
44
|
+
value: SelectValue;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type Props<O extends Option = Option> = {
|
|
49
|
+
disabled?: boolean;
|
|
50
|
+
variant?: "normal" | "ghost";
|
|
51
|
+
showArrow?: boolean;
|
|
52
|
+
selectionClassName?: string;
|
|
53
|
+
width?: number | string;
|
|
54
|
+
floatingMinWidth?: number;
|
|
55
|
+
placement?: Placement;
|
|
56
|
+
options: O[];
|
|
57
|
+
placeholder?: string;
|
|
58
|
+
getSearchableValue?: (matchReg: RegExp, option: O) => string;
|
|
59
|
+
searchable?: boolean;
|
|
60
|
+
required?: boolean;
|
|
61
|
+
selectOptionComponent?: (props: O) => ReactNode;
|
|
62
|
+
selectSelectionComponent?: (props: SelectSelectionProps<O>) => ReactNode;
|
|
63
|
+
value?: SelectValue;
|
|
64
|
+
onChange?: ((e: ChangeEventLike) => void) | null;
|
|
65
|
+
zIndex?: number;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
function defaultGetSearchableValue(matchReg: RegExp, option: Option) {
|
|
69
|
+
return matchReg.test(option.label.toLowerCase().trim());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const Select = forwardRef<HTMLDivElement, Props>(
|
|
73
|
+
(
|
|
74
|
+
{
|
|
75
|
+
variant = "normal",
|
|
76
|
+
disabled = false,
|
|
77
|
+
showArrow = true,
|
|
78
|
+
selectionClassName,
|
|
79
|
+
width = "100%",
|
|
80
|
+
floatingMinWidth = 130,
|
|
81
|
+
placement = "bottom",
|
|
82
|
+
searchable = false,
|
|
83
|
+
required = true,
|
|
84
|
+
value = null,
|
|
85
|
+
onChange = null,
|
|
86
|
+
placeholder = "Select ...",
|
|
87
|
+
getSearchableValue,
|
|
88
|
+
selectSelectionComponent: SelectSelectionCustomComponent,
|
|
89
|
+
selectOptionComponent: SelectOptionCustomComponent,
|
|
90
|
+
options = [],
|
|
91
|
+
zIndex,
|
|
92
|
+
},
|
|
93
|
+
propRef,
|
|
94
|
+
) => {
|
|
95
|
+
const isControlled = onChange !== null;
|
|
96
|
+
|
|
97
|
+
const onChangeStable = useEventCallback(onChange);
|
|
98
|
+
|
|
99
|
+
const [uncontrolledSelectedIndex, setUncontrolledSelectedIndex] = useState<number | null>(null);
|
|
100
|
+
const [search, setSearch] = useState("");
|
|
101
|
+
|
|
102
|
+
const filteredOptions = useMemo(
|
|
103
|
+
() =>
|
|
104
|
+
options.filter((option) => {
|
|
105
|
+
if (!searchable || search.trim() === "") {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
const fn = getSearchableValue ?? defaultGetSearchableValue;
|
|
109
|
+
const matchReg = new RegExp(search.toLowerCase().trim());
|
|
110
|
+
return fn(matchReg, option);
|
|
111
|
+
}),
|
|
112
|
+
[searchable, search, getSearchableValue, options],
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
let selectedIndex: number | null = null;
|
|
116
|
+
if (isControlled) {
|
|
117
|
+
if (value !== null) {
|
|
118
|
+
const pos = filteredOptions.findIndex((o) => o.value === value);
|
|
119
|
+
if (pos !== -1) {
|
|
120
|
+
selectedIndex = pos;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
selectedIndex = uncontrolledSelectedIndex;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
128
|
+
const [activeIndex, setActiveIndex] = useState<number | null>(null);
|
|
129
|
+
const [searchHasFocus, setSearchHasFocus] = useState(false);
|
|
130
|
+
|
|
131
|
+
const SelectSelectionComponent = SelectSelectionCustomComponent ?? SelectSelection;
|
|
132
|
+
|
|
133
|
+
function handleSearchChange(e: ChangeEvent<HTMLInputElement>) {
|
|
134
|
+
setActiveIndex(null);
|
|
135
|
+
setSearch(e.target.value);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const { refs, floatingStyles, context } = useFloating({
|
|
139
|
+
placement,
|
|
140
|
+
open: isOpen,
|
|
141
|
+
onOpenChange: setIsOpen,
|
|
142
|
+
whileElementsMounted: autoUpdate,
|
|
143
|
+
middleware: [
|
|
144
|
+
offset(5),
|
|
145
|
+
flip({ padding: 10 }),
|
|
146
|
+
size({
|
|
147
|
+
apply({ rects, elements, availableHeight }) {
|
|
148
|
+
Object.assign(elements.floating.style, {
|
|
149
|
+
width: `${Math.max(floatingMinWidth, rects.reference.width)}px`,
|
|
150
|
+
});
|
|
151
|
+
const firstChild = elements.floating.firstElementChild as HTMLElement;
|
|
152
|
+
if (firstChild) {
|
|
153
|
+
firstChild.style.maxHeight = `${Math.min(availableHeight, 300)}px`;
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
padding: 10,
|
|
157
|
+
}),
|
|
158
|
+
],
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const ref = useMergeRefs([refs.setReference, propRef]);
|
|
162
|
+
|
|
163
|
+
const listRef = useRef<Array<HTMLElement | null>>([]);
|
|
164
|
+
const labelsRef = useRef<Array<string | null>>([]);
|
|
165
|
+
const isTypingRef = useRef(false);
|
|
166
|
+
|
|
167
|
+
const click = useClick(context, { event: "mousedown" });
|
|
168
|
+
const dismiss = useDismiss(context);
|
|
169
|
+
const role = useRole(context, { role: "listbox" });
|
|
170
|
+
const listNav = useListNavigation(context, {
|
|
171
|
+
listRef,
|
|
172
|
+
activeIndex,
|
|
173
|
+
focusItemOnHover: searchable ? false : true,
|
|
174
|
+
selectedIndex,
|
|
175
|
+
onNavigate: setActiveIndex,
|
|
176
|
+
loop: true,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const typeahead = useTypeahead(context, {
|
|
180
|
+
enabled: !searchHasFocus,
|
|
181
|
+
listRef: labelsRef,
|
|
182
|
+
activeIndex,
|
|
183
|
+
selectedIndex,
|
|
184
|
+
onMatch: isOpen ? setActiveIndex : undefined,
|
|
185
|
+
onTypingChange(isTyping) {
|
|
186
|
+
isTypingRef.current = isTyping;
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([
|
|
191
|
+
listNav,
|
|
192
|
+
typeahead,
|
|
193
|
+
click,
|
|
194
|
+
dismiss,
|
|
195
|
+
role,
|
|
196
|
+
]);
|
|
197
|
+
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
if (SelectOptionCustomComponent) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
filteredOptions.map((option, i) => {
|
|
203
|
+
labelsRef.current[i] = option.label;
|
|
204
|
+
});
|
|
205
|
+
}, [search, filteredOptions, SelectOptionCustomComponent]);
|
|
206
|
+
|
|
207
|
+
const handleSelect = useCallback(
|
|
208
|
+
(index: number | null) => {
|
|
209
|
+
setIsOpen(false);
|
|
210
|
+
setSearch("");
|
|
211
|
+
if (isControlled) {
|
|
212
|
+
const event: ChangeEventLike = {
|
|
213
|
+
target: {
|
|
214
|
+
value: index === null ? null : filteredOptions[index].value,
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
onChangeStable(event);
|
|
218
|
+
} else {
|
|
219
|
+
setUncontrolledSelectedIndex(index);
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
[isControlled, onChangeStable, filteredOptions],
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const selectContext = useMemo(
|
|
226
|
+
() => ({
|
|
227
|
+
activeIndex,
|
|
228
|
+
selectedIndex,
|
|
229
|
+
getItemProps,
|
|
230
|
+
handleSelect,
|
|
231
|
+
}),
|
|
232
|
+
[activeIndex, selectedIndex, getItemProps, handleSelect],
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const showCancelButton = !required && selectedIndex !== null;
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<div>
|
|
239
|
+
<div
|
|
240
|
+
className={clsx(
|
|
241
|
+
// "selection",
|
|
242
|
+
"rounded-2xl outline outline-1 outline-offset-[-1px] hover:outline-gray-3 flex focus-full:outline-yellow-4 focus-full:outline-2 cursor-pointer",
|
|
243
|
+
variant === "normal" ? "outline-gray-2" : "outline-transparent",
|
|
244
|
+
selectionClassName,
|
|
245
|
+
isOpen && "focus",
|
|
246
|
+
disabled && "disabled",
|
|
247
|
+
)}
|
|
248
|
+
ref={ref}
|
|
249
|
+
tabIndex={0}
|
|
250
|
+
style={{
|
|
251
|
+
width: typeof width === "number" ? `${width}px` : width,
|
|
252
|
+
}}
|
|
253
|
+
{...getReferenceProps()}
|
|
254
|
+
>
|
|
255
|
+
<span className={clsx("h-8 flex-1 flex items-center px-2")}>
|
|
256
|
+
{selectedIndex !== null ? (
|
|
257
|
+
<SelectSelectionComponent {...filteredOptions[selectedIndex]} key={selectedIndex} />
|
|
258
|
+
) : (
|
|
259
|
+
placeholder
|
|
260
|
+
)}
|
|
261
|
+
</span>
|
|
262
|
+
{showCancelButton && (
|
|
263
|
+
<Button
|
|
264
|
+
withRipple={false}
|
|
265
|
+
icon
|
|
266
|
+
variant="text"
|
|
267
|
+
color="gray"
|
|
268
|
+
onMouseDown={(e) => {
|
|
269
|
+
// we don't want dropdown to open
|
|
270
|
+
e.stopPropagation();
|
|
271
|
+
}}
|
|
272
|
+
onClick={() => {
|
|
273
|
+
handleSelect(null);
|
|
274
|
+
}}
|
|
275
|
+
>
|
|
276
|
+
<i className="fe-cancel"></i>
|
|
277
|
+
</Button>
|
|
278
|
+
)}
|
|
279
|
+
{!showCancelButton && showArrow && (
|
|
280
|
+
<Button color="gray" withRipple={false} icon variant="text" focusable={false}>
|
|
281
|
+
<i className={isOpen ? "fe-angle-up" : "fe-angle-down"}></i>
|
|
282
|
+
</Button>
|
|
283
|
+
)}
|
|
284
|
+
</div>
|
|
285
|
+
<FloatingPortal>
|
|
286
|
+
<SelectContext.Provider value={selectContext}>
|
|
287
|
+
{isOpen && (
|
|
288
|
+
<FloatingFocusManager context={context} modal={false}>
|
|
289
|
+
<div
|
|
290
|
+
className="z-dialog outline-none"
|
|
291
|
+
ref={refs.setFloating}
|
|
292
|
+
style={{
|
|
293
|
+
...floatingStyles,
|
|
294
|
+
zIndex,
|
|
295
|
+
}}
|
|
296
|
+
{...getFloatingProps()}
|
|
297
|
+
>
|
|
298
|
+
<Dialog
|
|
299
|
+
placement={context.placement}
|
|
300
|
+
className="animate-fade-in-list max-h-80 overflow-auto"
|
|
301
|
+
>
|
|
302
|
+
{searchable && (
|
|
303
|
+
<div className="p-2">
|
|
304
|
+
<Input
|
|
305
|
+
placeholder="Search"
|
|
306
|
+
tabIndex={selectedIndex === null ? 0 : -1}
|
|
307
|
+
value={search}
|
|
308
|
+
onChange={handleSearchChange}
|
|
309
|
+
onFocus={() => setSearchHasFocus(true)}
|
|
310
|
+
onBlur={() => setSearchHasFocus(false)}
|
|
311
|
+
></Input>
|
|
312
|
+
</div>
|
|
313
|
+
)}
|
|
314
|
+
{SelectOptionCustomComponent ? (
|
|
315
|
+
<FloatingList elementsRef={listRef} labelsRef={labelsRef}>
|
|
316
|
+
{filteredOptions.map((option) => (
|
|
317
|
+
<SelectOptionCustomComponent {...option} key={option.value} />
|
|
318
|
+
))}
|
|
319
|
+
</FloatingList>
|
|
320
|
+
) : (
|
|
321
|
+
filteredOptions.map((option, index) => {
|
|
322
|
+
const isActive = activeIndex === index;
|
|
323
|
+
const isSelected = selectedIndex === index;
|
|
324
|
+
|
|
325
|
+
return (
|
|
326
|
+
<button
|
|
327
|
+
key={option.value}
|
|
328
|
+
className={clsx(
|
|
329
|
+
"option",
|
|
330
|
+
isSelected && "bg-gray-2",
|
|
331
|
+
isActive && "bg-gray-1",
|
|
332
|
+
)}
|
|
333
|
+
role="option"
|
|
334
|
+
aria-selected={isActive && isSelected}
|
|
335
|
+
tabIndex={isActive ? 0 : -1}
|
|
336
|
+
ref={(node) => {
|
|
337
|
+
listRef.current[index] = node;
|
|
338
|
+
}}
|
|
339
|
+
{...getItemProps({
|
|
340
|
+
onClick: () => handleSelect(index),
|
|
341
|
+
})}
|
|
342
|
+
>
|
|
343
|
+
{option.label}
|
|
344
|
+
</button>
|
|
345
|
+
);
|
|
346
|
+
})
|
|
347
|
+
)}
|
|
348
|
+
</Dialog>
|
|
349
|
+
</div>
|
|
350
|
+
</FloatingFocusManager>
|
|
351
|
+
)}
|
|
352
|
+
</SelectContext.Provider>
|
|
353
|
+
</FloatingPortal>
|
|
354
|
+
</div>
|
|
355
|
+
);
|
|
356
|
+
},
|
|
357
|
+
|
|
358
|
+
// see : React with Typescript -- Generics while using React.forwardRef
|
|
359
|
+
// https://stackoverflow.com/questions/58469229/react-with-typescript-generics-while-using-react-forwardref
|
|
360
|
+
) as <O extends Option>(p: Props<O> & { ref?: Ref<HTMLDivElement> }) => ReactElement;
|
|
361
|
+
|
|
362
|
+
export default Select;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useListItem } from "@floating-ui/react";
|
|
2
|
+
import { useSelect } from ".";
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import { Option } from "./interface";
|
|
5
|
+
|
|
6
|
+
type Props<O extends Option> = O;
|
|
7
|
+
|
|
8
|
+
export default function SelectOption<O extends Option>({ label }: Props<O>) {
|
|
9
|
+
const { activeIndex, selectedIndex, getItemProps, handleSelect } = useSelect();
|
|
10
|
+
|
|
11
|
+
const { ref, index } = useListItem({ label });
|
|
12
|
+
const isActive = activeIndex === index;
|
|
13
|
+
const isSelected = selectedIndex === index;
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<button
|
|
17
|
+
className={clsx("option", isSelected && "bg-gray-2", isActive && "bg-gray-1")}
|
|
18
|
+
ref={ref}
|
|
19
|
+
role="option"
|
|
20
|
+
aria-selected={isActive && isSelected}
|
|
21
|
+
tabIndex={isActive ? 0 : -1}
|
|
22
|
+
{...getItemProps({
|
|
23
|
+
onClick: () => handleSelect(index),
|
|
24
|
+
})}
|
|
25
|
+
>
|
|
26
|
+
{label}
|
|
27
|
+
</button>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import Badge from "../badge/Badge";
|
|
2
|
+
import type { Option } from "./interface";
|
|
3
|
+
|
|
4
|
+
export type SelectSelectionProps<O extends Option> = O & {
|
|
5
|
+
multiple?: boolean;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export default function SelectSelection<O extends Option>({
|
|
9
|
+
label,
|
|
10
|
+
multiple = false,
|
|
11
|
+
}: SelectSelectionProps<O>) {
|
|
12
|
+
return multiple ? <Badge className="ml-1">{label}</Badge> : <span>{label}</span>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { default as SelectOption } from "./SelectOption";
|
|
2
|
+
export { default as SelectSelection, type SelectSelectionProps } from "./SelectSelection";
|
|
3
|
+
export { default as Select } from "./Select";
|
|
4
|
+
export { default as useSelect, SelectContext } from "./useSelectContext";
|
|
5
|
+
export type * from "./interface";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { BBox, Geometry } from "geojson";
|
|
2
|
+
|
|
3
|
+
export type OptionLike = Option | GeoOption;
|
|
4
|
+
|
|
5
|
+
export type Option = {
|
|
6
|
+
type?: "Option";
|
|
7
|
+
value: string;
|
|
8
|
+
label: string;
|
|
9
|
+
sourceId?: string | number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type NoDataOption = {
|
|
13
|
+
id: string;
|
|
14
|
+
type: "nodata";
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** For compatibility with OptionLike. get original types from pentatrion-geo */
|
|
18
|
+
type GeoOption<G extends Geometry | null = Geometry, T extends string = string> = {
|
|
19
|
+
id: string;
|
|
20
|
+
type: "Feature";
|
|
21
|
+
properties: FeatureProperties<T>;
|
|
22
|
+
geometry: G;
|
|
23
|
+
bbox?: BBox | undefined;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type FeatureProperties<T extends string = string> = {
|
|
27
|
+
id: string;
|
|
28
|
+
|
|
29
|
+
/** computed name + short context for input string */
|
|
30
|
+
label: string;
|
|
31
|
+
name: string;
|
|
32
|
+
context: string | null;
|
|
33
|
+
score: number;
|
|
34
|
+
type: T;
|
|
35
|
+
originalProperties?: any;
|
|
36
|
+
};
|