pentatrion-design 0.0.4 → 0.0.6

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.
Files changed (204) hide show
  1. package/README.md +18 -29
  2. package/components/autocomplete/Autocomplete.stories.tsx +63 -0
  3. package/components/autocomplete/Autocomplete.tsx +334 -0
  4. package/components/autocomplete/AutocompleteOption.tsx +35 -0
  5. package/components/autocomplete/LazyAutocomplete.tsx +132 -0
  6. package/components/autocomplete/SimpleAutocomplete.tsx +74 -0
  7. package/components/autocomplete/_fixtures.tsx +27 -0
  8. package/components/autocomplete/index.ts +6 -0
  9. package/components/autocomplete/useAutocompleteContext.ts +20 -0
  10. package/components/autocomplete/util.ts +19 -0
  11. package/components/badge/Badge.stories.tsx +72 -0
  12. package/components/badge/Badge.tsx +73 -0
  13. package/components/badge/index.ts +1 -0
  14. package/components/button/Button.stories.tsx +190 -0
  15. package/components/button/Button.test.tsx +20 -0
  16. package/components/button/Button.tsx +209 -0
  17. package/components/button/ButtonGroup.stories.tsx +46 -0
  18. package/components/button/ButtonGroup.tsx +29 -0
  19. package/components/button/LinkButton.tsx +89 -0
  20. package/components/button/index.ts +3 -0
  21. package/components/card/Card.stories.tsx +21 -0
  22. package/components/card/config.ts +4 -0
  23. package/components/card/index.ts +1 -0
  24. package/components/code/Code.tsx +15 -0
  25. package/components/code/index.ts +1 -0
  26. package/components/context-menu/ContextMenu.stories.tsx +22 -0
  27. package/components/context-menu/ContextMenu.tsx +185 -0
  28. package/components/context-menu/ContextMenuItem.tsx +32 -0
  29. package/components/context-menu/index.ts +2 -0
  30. package/components/dialog/Dialog.stories.tsx +270 -0
  31. package/components/dialog/Dialog.tsx +46 -0
  32. package/components/dialog/index.ts +1 -0
  33. package/components/dialog/util.ts +29 -0
  34. package/components/dropdown-menu/DropdownMenu.stories.tsx +19 -0
  35. package/components/dropdown-menu/DropdownMenu.tsx +12 -0
  36. package/components/dropdown-menu/DropdownMenuContent.tsx +52 -0
  37. package/components/dropdown-menu/DropdownMenuItem.tsx +33 -0
  38. package/components/dropdown-menu/DropdownMenuTrigger.tsx +40 -0
  39. package/components/dropdown-menu/index.ts +7 -0
  40. package/components/dropdown-menu/interface.d.ts +13 -0
  41. package/components/dropdown-menu/useDropdownMenu.tsx +115 -0
  42. package/components/dropdown-menu/useDropdownMenuContext.ts +16 -0
  43. package/components/dropdown-menu-nested/DropdownMenuNested.stories.tsx +33 -0
  44. package/components/dropdown-menu-nested/DropdownMenuNested.tsx +17 -0
  45. package/components/dropdown-menu-nested/MenuContext.ts +15 -0
  46. package/components/dropdown-menu-nested/MenuItem.tsx +42 -0
  47. package/components/dropdown-menu-nested/MenuItemWithChildren.tsx +202 -0
  48. package/components/dropdown-menu-nested/index.ts +3 -0
  49. package/components/flash/Flash.stories.tsx +34 -0
  50. package/components/flash/Flash.tsx +24 -0
  51. package/components/flash/index.ts +1 -0
  52. package/components/highlight/Highlight.stories.tsx +142 -0
  53. package/components/highlight/Highlight.tsx +29 -0
  54. package/components/highlight/index.ts +1 -0
  55. package/components/href/Href.stories.tsx +56 -0
  56. package/components/href/Href.tsx +21 -0
  57. package/components/href/index.ts +1 -0
  58. package/components/input/Checkbox.stories.tsx +56 -0
  59. package/components/input/Checkbox.tsx +52 -0
  60. package/components/input/Input.stories.tsx +103 -0
  61. package/components/input/Input.test.tsx +10 -0
  62. package/components/input/Input.tsx +58 -0
  63. package/components/input/RadioGroup.stories.tsx +81 -0
  64. package/components/input/RadioGroup.tsx +84 -0
  65. package/components/input/Range.stories.tsx +102 -0
  66. package/components/input/Range.tsx +135 -0
  67. package/components/input/Toggle.stories.tsx +29 -0
  68. package/components/input/Toggle.tsx +48 -0
  69. package/components/input/index.ts +5 -0
  70. package/components/input-field/InputField.stories.tsx +253 -0
  71. package/components/input-field/InputField.tsx +68 -0
  72. package/components/input-field/index.ts +1 -0
  73. package/components/loader/Loader.stories.tsx +37 -0
  74. package/components/loader/Loader.tsx +54 -0
  75. package/components/loader/index.ts +1 -0
  76. package/components/modal/Components.tsx +75 -0
  77. package/components/modal/Modal.stories.tsx +113 -0
  78. package/components/modal/Modal.tsx +13 -0
  79. package/components/modal/ModalContent.tsx +51 -0
  80. package/components/modal/ModalTrigger.tsx +39 -0
  81. package/components/modal/index.ts +7 -0
  82. package/components/modal/interface.d.ts +10 -0
  83. package/components/modal/useModal.ts +48 -0
  84. package/components/modal/useModalContext.ts +16 -0
  85. package/components/notification/Notification.stories.tsx +51 -0
  86. package/components/notification/NotificationsContext.ts +79 -0
  87. package/components/notification/NotificationsProvider-ssr.tsx +73 -0
  88. package/components/notification/NotificationsProvider.tsx +66 -0
  89. package/components/notification/index.ts +4 -0
  90. package/components/notification/useContextNotifications.tsx +13 -0
  91. package/components/notification/useFetch.ts +21 -0
  92. package/components/popover/Components.tsx +61 -0
  93. package/components/popover/Popover.stories.tsx +46 -0
  94. package/components/popover/Popover.test.tsx +21 -0
  95. package/components/popover/Popover.tsx +12 -0
  96. package/components/popover/PopoverContent.tsx +49 -0
  97. package/components/popover/PopoverTrigger.tsx +40 -0
  98. package/components/popover/index.ts +7 -0
  99. package/components/popover/interface.d.ts +13 -0
  100. package/components/popover/usePopover.tsx +80 -0
  101. package/components/popover/usePopoverContext.ts +16 -0
  102. package/components/portal/Portal.tsx +10 -0
  103. package/components/portal/index.ts +1 -0
  104. package/components/quick-settings/QuickSettings.stories.tsx +182 -0
  105. package/components/resize-area/ResizeArea.stories.css +40 -0
  106. package/components/resize-area/ResizeArea.stories.tsx +30 -0
  107. package/components/resize-area/ResizeArea.tsx +98 -0
  108. package/components/resize-area/index.ts +1 -0
  109. package/components/scroll/Scroll.stories.tsx +81 -0
  110. package/components/scroll/Scroll.tsx +111 -0
  111. package/components/scroll/index.ts +1 -0
  112. package/components/select/Select.stories.tsx +170 -0
  113. package/components/select/Select.test.tsx +19 -0
  114. package/components/select/Select.tsx +364 -0
  115. package/components/select/SelectOption.tsx +29 -0
  116. package/components/select/SelectSelection.tsx +13 -0
  117. package/components/select/_fixtures.tsx +89 -0
  118. package/components/select/index.ts +5 -0
  119. package/components/select/interface.d.ts +35 -0
  120. package/components/select/useSelectContext.ts +19 -0
  121. package/components/select/util.ts +12 -0
  122. package/components/snack/Snack.stories.tsx +33 -0
  123. package/components/snack/Snack.tsx +51 -0
  124. package/components/snack/index.ts +1 -0
  125. package/components/sortable/Sortable.stories.tsx +98 -0
  126. package/components/sortable/Sortable.tsx +37 -0
  127. package/components/sortable/index.ts +2 -0
  128. package/components/sortable/types.d.ts +12 -0
  129. package/components/steps/Step.tsx +41 -0
  130. package/components/steps/Steps.stories.tsx +140 -0
  131. package/components/steps/Steps.tsx +49 -0
  132. package/components/steps/index.ts +3 -0
  133. package/components/steps/util.ts +3 -0
  134. package/components/table/Table.stories.tsx +36 -0
  135. package/components/table/Table.tsx +13 -0
  136. package/components/table/TableBody.tsx +9 -0
  137. package/components/table/TableCell.tsx +16 -0
  138. package/components/table/TableFooter.tsx +9 -0
  139. package/components/table/TableHeader.tsx +14 -0
  140. package/components/table/TableHeaderCell.tsx +14 -0
  141. package/components/table/TableRow.tsx +14 -0
  142. package/components/table/index.ts +7 -0
  143. package/components/tabs/Tabs.stories.tsx +66 -0
  144. package/components/tabs/Tabs.tsx +93 -0
  145. package/components/tabs/index.ts +1 -0
  146. package/components/tooltip/SimpleTooltip.tsx +17 -0
  147. package/components/tooltip/Tooltip.stories.tsx +128 -0
  148. package/components/tooltip/Tooltip.test.tsx +20 -0
  149. package/components/tooltip/Tooltip.tsx +12 -0
  150. package/components/tooltip/TooltipContent.tsx +45 -0
  151. package/components/tooltip/TooltipTrigger.tsx +34 -0
  152. package/components/tooltip/index.ts +6 -0
  153. package/components/tooltip/interface.d.ts +15 -0
  154. package/components/tooltip/useTooltip.ts +87 -0
  155. package/components/tooltip/useTooltipContext.ts +16 -0
  156. package/dist/index.d.ts +20 -3
  157. package/dist/index.js +309 -229
  158. package/hooks/index.ts +11 -0
  159. package/hooks/useCombinedRefs.test.tsx +42 -0
  160. package/hooks/useCombinedRefs.ts +21 -0
  161. package/hooks/useCopyToClipboard.ts +28 -0
  162. package/hooks/useDebounce.ts +59 -0
  163. package/hooks/useEventCallback.ts +13 -0
  164. package/hooks/useEventListener.ts +86 -0
  165. package/hooks/useIsClosing.ts +25 -0
  166. package/hooks/useIsMounted.ts +16 -0
  167. package/hooks/useIsomorphicLayoutEffect.ts +4 -0
  168. package/hooks/useOnClickOutside.ts +35 -0
  169. package/hooks/usePrevious.ts +9 -0
  170. package/hooks/useRipple.test.tsx +52 -0
  171. package/hooks/useRipple.tsx +79 -0
  172. package/lib/arrUtil.ts +8 -0
  173. package/lib/error.ts +18 -0
  174. package/lib/fetch.test.ts +107 -0
  175. package/lib/fetch.ts +115 -0
  176. package/lib/index.ts +4 -0
  177. package/lib/tailwindVariants.ts +35 -0
  178. package/package.json +8 -2
  179. package/redux/index.ts +1 -0
  180. package/redux/notification/NotificationConsumer.tsx +52 -0
  181. package/redux/notification/errorCatcherMiddleware.ts +17 -0
  182. package/redux/notification/index.ts +5 -0
  183. package/redux/notification/notificationSlice.ts +68 -0
  184. package/redux/notification/useReduxNotifications.ts +26 -0
  185. package/styles/Animation.stories.tsx +47 -0
  186. package/styles/Content.tsx +340 -0
  187. package/styles/Icons.stories.tsx +69 -0
  188. package/styles/Shadow.stories.tsx +43 -0
  189. package/styles/Themes.stories.tsx +31 -0
  190. package/styles/Typography.stories.tsx +89 -0
  191. package/tailwind/base.ts +72 -0
  192. package/tailwind/components.ts +315 -0
  193. package/tailwind/index.ts +2 -0
  194. package/tailwind/keyframes.ts +69 -0
  195. package/tailwind/legacy/base.css +189 -0
  196. package/tailwind/legacy/components-input-outline.css +62 -0
  197. package/tailwind/legacy/components-resize-area.css +112 -0
  198. package/tailwind/legacy/components-step.css +161 -0
  199. package/tailwind/legacy/components.css +59 -0
  200. package/tailwind/legacy/utilities-dialog.css +101 -0
  201. package/tailwind/legacy/utilities.css +3 -0
  202. package/tailwind/pentatrionTw.ts +216 -0
  203. package/tailwind/utilities.ts +78 -0
  204. package/tailwind/vars.ts +125 -0
package/README.md CHANGED
@@ -41,18 +41,17 @@ Créer un fichier `tailwind.config.js` et `postcss.config.js`.
41
41
  // tailwind.config.js
42
42
 
43
43
  /** @type {import('tailwindcss').Config} */
44
- export default {
45
- content: [
46
- "./index.html",
47
- "./src/**/*.{js,ts,jsx,tsx}",
48
- ],
44
+ const config = {
45
+ content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
49
46
  darkMode: ["class"],
50
47
  };
51
48
 
49
+ export default config;
50
+
52
51
 
53
52
  // postcss.config.js
54
53
  /** @type {import("postcss-load-config").Config} */
55
- export default {
54
+ const config = {
56
55
  plugins: {
57
56
  "tailwindcss/nesting": {},
58
57
  tailwindcss: {},
@@ -62,11 +61,18 @@ export default {
62
61
  "postcss-input-range": {}
63
62
  },
64
63
  };
64
+ export default config;
65
65
  ```
66
66
 
67
67
  Mettre à jour le fichier `src/index.css`
68
68
  ```css
69
- /* on utilisera des imports pour faciliter l'intégration du design système */
69
+ @tailwind base;
70
+ @tailwind components;
71
+ @tailwind utilities;
72
+
73
+ /**
74
+ * ou bien on utilisera des imports pour faciliter l'intégration du design système
75
+ */
70
76
  @import "tailwindcss/base";
71
77
  @import "tailwindcss/components";
72
78
  @import "tailwindcss/utilities";
@@ -86,10 +92,11 @@ Mettre à jour la configuration de tailwind.
86
92
  + import { pentatrionTw } from "pentatrion-design/tailwind";
87
93
 
88
94
  /** @type {import('tailwindcss').Config} */
89
- export default {
95
+ const config = {
90
96
  content: [
91
97
  "./index.html",
92
98
  "./src/**/*.{js,ts,jsx,tsx}",
99
+ + "./node_modules/pentatrion-design/lib/**/*.{ts,tsx}",
93
100
  + "./node_modules/pentatrion-design/components/**/*.{ts,tsx}",
94
101
  + "./node_modules/pentatrion-design/hooks/**/*.{ts,tsx}",
95
102
  + "./node_modules/pentatrion-design/redux/**/*.{ts,tsx}",
@@ -97,6 +104,9 @@ export default {
97
104
  darkMode: ["class"],
98
105
  + plugins: [pentatrionTw],
99
106
  };
107
+
108
+ export default config;
109
+
100
110
  ```
101
111
 
102
112
  La dépendance `pentatrion-fonts` est optionnelle.
@@ -128,27 +138,6 @@ export default App
128
138
 
129
139
  L'import minimal `import { Button } from "pentatrion-design/components/button";` est plus contraignant et n'est pas nécessaire si votre projet utilise toutes les dépendances de `pentatrion-design`. si par contre votre projet n'utilise que certains composants et n'utilise pas certains dépendances comme `react-sortablejs` il vaut mieux utiliser l'import `pentatrion-design/components/button`. Cela allégera le build (que notre compilateur fasse du tree shaking ou non).
130
140
 
131
- Mettre à jour `src/index.css`
132
- ```diff
133
- /* index.css */
134
- @import "tailwindcss/base";
135
- + @import "pentatrion-design/tailwind/base.css" layer(base);
136
-
137
- @import "tailwindcss/components";
138
- + @import "pentatrion-design/tailwind/components.css" layer(components);
139
- + @import "pentatrion-design/tailwind/components-input-outline.css" layer(components);
140
- + @import "pentatrion-design/tailwind/components-resize-area.css" layer(components);
141
- + @import "pentatrion-design/tailwind/components-step.css" layer(components);
142
-
143
- @import "tailwindcss/utilities";
144
- + @import "pentatrion-design/tailwind/utilities.css" layer(utilities);
145
- + @import "pentatrion-design/tailwind/utilities-dialog.css" layer(utilities);
146
-
147
- /* Optionel */
148
- + @import "pentatrion-fonts/fontello-lonlat";
149
- ```
150
-
151
-
152
141
  ## projet sans TypeScript
153
142
 
154
143
  pour faciliter l'expérience de développement, `pentatrion-design` fait référence par défaut aux fichiers TypeScript non compilés. Si votre projet n'utilise pas TypeScript il faudra faire référence au dossier `dist`
@@ -0,0 +1,63 @@
1
+ import { Meta, ReactRenderer } from "@storybook/react";
2
+
3
+ import { useState } from "react";
4
+ import { NotificationsProvider } from "../notification";
5
+ import { Button } from "../button";
6
+ import { SimpleAutocomplete, Autocomplete } from ".";
7
+ import { Option } from "../select";
8
+ import { PartialStoryFn } from "@storybook/types";
9
+ import { options } from "./_fixtures";
10
+
11
+ const meta = {
12
+ title: "Components/Autocomplete",
13
+ component: Autocomplete,
14
+ decorators: [
15
+ (Story) => (
16
+ <NotificationsProvider>
17
+ <Story />
18
+ </NotificationsProvider>
19
+ ),
20
+ ] as ((story: PartialStoryFn<ReactRenderer, any>) => JSX.Element)[],
21
+ } satisfies Meta<typeof Autocomplete>;
22
+ export default meta;
23
+
24
+ export const Simple = () => {
25
+ const [selection, setSelection] = useState<Option | null>(null);
26
+
27
+ function handleClick(action: "random" | "unknown" | "unselect") {
28
+ switch (action) {
29
+ case "random":
30
+ setSelection(options[Math.floor(Math.random() * options.length)]);
31
+ break;
32
+ case "unknown":
33
+ setSelection({
34
+ value: "unknown",
35
+ label: "Unknown",
36
+ });
37
+ break;
38
+ case "unselect":
39
+ setSelection(null);
40
+ break;
41
+ }
42
+ }
43
+
44
+ return (
45
+ <>
46
+ <SimpleAutocomplete
47
+ options={options}
48
+ selection={selection}
49
+ onChangeSelection={setSelection}
50
+ />
51
+ <div>sélection : {selection && selection.label}</div>
52
+ <div>
53
+ <Button onClick={() => handleClick("random")} className="mr-2">
54
+ select random
55
+ </Button>
56
+ <Button onClick={() => handleClick("unknown")} className="mr-2">
57
+ select Unknown
58
+ </Button>
59
+ <Button onClick={() => handleClick("unselect")}>unselect</Button>
60
+ </div>
61
+ </>
62
+ );
63
+ };
@@ -0,0 +1,334 @@
1
+ import {
2
+ ChangeEvent,
3
+ KeyboardEvent,
4
+ useCallback,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ forwardRef,
9
+ ForwardedRef,
10
+ ReactNode,
11
+ } from "react";
12
+ import type { OptionLike, Option } from "../select";
13
+ import AutocompleteOption, {
14
+ AutocompleteOptionProps,
15
+ } from "./AutocompleteOption.tsx";
16
+ import {
17
+ FloatingFocusManager,
18
+ FloatingList,
19
+ FloatingPortal,
20
+ Placement,
21
+ autoUpdate,
22
+ flip,
23
+ offset,
24
+ size,
25
+ useDismiss,
26
+ useFloating,
27
+ useInteractions,
28
+ useListNavigation,
29
+ useRole,
30
+ } from "@floating-ui/react";
31
+ import { AutocompleteContext } from "./useAutocompleteContext.ts";
32
+ import clsx from "clsx";
33
+ import { useEventCallback } from "../../hooks";
34
+
35
+ import { getOptionLabel, getOptionValue } from "./util.ts";
36
+ import { Button } from "../button";
37
+ import { Dialog } from "../dialog";
38
+ import { Loader } from "../loader";
39
+ import { inputConfig } from "../input/Input.tsx";
40
+ import { ThemeColor } from "../../types.d";
41
+
42
+ export interface AutocompleteProps<O extends OptionLike = Option> {
43
+ className?: string;
44
+
45
+ icon?: boolean | ReactNode;
46
+
47
+ color?: ThemeColor;
48
+
49
+ /**
50
+ * render suffix only if selection
51
+ * ex: add contextuel buttons associated to selection
52
+ */
53
+ selectionSuffix?: ReactNode;
54
+ /**
55
+ * render suffix only if selection and searchValue is empty
56
+ */
57
+ noSearchSuffix?: ReactNode;
58
+
59
+ placement?: Placement;
60
+ placeholder?: string;
61
+ /**
62
+ * search string used as value for <input>
63
+ */
64
+ searchValue: string;
65
+ /**
66
+ * change handler when typing in the input
67
+ * or when clicking on an option (the label value is passed)
68
+ */
69
+ onChangeSearchValue: (value: string, selectionLabel: boolean) => void;
70
+
71
+ selection?: O | null;
72
+ /**
73
+ * select handler for a selection from the autocomplete's dropdown
74
+ * the option object is passed when clicking on an option
75
+ * null value is passed when typing a new character in the input search.
76
+ */
77
+ onChangeSelection: (option: O | null) => void;
78
+ options: O[];
79
+
80
+ autocompleteOptionComponent?: (
81
+ props: AutocompleteOptionProps<O>,
82
+ ) => ReactNode;
83
+
84
+ loading?: boolean;
85
+
86
+ clearSearchButton?: boolean;
87
+
88
+ selectOnFocus?: boolean;
89
+ }
90
+
91
+ function Autocomplete<O extends OptionLike = Option>(
92
+ {
93
+ className,
94
+ icon = true,
95
+ color = "yellow",
96
+ selectionSuffix,
97
+ noSearchSuffix,
98
+ placement = "bottom",
99
+ placeholder = "Search...",
100
+ selection = null,
101
+ onChangeSelection,
102
+ autocompleteOptionComponent,
103
+ loading = false,
104
+ clearSearchButton = false,
105
+ searchValue,
106
+ onChangeSearchValue,
107
+ options,
108
+ selectOnFocus = true,
109
+ }: AutocompleteProps<O>,
110
+ inputRef: ForwardedRef<HTMLInputElement>,
111
+ ) {
112
+ const [isOpen, setIsOpen] = useState(false);
113
+ const [activeIndex, setActiveIndex] = useState<number | null>(null);
114
+
115
+ const onChangeSearchValueStable = useEventCallback(onChangeSearchValue);
116
+ const onChangeSelectionStable = useEventCallback(onChangeSelection);
117
+
118
+ const OptionComponent = autocompleteOptionComponent ?? AutocompleteOption;
119
+
120
+ const listRef = useRef<Array<HTMLElement | null>>([]);
121
+
122
+ const { refs, floatingStyles, context } = useFloating({
123
+ placement,
124
+ open: isOpen,
125
+ onOpenChange: setIsOpen,
126
+ whileElementsMounted: autoUpdate,
127
+ middleware: [
128
+ offset(5),
129
+ flip({ padding: 10 }),
130
+ size({
131
+ apply({ rects, elements, availableHeight }) {
132
+ Object.assign(elements.floating.style, {
133
+ width: `${Math.max(130, rects.reference.width)}px`,
134
+ });
135
+ const firstChild = elements.floating.firstElementChild as HTMLElement;
136
+ if (firstChild) {
137
+ firstChild.style.maxHeight = `${Math.min(availableHeight, 288)}px`;
138
+ }
139
+ },
140
+ padding: 10,
141
+ }),
142
+ ],
143
+ });
144
+
145
+ const role = useRole(context, { role: "listbox" });
146
+ const dismiss = useDismiss(context);
147
+ const listNav = useListNavigation(context, {
148
+ listRef,
149
+ activeIndex,
150
+ onNavigate: setActiveIndex,
151
+ /* we want the real focus to stay on the input, so focus associated to active items is simulated */
152
+ virtual: true,
153
+ loop: true,
154
+ });
155
+
156
+ const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions(
157
+ [role, dismiss, listNav],
158
+ );
159
+
160
+ function handleChangeSearchValue(e: ChangeEvent<HTMLInputElement>) {
161
+ const value = e.target.value;
162
+
163
+ onChangeSearchValueStable(value, false);
164
+
165
+ if (value !== searchValue && selection !== null) {
166
+ onChangeSelectionStable(null);
167
+ setActiveIndex(null);
168
+ return;
169
+ }
170
+
171
+ if (value) {
172
+ setIsOpen(true);
173
+ /**
174
+ * bug whith LazyAutocomplete activeIndex is set to 0 but the options array has immediately 0 elements
175
+ * so focus can't be set to the first Dom element.
176
+ */
177
+ setActiveIndex(listRef.current[0] ? 0 : null);
178
+ } else {
179
+ setIsOpen(false);
180
+ setActiveIndex(null);
181
+ }
182
+ }
183
+
184
+ const handleSelect = useCallback(
185
+ (index: number | null) => {
186
+ if (index === null) {
187
+ onChangeSelectionStable(null);
188
+ } else if (options[index]) {
189
+ const newSelection = options[index];
190
+ onChangeSelectionStable(newSelection);
191
+
192
+ onChangeSearchValueStable(getOptionLabel(newSelection), true);
193
+ }
194
+ setActiveIndex(null);
195
+ setIsOpen(false);
196
+ },
197
+
198
+ [options, onChangeSelectionStable, onChangeSearchValueStable],
199
+ );
200
+
201
+ const autocompleteContext = useMemo(
202
+ () => ({
203
+ activeIndex,
204
+ selection,
205
+ getItemProps,
206
+ handleSelect,
207
+ }),
208
+ [activeIndex, selection, getItemProps, handleSelect],
209
+ );
210
+
211
+ return (
212
+ <div className={className}>
213
+ <div
214
+ className={clsx(inputConfig.container)}
215
+ ref={refs.setReference}
216
+ data-color={color}
217
+ >
218
+ {icon !== false && (
219
+ <div className="relative flex-center">
220
+ {loading && (
221
+ <Loader
222
+ size="medium"
223
+ color="gray"
224
+ className="absolute left-0 top-0"
225
+ />
226
+ )}
227
+ {icon === true ? (
228
+ <span className="h-8 w-8 flex-center">
229
+ <i className="fe-search"></i>
230
+ </span>
231
+ ) : (
232
+ icon
233
+ )}
234
+ </div>
235
+ )}
236
+ <input
237
+ spellCheck="false"
238
+ className={clsx(inputConfig.input, icon === false && "pl-4")}
239
+ ref={inputRef}
240
+ type="search"
241
+ value={searchValue}
242
+ placeholder={placeholder}
243
+ aria-autocomplete="list"
244
+ {...getReferenceProps({
245
+ onFocus() {
246
+ if (selectOnFocus) {
247
+ (document.activeElement as HTMLInputElement)?.select();
248
+ }
249
+ },
250
+ onChange: handleChangeSearchValue,
251
+ onMouseDown() {
252
+ if (options.length === 0) {
253
+ return;
254
+ }
255
+ /**
256
+ * There is a already selection
257
+ * and the only element available is the selection,
258
+ * no need to display the dropdown.
259
+ */
260
+ // if (options.length === 1 && options[0].value === selection?.value) {
261
+ // return;
262
+ // }
263
+ setIsOpen((isOpen) => !isOpen);
264
+ },
265
+ onKeyDown(e: KeyboardEvent<HTMLInputElement>) {
266
+ if (e.key === "Enter" && activeIndex != null) {
267
+ handleSelect(activeIndex);
268
+ }
269
+ },
270
+ })}
271
+ />
272
+ <div className="relative flex-center">
273
+ {selection && selectionSuffix}
274
+ {clearSearchButton &&
275
+ (searchValue.trim() !== "" || selection !== null) && (
276
+ <Button
277
+ withRipple={false}
278
+ icon
279
+ color="gray"
280
+ variant="text"
281
+ onClick={() => {
282
+ setIsOpen(false);
283
+ onChangeSelectionStable(null);
284
+ setActiveIndex(null);
285
+ onChangeSearchValueStable("", true);
286
+ }}
287
+ >
288
+ <i className="fe-cancel"></i>
289
+ </Button>
290
+ )}
291
+ {icon === false && loading && <Loader size="medium" color="gray" />}
292
+ {!selection && searchValue === "" && noSearchSuffix}
293
+ </div>
294
+ </div>
295
+ <FloatingPortal>
296
+ <AutocompleteContext.Provider value={autocompleteContext}>
297
+ {isOpen && (
298
+ <FloatingFocusManager
299
+ disabled={selectOnFocus}
300
+ context={context}
301
+ initialFocus={-1}
302
+ visuallyHiddenDismiss
303
+ >
304
+ <div
305
+ className="z-dialog outline-none"
306
+ ref={refs.setFloating}
307
+ style={floatingStyles}
308
+ {...getFloatingProps()}
309
+ >
310
+ <Dialog
311
+ placement={context.placement}
312
+ className="overflow-auto motion-safe:animate-fade-in-list"
313
+ >
314
+ <FloatingList elementsRef={listRef}>
315
+ {options.map((option) => {
316
+ return (
317
+ <OptionComponent
318
+ {...option}
319
+ key={getOptionValue(option)}
320
+ />
321
+ );
322
+ })}
323
+ </FloatingList>
324
+ </Dialog>
325
+ </div>
326
+ </FloatingFocusManager>
327
+ )}
328
+ </AutocompleteContext.Provider>
329
+ </FloatingPortal>
330
+ </div>
331
+ );
332
+ }
333
+
334
+ export default forwardRef(Autocomplete);
@@ -0,0 +1,35 @@
1
+ import { memo, useId } from "react";
2
+ import { OptionLike } from "../select";
3
+ import { useListItem } from "@floating-ui/react";
4
+ import { useAutocomplete } from ".";
5
+ import clsx from "clsx";
6
+ import { getOptionLabel, getOptionValue } from "./util";
7
+
8
+ export type AutocompleteOptionProps<O extends OptionLike> = O;
9
+
10
+ function AutocompleteOption<O extends OptionLike>(props: AutocompleteOptionProps<O>) {
11
+ const label = getOptionLabel(props);
12
+
13
+ const id = useId();
14
+ const { activeIndex, selection, getItemProps, handleSelect } = useAutocomplete();
15
+ const { ref, index } = useListItem({ label });
16
+ const isActive = activeIndex === index;
17
+ const isSelected = selection ? getOptionValue(selection) === getOptionValue(props) : false;
18
+
19
+ return (
20
+ <div
21
+ className={clsx("option", isSelected ? "bg-gray-2" : isActive && "bg-gray-1")}
22
+ ref={ref}
23
+ role="option"
24
+ id={id}
25
+ aria-selected={isActive}
26
+ {...getItemProps({
27
+ onClick: () => handleSelect(index),
28
+ })}
29
+ >
30
+ {label}
31
+ </div>
32
+ );
33
+ }
34
+
35
+ export default memo(AutocompleteOption);
@@ -0,0 +1,132 @@
1
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
2
+ import { Autocomplete, AutocompleteProps } from ".";
3
+ import { OptionLike, Option } from "../select";
4
+ import { getOptionLabel } from "./util";
5
+ import { useStateDebounce, useEventCallback } from "../../hooks";
6
+
7
+ interface Props<O extends OptionLike = Option>
8
+ // Omit "searchValue" | "onChangeSearchValue" | "options"
9
+ extends Pick<
10
+ AutocompleteProps<O>,
11
+ | "className"
12
+ | "icon"
13
+ | "color"
14
+ | "selectionSuffix"
15
+ | "noSearchSuffix"
16
+ | "placement"
17
+ | "placeholder"
18
+ | "selection"
19
+ | "onChangeSelection"
20
+ | "autocompleteOptionComponent"
21
+ | "loading"
22
+ | "clearSearchButton"
23
+ > {
24
+ onChangeSearchValueCallback: (searchValue: string) => Promise<O[]>;
25
+ debounce?: number;
26
+ }
27
+
28
+ export function LazyAutocomplete<O extends OptionLike = Option>({
29
+ onChangeSearchValueCallback,
30
+ debounce = 5000,
31
+ selection = null,
32
+ onChangeSelection,
33
+ ...rest
34
+ }: Props<O>) {
35
+ const onChangeSelectionStable = useEventCallback(onChangeSelection);
36
+ const onChangeSearchValueCallbackStable = useEventCallback(onChangeSearchValueCallback);
37
+
38
+ const [searchValue, searchValueDebounced, setSearchValue] = useStateDebounce(
39
+ selection ? getOptionLabel(selection) : "",
40
+ debounce,
41
+ );
42
+ // console.log(searchValue, searchValueDebounced);
43
+
44
+ const searchValueRef = useRef(searchValue);
45
+ searchValueRef.current = searchValue;
46
+
47
+ const inputRef = useRef<HTMLInputElement>(null!);
48
+
49
+ const [loading, setLoading] = useState(false);
50
+
51
+ const [options, setOptions] = useState<O[]>([]);
52
+
53
+ const handleChangeSelection = useCallback(
54
+ (selection: O | null) => {
55
+ onChangeSelectionStable(selection);
56
+ if (selection) {
57
+ setOptions([]);
58
+ }
59
+ },
60
+ [onChangeSelectionStable],
61
+ );
62
+
63
+ // side effect, update the searchValue <input /> value when
64
+ // selection come from outside
65
+ useLayoutEffect(() => {
66
+ if (selection === null) {
67
+ if (document.activeElement !== inputRef.current) {
68
+ setSearchValue("", true);
69
+ }
70
+ return;
71
+ }
72
+
73
+ // we need the fresh value of searchValue but we don't
74
+ // want searchValue as useEffect dependency
75
+ const selectionLabel = getOptionLabel(selection);
76
+ if (searchValueRef.current !== selectionLabel) {
77
+ setSearchValue(selectionLabel, true);
78
+ }
79
+ }, [selection, setSearchValue]);
80
+
81
+ /**
82
+ * set new options when searchValueDebounced change
83
+ */
84
+ useEffect(() => {
85
+ let abort = false;
86
+
87
+ // when we're selecting option we change searchValue to be the complete label
88
+ // value but we don't want to fetch new research
89
+ if (selection) {
90
+ return;
91
+ }
92
+
93
+ if (searchValueDebounced.trim() === "") {
94
+ setOptions([]);
95
+ return;
96
+ }
97
+
98
+ setLoading(true);
99
+
100
+ onChangeSearchValueCallbackStable(searchValueDebounced)
101
+ ?.then((newOptions) => {
102
+ setLoading(false);
103
+
104
+ if (!abort) {
105
+ setOptions(newOptions);
106
+ }
107
+ })
108
+ .catch(() => {
109
+ setLoading(false);
110
+ });
111
+
112
+ return () => {
113
+ setLoading(false);
114
+ abort = true;
115
+ };
116
+ }, [selection, searchValueDebounced, onChangeSearchValueCallbackStable]);
117
+
118
+ return (
119
+ <Autocomplete
120
+ ref={inputRef}
121
+ searchValue={searchValue}
122
+ onChangeSearchValue={(newValue, immediate) => {
123
+ setSearchValue(newValue, immediate || newValue === "" ? true : false);
124
+ }}
125
+ selection={selection}
126
+ onChangeSelection={handleChangeSelection}
127
+ options={options}
128
+ loading={loading}
129
+ {...rest}
130
+ />
131
+ );
132
+ }