@stack-spot/citric-react 0.38.0 → 0.39.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/CHANGELOG.md +13 -13
- package/dist/citric.css +2844 -2844
- package/dist/components/Accordion.d.ts +1 -1
- package/dist/components/Accordion.js +1 -1
- package/dist/components/Alert.d.ts +1 -1
- package/dist/components/Alert.js +1 -1
- package/dist/components/AsyncContent.d.ts +1 -1
- package/dist/components/AsyncContent.js +1 -1
- package/dist/components/Avatar.d.ts +1 -1
- package/dist/components/Avatar.js +1 -1
- package/dist/components/AvatarGroup.d.ts +1 -1
- package/dist/components/AvatarGroup.js +1 -1
- package/dist/components/Badge.d.ts +1 -1
- package/dist/components/Badge.js +1 -1
- package/dist/components/Blockquote.d.ts +1 -1
- package/dist/components/Blockquote.js +1 -1
- package/dist/components/Breadcrumb.d.ts +1 -1
- package/dist/components/Breadcrumb.js +1 -1
- package/dist/components/Button.d.ts +1 -1
- package/dist/components/Button.js +1 -1
- package/dist/components/ButtonLink.d.ts +1 -1
- package/dist/components/ButtonLink.js +1 -1
- package/dist/components/Card.d.ts +1 -1
- package/dist/components/Card.js +1 -1
- package/dist/components/Checkbox.d.ts +1 -1
- package/dist/components/Checkbox.js +1 -1
- package/dist/components/CheckboxGroup.d.ts +1 -1
- package/dist/components/CheckboxGroup.d.ts.map +1 -1
- package/dist/components/CheckboxGroup.js +2 -2
- package/dist/components/CheckboxGroup.js.map +1 -1
- package/dist/components/Circle.d.ts +1 -1
- package/dist/components/Circle.js +1 -1
- package/dist/components/Divider.d.ts +1 -1
- package/dist/components/Divider.js +1 -1
- package/dist/components/ErrorBoundary.d.ts +1 -1
- package/dist/components/ErrorBoundary.js +1 -1
- package/dist/components/ErrorMessage.d.ts +1 -1
- package/dist/components/ErrorMessage.js +1 -1
- package/dist/components/FallbackBoundary.d.ts +1 -1
- package/dist/components/FallbackBoundary.js +1 -1
- package/dist/components/Favorite.d.ts +1 -1
- package/dist/components/Favorite.js +1 -1
- package/dist/components/FieldGroup.d.ts +1 -1
- package/dist/components/FieldGroup.js +1 -1
- package/dist/components/Form.d.ts +2 -2
- package/dist/components/Form.js +1 -1
- package/dist/components/FormGroup.d.ts +1 -1
- package/dist/components/FormGroup.js +1 -1
- package/dist/components/Icon.d.ts +1 -1
- package/dist/components/Icon.js +1 -1
- package/dist/components/IconBox.d.ts +3 -3
- package/dist/components/IconBox.js +1 -1
- package/dist/components/ImageBox.d.ts +3 -3
- package/dist/components/ImageBox.js +1 -1
- package/dist/components/ImageWithFallback.d.ts +1 -1
- package/dist/components/ImageWithFallback.js +1 -1
- package/dist/components/Input.d.ts +1 -1
- package/dist/components/Input.js +1 -1
- package/dist/components/Link.d.ts +1 -1
- package/dist/components/Link.js +1 -1
- package/dist/components/LoadingPanel.d.ts +1 -1
- package/dist/components/LoadingPanel.js +1 -1
- package/dist/components/MenuOverlay/Menu.d.ts +1 -1
- package/dist/components/MenuOverlay/Menu.js +1 -1
- package/dist/components/MenuOverlay/index.d.ts +1 -1
- package/dist/components/MenuOverlay/index.js +1 -1
- package/dist/components/Overlay/index.d.ts +4 -1
- package/dist/components/Overlay/index.d.ts.map +1 -1
- package/dist/components/Overlay/index.js +4 -1
- package/dist/components/Overlay/index.js.map +1 -1
- package/dist/components/Pagination.d.ts +1 -1
- package/dist/components/Pagination.js +1 -1
- package/dist/components/ProgressBar.d.ts +1 -1
- package/dist/components/ProgressBar.js +1 -1
- package/dist/components/ProgressCircular.d.ts +1 -1
- package/dist/components/ProgressCircular.js +1 -1
- package/dist/components/RadioGroup.d.ts +1 -1
- package/dist/components/RadioGroup.d.ts.map +1 -1
- package/dist/components/RadioGroup.js +2 -2
- package/dist/components/RadioGroup.js.map +1 -1
- package/dist/components/Rating.d.ts +1 -1
- package/dist/components/Rating.js +1 -1
- package/dist/components/Select/MultiSelect.d.ts +1 -1
- package/dist/components/Select/MultiSelect.js +1 -1
- package/dist/components/Select/RichSelect.d.ts +1 -1
- package/dist/components/Select/RichSelect.js +1 -1
- package/dist/components/Select/SimpleSelect.d.ts +1 -1
- package/dist/components/Select/SimpleSelect.js +1 -1
- package/dist/components/Select/index.d.ts +1 -1
- package/dist/components/Select/index.js +1 -1
- package/dist/components/SelectBox.d.ts +1 -1
- package/dist/components/SelectBox.js +1 -1
- package/dist/components/Skeleton.d.ts +1 -1
- package/dist/components/Skeleton.js +1 -1
- package/dist/components/Slider.d.ts +1 -1
- package/dist/components/Slider.js +1 -1
- package/dist/components/SmartTable.d.ts +1 -1
- package/dist/components/SmartTable.js +1 -1
- package/dist/components/Stepper.d.ts +1 -1
- package/dist/components/Stepper.js +1 -1
- package/dist/components/Table.d.ts +3 -3
- package/dist/components/Table.js +1 -1
- package/dist/components/Tabs/index.d.ts +1 -1
- package/dist/components/Tabs/index.js +1 -1
- package/dist/components/Textarea.d.ts +1 -1
- package/dist/components/Textarea.js +1 -1
- package/dist/components/Tooltip.d.ts +1 -1
- package/dist/components/Tooltip.js +1 -1
- package/dist/context/CitricProvider.d.ts +1 -1
- package/dist/context/CitricProvider.js +1 -1
- package/dist/overlay.js +1 -1
- package/dist/theme.css +415 -415
- package/package.json +1 -1
- package/scripts/build-css.ts +49 -49
- package/src/components/Accordion.tsx +130 -130
- package/src/components/Alert.tsx +24 -24
- package/src/components/AsyncContent.tsx +70 -70
- package/src/components/Avatar.tsx +45 -45
- package/src/components/AvatarGroup.tsx +49 -49
- package/src/components/Badge.tsx +47 -47
- package/src/components/Blockquote.tsx +18 -18
- package/src/components/Breadcrumb.tsx +33 -33
- package/src/components/Button.tsx +105 -105
- package/src/components/ButtonLink.tsx +45 -45
- package/src/components/Card.tsx +68 -68
- package/src/components/Checkbox.tsx +51 -51
- package/src/components/CheckboxGroup.tsx +153 -152
- package/src/components/Circle.tsx +43 -43
- package/src/components/CitricComponent.ts +47 -47
- package/src/components/Divider.tsx +24 -24
- package/src/components/ErrorBoundary.tsx +75 -75
- package/src/components/ErrorMessage.tsx +11 -11
- package/src/components/FallbackBoundary.tsx +40 -40
- package/src/components/Favorite.tsx +57 -57
- package/src/components/FieldGroup.tsx +46 -46
- package/src/components/Form.tsx +36 -36
- package/src/components/FormGroup.tsx +57 -57
- package/src/components/Icon.tsx +35 -35
- package/src/components/IconBox.tsx +134 -134
- package/src/components/ImageBox.tsx +125 -125
- package/src/components/ImageWithFallback.tsx +65 -65
- package/src/components/Input.tsx +49 -49
- package/src/components/Link.tsx +55 -55
- package/src/components/LoadingPanel.tsx +8 -8
- package/src/components/MenuOverlay/Menu.tsx +158 -158
- package/src/components/MenuOverlay/context.ts +20 -20
- package/src/components/MenuOverlay/index.tsx +55 -55
- package/src/components/MenuOverlay/keyboard.ts +60 -60
- package/src/components/MenuOverlay/types.ts +171 -171
- package/src/components/Overlay/context.ts +10 -10
- package/src/components/Overlay/index.tsx +167 -164
- package/src/components/Overlay/types.ts +70 -70
- package/src/components/Pagination.tsx +133 -133
- package/src/components/ProgressBar.tsx +45 -45
- package/src/components/ProgressCircular.tsx +45 -45
- package/src/components/RadioGroup.tsx +147 -146
- package/src/components/Rating.tsx +98 -98
- package/src/components/Select/MultiSelect.tsx +217 -217
- package/src/components/Select/RichSelect.tsx +128 -128
- package/src/components/Select/SimpleSelect.tsx +73 -73
- package/src/components/Select/hooks.ts +133 -133
- package/src/components/Select/index.tsx +35 -35
- package/src/components/Select/types.ts +134 -134
- package/src/components/SelectBox.tsx +167 -167
- package/src/components/Skeleton.tsx +53 -53
- package/src/components/Slider.tsx +89 -89
- package/src/components/SmartTable.tsx +227 -227
- package/src/components/Stepper.tsx +163 -163
- package/src/components/Table.tsx +234 -234
- package/src/components/Tabs/TabController.ts +54 -54
- package/src/components/Tabs/index.tsx +87 -87
- package/src/components/Tabs/types.ts +54 -54
- package/src/components/Tabs/utils.ts +6 -6
- package/src/components/Text.ts +111 -111
- package/src/components/Textarea.tsx +27 -27
- package/src/components/Tooltip.tsx +72 -72
- package/src/components/layout.tsx +101 -101
- package/src/context/CitricContext.tsx +4 -4
- package/src/context/CitricProvider.tsx +14 -14
- package/src/context/hooks.ts +6 -6
- package/src/index.ts +58 -58
- package/src/overlay.ts +341 -341
- package/src/types.ts +216 -216
- package/src/utils/ValueController.ts +28 -28
- package/src/utils/acessibility.ts +92 -92
- package/src/utils/checkbox.ts +121 -121
- package/src/utils/css.ts +119 -119
- package/src/utils/options.ts +9 -9
- package/src/utils/radio.ts +93 -93
- package/src/utils/react.ts +6 -6
- package/tsconfig.json +10 -10
|
@@ -1,133 +1,133 @@
|
|
|
1
|
-
import { useEffect } from 'react'
|
|
2
|
-
|
|
3
|
-
interface OpenPanelEffectParams {
|
|
4
|
-
open: boolean,
|
|
5
|
-
setOpen: React.Dispatch<React.SetStateAction<boolean>>,
|
|
6
|
-
setSearch: React.Dispatch<React.SetStateAction<string | undefined>>,
|
|
7
|
-
element: React.MutableRefObject<HTMLDivElement | null>,
|
|
8
|
-
searchable?: boolean,
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
interface FocusEffectParams {
|
|
12
|
-
element: React.MutableRefObject<HTMLDivElement | null>,
|
|
13
|
-
focused: boolean,
|
|
14
|
-
setFocused: React.Dispatch<React.SetStateAction<boolean>>,
|
|
15
|
-
setOpen: React.Dispatch<React.SetStateAction<boolean>>,
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface DisabledEffectParams {
|
|
19
|
-
disabled?: boolean,
|
|
20
|
-
setFocused: React.Dispatch<React.SetStateAction<boolean>>,
|
|
21
|
-
setOpen: React.Dispatch<React.SetStateAction<boolean>>,
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/* this runs whenever the selection panel is opened */
|
|
25
|
-
export function useOpenPanelEffect({ open, setOpen, setSearch, element, searchable }: OpenPanelEffectParams) {
|
|
26
|
-
useEffect(() => {
|
|
27
|
-
if (open) {
|
|
28
|
-
setSearch('')
|
|
29
|
-
const selectionPanel = element.current?.querySelector('.selection-panel') as HTMLElement | undefined
|
|
30
|
-
selectionPanel?.querySelector('.options')?.scrollTo({ top: 0 })
|
|
31
|
-
const getCurrent = () => selectionPanel?.querySelector('.option.focused') as HTMLElement | undefined
|
|
32
|
-
const scrollTo = (item: HTMLElement) => {
|
|
33
|
-
const list = item.closest('.options')
|
|
34
|
-
if (!list) return
|
|
35
|
-
const { top: listTop, height: listHeight } = list.getBoundingClientRect()
|
|
36
|
-
const { height: itemHeight, top: itemTop } = item.getBoundingClientRect()
|
|
37
|
-
const offset = itemTop + list.scrollTop - listTop
|
|
38
|
-
if ((list.scrollTop + listHeight < offset + itemHeight) || list.scrollTop > offset) {
|
|
39
|
-
list.scrollTo({ top: offset })
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
/* keyboard and mouse controls */
|
|
43
|
-
const listenToMouse = (event: Event) => {
|
|
44
|
-
if (!selectionPanel?.contains(event.target as HTMLElement)) {
|
|
45
|
-
setOpen(false)
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
const listenToKeyboard = (event: KeyboardEvent) => {
|
|
49
|
-
const isCharacter = event.key.length === 1
|
|
50
|
-
if (['Escape', 'ArrowUp', 'ArrowDown', 'Enter'].includes(event.key) ||
|
|
51
|
-
(searchable && (isCharacter || event.key === 'Backspace'))) {
|
|
52
|
-
event.preventDefault()
|
|
53
|
-
event.stopPropagation()
|
|
54
|
-
}
|
|
55
|
-
if (event.key === 'Escape') setOpen(false)
|
|
56
|
-
if (searchable) {
|
|
57
|
-
if (isCharacter) setSearch(v => `${v}${event.key}`)
|
|
58
|
-
if (event.key === 'Backspace') setSearch(v => v?.substring(0, v.length - 1))
|
|
59
|
-
}
|
|
60
|
-
if (event.key === 'ArrowDown') {
|
|
61
|
-
const current = getCurrent()
|
|
62
|
-
const next = (current?.nextElementSibling ?? selectionPanel?.querySelector('.option')) as HTMLAreaElement | undefined
|
|
63
|
-
if (next) {
|
|
64
|
-
current?.classList.remove('focused')
|
|
65
|
-
next.classList.add('focused')
|
|
66
|
-
scrollTo(next)
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
if (event.key === 'ArrowUp') {
|
|
70
|
-
const current = getCurrent()
|
|
71
|
-
const prev = (
|
|
72
|
-
current?.previousElementSibling ?? selectionPanel?.querySelector('.option:last-child')
|
|
73
|
-
) as HTMLAreaElement | undefined
|
|
74
|
-
if (prev) {
|
|
75
|
-
current?.classList.remove('focused')
|
|
76
|
-
prev.classList.add('focused')
|
|
77
|
-
scrollTo(prev)
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
if (event.key === 'Enter') {
|
|
81
|
-
setTimeout(() => getCurrent()?.click(), 0)
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
// below, we wait 20ms so the same click that opened the select doesn't close it. Removing it will cause problems with selects under
|
|
85
|
-
// labels.
|
|
86
|
-
setTimeout(() => document.addEventListener('click', listenToMouse), 20)
|
|
87
|
-
document.addEventListener('keydown', listenToKeyboard)
|
|
88
|
-
return () => {
|
|
89
|
-
document.removeEventListener('click', listenToMouse)
|
|
90
|
-
document.removeEventListener('keydown', listenToKeyboard)
|
|
91
|
-
getCurrent()?.classList.remove('focused')
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}, [open])
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/* this runs whenever the select is focused */
|
|
98
|
-
export function useFocusEffect({ element, focused, setFocused, setOpen }: FocusEffectParams) {
|
|
99
|
-
useEffect(() => {
|
|
100
|
-
if (focused) {
|
|
101
|
-
const listenToMouse = (event: MouseEvent) => {
|
|
102
|
-
if (!element.current?.contains(event.target as HTMLElement)) {
|
|
103
|
-
setFocused(false)
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
const listenToKeyboard = (event: KeyboardEvent) => {
|
|
107
|
-
if (['Enter', 'ArrowDown', 'ArrowUp'].includes(event.key)) {
|
|
108
|
-
event.preventDefault()
|
|
109
|
-
if (!element.current?.classList.contains('open')) setOpen(true)
|
|
110
|
-
}
|
|
111
|
-
if (event.key === 'Tab') {
|
|
112
|
-
setFocused(false)
|
|
113
|
-
if (element.current?.classList.contains('open')) setOpen(false)
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
document.addEventListener('click', listenToMouse)
|
|
117
|
-
document.addEventListener('keydown', listenToKeyboard)
|
|
118
|
-
return () => {
|
|
119
|
-
document.removeEventListener('click', listenToMouse)
|
|
120
|
-
document.removeEventListener('keydown', listenToKeyboard)
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}, [focused])
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export function useDisabledEffect({ disabled, setOpen, setFocused }: DisabledEffectParams) {
|
|
127
|
-
useEffect(() => {
|
|
128
|
-
if (disabled) {
|
|
129
|
-
setOpen(false)
|
|
130
|
-
setFocused(false)
|
|
131
|
-
}
|
|
132
|
-
}, [disabled])
|
|
133
|
-
}
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
interface OpenPanelEffectParams {
|
|
4
|
+
open: boolean,
|
|
5
|
+
setOpen: React.Dispatch<React.SetStateAction<boolean>>,
|
|
6
|
+
setSearch: React.Dispatch<React.SetStateAction<string | undefined>>,
|
|
7
|
+
element: React.MutableRefObject<HTMLDivElement | null>,
|
|
8
|
+
searchable?: boolean,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface FocusEffectParams {
|
|
12
|
+
element: React.MutableRefObject<HTMLDivElement | null>,
|
|
13
|
+
focused: boolean,
|
|
14
|
+
setFocused: React.Dispatch<React.SetStateAction<boolean>>,
|
|
15
|
+
setOpen: React.Dispatch<React.SetStateAction<boolean>>,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface DisabledEffectParams {
|
|
19
|
+
disabled?: boolean,
|
|
20
|
+
setFocused: React.Dispatch<React.SetStateAction<boolean>>,
|
|
21
|
+
setOpen: React.Dispatch<React.SetStateAction<boolean>>,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* this runs whenever the selection panel is opened */
|
|
25
|
+
export function useOpenPanelEffect({ open, setOpen, setSearch, element, searchable }: OpenPanelEffectParams) {
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (open) {
|
|
28
|
+
setSearch('')
|
|
29
|
+
const selectionPanel = element.current?.querySelector('.selection-panel') as HTMLElement | undefined
|
|
30
|
+
selectionPanel?.querySelector('.options')?.scrollTo({ top: 0 })
|
|
31
|
+
const getCurrent = () => selectionPanel?.querySelector('.option.focused') as HTMLElement | undefined
|
|
32
|
+
const scrollTo = (item: HTMLElement) => {
|
|
33
|
+
const list = item.closest('.options')
|
|
34
|
+
if (!list) return
|
|
35
|
+
const { top: listTop, height: listHeight } = list.getBoundingClientRect()
|
|
36
|
+
const { height: itemHeight, top: itemTop } = item.getBoundingClientRect()
|
|
37
|
+
const offset = itemTop + list.scrollTop - listTop
|
|
38
|
+
if ((list.scrollTop + listHeight < offset + itemHeight) || list.scrollTop > offset) {
|
|
39
|
+
list.scrollTo({ top: offset })
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/* keyboard and mouse controls */
|
|
43
|
+
const listenToMouse = (event: Event) => {
|
|
44
|
+
if (!selectionPanel?.contains(event.target as HTMLElement)) {
|
|
45
|
+
setOpen(false)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const listenToKeyboard = (event: KeyboardEvent) => {
|
|
49
|
+
const isCharacter = event.key.length === 1
|
|
50
|
+
if (['Escape', 'ArrowUp', 'ArrowDown', 'Enter'].includes(event.key) ||
|
|
51
|
+
(searchable && (isCharacter || event.key === 'Backspace'))) {
|
|
52
|
+
event.preventDefault()
|
|
53
|
+
event.stopPropagation()
|
|
54
|
+
}
|
|
55
|
+
if (event.key === 'Escape') setOpen(false)
|
|
56
|
+
if (searchable) {
|
|
57
|
+
if (isCharacter) setSearch(v => `${v}${event.key}`)
|
|
58
|
+
if (event.key === 'Backspace') setSearch(v => v?.substring(0, v.length - 1))
|
|
59
|
+
}
|
|
60
|
+
if (event.key === 'ArrowDown') {
|
|
61
|
+
const current = getCurrent()
|
|
62
|
+
const next = (current?.nextElementSibling ?? selectionPanel?.querySelector('.option')) as HTMLAreaElement | undefined
|
|
63
|
+
if (next) {
|
|
64
|
+
current?.classList.remove('focused')
|
|
65
|
+
next.classList.add('focused')
|
|
66
|
+
scrollTo(next)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (event.key === 'ArrowUp') {
|
|
70
|
+
const current = getCurrent()
|
|
71
|
+
const prev = (
|
|
72
|
+
current?.previousElementSibling ?? selectionPanel?.querySelector('.option:last-child')
|
|
73
|
+
) as HTMLAreaElement | undefined
|
|
74
|
+
if (prev) {
|
|
75
|
+
current?.classList.remove('focused')
|
|
76
|
+
prev.classList.add('focused')
|
|
77
|
+
scrollTo(prev)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (event.key === 'Enter') {
|
|
81
|
+
setTimeout(() => getCurrent()?.click(), 0)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// below, we wait 20ms so the same click that opened the select doesn't close it. Removing it will cause problems with selects under
|
|
85
|
+
// labels.
|
|
86
|
+
setTimeout(() => document.addEventListener('click', listenToMouse), 20)
|
|
87
|
+
document.addEventListener('keydown', listenToKeyboard)
|
|
88
|
+
return () => {
|
|
89
|
+
document.removeEventListener('click', listenToMouse)
|
|
90
|
+
document.removeEventListener('keydown', listenToKeyboard)
|
|
91
|
+
getCurrent()?.classList.remove('focused')
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}, [open])
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* this runs whenever the select is focused */
|
|
98
|
+
export function useFocusEffect({ element, focused, setFocused, setOpen }: FocusEffectParams) {
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (focused) {
|
|
101
|
+
const listenToMouse = (event: MouseEvent) => {
|
|
102
|
+
if (!element.current?.contains(event.target as HTMLElement)) {
|
|
103
|
+
setFocused(false)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const listenToKeyboard = (event: KeyboardEvent) => {
|
|
107
|
+
if (['Enter', 'ArrowDown', 'ArrowUp'].includes(event.key)) {
|
|
108
|
+
event.preventDefault()
|
|
109
|
+
if (!element.current?.classList.contains('open')) setOpen(true)
|
|
110
|
+
}
|
|
111
|
+
if (event.key === 'Tab') {
|
|
112
|
+
setFocused(false)
|
|
113
|
+
if (element.current?.classList.contains('open')) setOpen(false)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
document.addEventListener('click', listenToMouse)
|
|
117
|
+
document.addEventListener('keydown', listenToKeyboard)
|
|
118
|
+
return () => {
|
|
119
|
+
document.removeEventListener('click', listenToMouse)
|
|
120
|
+
document.removeEventListener('keydown', listenToKeyboard)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}, [focused])
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function useDisabledEffect({ disabled, setOpen, setFocused }: DisabledEffectParams) {
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (disabled) {
|
|
129
|
+
setOpen(false)
|
|
130
|
+
setFocused(false)
|
|
131
|
+
}
|
|
132
|
+
}, [disabled])
|
|
133
|
+
}
|
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
import { withRef } from '../../utils/react'
|
|
2
|
-
import { RichSelect } from './RichSelect'
|
|
3
|
-
import { SimpleSelect } from './SimpleSelect'
|
|
4
|
-
import { SelectProps } from './types'
|
|
5
|
-
export type * from './types'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Renders a Select form component. By default, it renders a rich, fully styled component (rich = true). You can set `rich = false` to use
|
|
9
|
-
* only the default select of the browser.
|
|
10
|
-
*
|
|
11
|
-
* The default select from the browser is also rendered when `rich = true`, but it is only accessible when navigating with the keyboard
|
|
12
|
-
* (accessibility).
|
|
13
|
-
*
|
|
14
|
-
* This Select is searchable! You just need to set `searchable = true`.
|
|
15
|
-
*
|
|
16
|
-
* Attention: this component doesn't accept children, instead of manually writing the tag "option", use the property "options".
|
|
17
|
-
*
|
|
18
|
-
* @example
|
|
19
|
-
*
|
|
20
|
-
* ```
|
|
21
|
-
* const options: = useMemo(() => [
|
|
22
|
-
* { id: 1, name: 'Lorem' },
|
|
23
|
-
* { id: 2, name: 'Ipsum' },
|
|
24
|
-
* { id: 3, name: 'Dolor' },
|
|
25
|
-
* ], [])
|
|
26
|
-
* const [value, setValue] = useState(options[0])
|
|
27
|
-
*
|
|
28
|
-
* return <Select options={options} renderLabel={o => o.name} renderKey={o => o.id} value={value} onChange={setValue} />
|
|
29
|
-
* ```
|
|
30
|
-
*/
|
|
31
|
-
export const Select = withRef(
|
|
32
|
-
function Select<T>(props: SelectProps<T>) {
|
|
33
|
-
return props.type === 'simple' ? <SimpleSelect {...props} /> : <RichSelect {...props} />
|
|
34
|
-
},
|
|
35
|
-
)
|
|
1
|
+
import { withRef } from '../../utils/react'
|
|
2
|
+
import { RichSelect } from './RichSelect'
|
|
3
|
+
import { SimpleSelect } from './SimpleSelect'
|
|
4
|
+
import { SelectProps } from './types'
|
|
5
|
+
export type * from './types'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Renders a Select form component. By default, it renders a rich, fully styled component (rich = true). You can set `rich = false` to use
|
|
9
|
+
* only the default select of the browser.
|
|
10
|
+
*
|
|
11
|
+
* The default select from the browser is also rendered when `rich = true`, but it is only accessible when navigating with the keyboard
|
|
12
|
+
* (accessibility).
|
|
13
|
+
*
|
|
14
|
+
* This Select is searchable! You just need to set `searchable = true`.
|
|
15
|
+
*
|
|
16
|
+
* Attention: this component doesn't accept children, instead of manually writing the tag "option", use the property "options".
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
*
|
|
20
|
+
* ```
|
|
21
|
+
* const options: = useMemo(() => [
|
|
22
|
+
* { id: 1, name: 'Lorem' },
|
|
23
|
+
* { id: 2, name: 'Ipsum' },
|
|
24
|
+
* { id: 3, name: 'Dolor' },
|
|
25
|
+
* ], [])
|
|
26
|
+
* const [value, setValue] = useState(options[0])
|
|
27
|
+
*
|
|
28
|
+
* return <Select options={options} renderLabel={o => o.name} renderKey={o => o.id} value={value} onChange={setValue} />
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export const Select = withRef(
|
|
32
|
+
function Select<T>(props: SelectProps<T>) {
|
|
33
|
+
return props.type === 'simple' ? <SimpleSelect {...props} /> : <RichSelect {...props} />
|
|
34
|
+
},
|
|
35
|
+
)
|
|
@@ -1,134 +1,134 @@
|
|
|
1
|
-
import { WithColorScheme } from '../../types'
|
|
2
|
-
|
|
3
|
-
export interface CommonSelectProps<T> extends WithColorScheme {
|
|
4
|
-
/**
|
|
5
|
-
* All the items (options) to render.
|
|
6
|
-
*/
|
|
7
|
-
options: T[],
|
|
8
|
-
/**
|
|
9
|
-
* If the value is undefined or "", an empty option is rendered. This option will disappear as soon as a valid option is selected, unless
|
|
10
|
-
* "required" is set to false.
|
|
11
|
-
*/
|
|
12
|
-
value?: T,
|
|
13
|
-
/**
|
|
14
|
-
* Called whenever the selected option changes.
|
|
15
|
-
* @param value the currently selected item.
|
|
16
|
-
*/
|
|
17
|
-
onChange?: (value: T | undefined) => void,
|
|
18
|
-
/**
|
|
19
|
-
* A function to render the item label.
|
|
20
|
-
* @example
|
|
21
|
-
* `(option) => option.name`
|
|
22
|
-
* @default "the item's toString() result."
|
|
23
|
-
* @param option the item to render.
|
|
24
|
-
* @returns a React Node to render.
|
|
25
|
-
*/
|
|
26
|
-
renderLabel?: (value: T | undefined) => string,
|
|
27
|
-
/**
|
|
28
|
-
* A function to render the item value, a unique identifier for the option.
|
|
29
|
-
* @example
|
|
30
|
-
* `(option) => option.id`
|
|
31
|
-
* @default "if the item is a string or a number, the stringified item. Otherwise, undefined."
|
|
32
|
-
* @param option the item to compute a key for.
|
|
33
|
-
* @returns a string key.
|
|
34
|
-
*/
|
|
35
|
-
renderKey?: (option: T) => string | number | undefined,
|
|
36
|
-
/**
|
|
37
|
-
* Whether or not this input is required. This is also used to figure out if an empty option should be rendered or not.
|
|
38
|
-
*
|
|
39
|
-
* An empty option is rendered whenever "required" is false or "value" is "" or undefined.
|
|
40
|
-
* @default true
|
|
41
|
-
*/
|
|
42
|
-
required?: boolean,
|
|
43
|
-
/**
|
|
44
|
-
* Whether or not to show a loading feedback to the user.
|
|
45
|
-
*
|
|
46
|
-
* Attention: selects that are loading should normally also be disabled.
|
|
47
|
-
*/
|
|
48
|
-
loading?: boolean,
|
|
49
|
-
/**
|
|
50
|
-
* Whether or not this field is disabled.
|
|
51
|
-
* @default false
|
|
52
|
-
*/
|
|
53
|
-
disabled?: boolean,
|
|
54
|
-
/**
|
|
55
|
-
* Function to run when the element receives focus. If the native select element received focus, a Select element is the target, otherwise
|
|
56
|
-
* the wrapping div element is.
|
|
57
|
-
*
|
|
58
|
-
* If `rich` is false, the target will always be a select element.
|
|
59
|
-
*/
|
|
60
|
-
onFocus?: React.EventHandler<React.FocusEvent<HTMLDivElement | HTMLSelectElement>>,
|
|
61
|
-
/**
|
|
62
|
-
* Function to run when the element loses focus. If the native select element received focus, a Select element is the target, otherwise
|
|
63
|
-
* the wrapping div element is.
|
|
64
|
-
*
|
|
65
|
-
* If `rich` is false, the target will always be a select element.
|
|
66
|
-
*/
|
|
67
|
-
onBlur?: React.EventHandler<React.FocusEvent<HTMLDivElement | HTMLSelectElement>>,
|
|
68
|
-
/**
|
|
69
|
-
* If the type is "simple", the native select element is rendered.
|
|
70
|
-
*
|
|
71
|
-
* If the type is "rich", a custom select is rendered and more options are enabled, like "searchable".
|
|
72
|
-
*
|
|
73
|
-
* @default 'rich'
|
|
74
|
-
*/
|
|
75
|
-
type?: 'simple' | 'rich',
|
|
76
|
-
/**
|
|
77
|
-
* A text to show when no option is selected.
|
|
78
|
-
*/
|
|
79
|
-
placeholder?: string,
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export interface SimpleSelectProps<T> extends CommonSelectProps<T> {
|
|
83
|
-
type: 'simple',
|
|
84
|
-
ref?: React.MutableRefObject<HTMLDivElement | HTMLSelectElement>,
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export interface RichSelectProps<T> extends CommonSelectProps<T> {
|
|
88
|
-
type?: 'rich',
|
|
89
|
-
/**
|
|
90
|
-
* Whether or not to render a search field. The search is based on the value returned by `renderLabel`.
|
|
91
|
-
*
|
|
92
|
-
* Attention: when "searchable" is true, make sure to implement `renderLabel` if your options are not strings or number. The search will
|
|
93
|
-
* be performed on the result of this function.
|
|
94
|
-
*
|
|
95
|
-
* @default false
|
|
96
|
-
*/
|
|
97
|
-
searchable?: boolean,
|
|
98
|
-
/**
|
|
99
|
-
* A function to render the option in the selectable list.
|
|
100
|
-
*
|
|
101
|
-
* The `renderLabel` function is used if this is not provided.
|
|
102
|
-
* @param option the option.
|
|
103
|
-
* @returns the React Node.
|
|
104
|
-
*/
|
|
105
|
-
renderOption?: (option: T | undefined) => React.ReactNode,
|
|
106
|
-
/**
|
|
107
|
-
* A function to render the selected option in the header.
|
|
108
|
-
*
|
|
109
|
-
* The `renderOption` function is used if this is not provided.
|
|
110
|
-
* @param option the option.
|
|
111
|
-
* @returns the React Node.
|
|
112
|
-
*/
|
|
113
|
-
renderHeader?: (option: T | undefined) => React.ReactNode,
|
|
114
|
-
/**
|
|
115
|
-
* The maximum height of the options list in pixels.
|
|
116
|
-
*
|
|
117
|
-
* @default 300
|
|
118
|
-
*/
|
|
119
|
-
maxHeight?: number,
|
|
120
|
-
/**
|
|
121
|
-
* Whether or not to show the arrow inside the select's header.
|
|
122
|
-
* @default true
|
|
123
|
-
*/
|
|
124
|
-
showArrow?: boolean,
|
|
125
|
-
ref?: React.MutableRefObject<HTMLDivElement>,
|
|
126
|
-
/**
|
|
127
|
-
* Text to show in the header when no option is selected.
|
|
128
|
-
*/
|
|
129
|
-
placeholder?: string,
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export type BaseSelectProps<T> = SimpleSelectProps<T> | RichSelectProps<T>
|
|
133
|
-
|
|
134
|
-
export type SelectProps<T> = Omit<React.JSX.IntrinsicElements['div'], 'ref' | 'onChange'> & BaseSelectProps<T>
|
|
1
|
+
import { WithColorScheme } from '../../types'
|
|
2
|
+
|
|
3
|
+
export interface CommonSelectProps<T> extends WithColorScheme {
|
|
4
|
+
/**
|
|
5
|
+
* All the items (options) to render.
|
|
6
|
+
*/
|
|
7
|
+
options: T[],
|
|
8
|
+
/**
|
|
9
|
+
* If the value is undefined or "", an empty option is rendered. This option will disappear as soon as a valid option is selected, unless
|
|
10
|
+
* "required" is set to false.
|
|
11
|
+
*/
|
|
12
|
+
value?: T,
|
|
13
|
+
/**
|
|
14
|
+
* Called whenever the selected option changes.
|
|
15
|
+
* @param value the currently selected item.
|
|
16
|
+
*/
|
|
17
|
+
onChange?: (value: T | undefined) => void,
|
|
18
|
+
/**
|
|
19
|
+
* A function to render the item label.
|
|
20
|
+
* @example
|
|
21
|
+
* `(option) => option.name`
|
|
22
|
+
* @default "the item's toString() result."
|
|
23
|
+
* @param option the item to render.
|
|
24
|
+
* @returns a React Node to render.
|
|
25
|
+
*/
|
|
26
|
+
renderLabel?: (value: T | undefined) => string,
|
|
27
|
+
/**
|
|
28
|
+
* A function to render the item value, a unique identifier for the option.
|
|
29
|
+
* @example
|
|
30
|
+
* `(option) => option.id`
|
|
31
|
+
* @default "if the item is a string or a number, the stringified item. Otherwise, undefined."
|
|
32
|
+
* @param option the item to compute a key for.
|
|
33
|
+
* @returns a string key.
|
|
34
|
+
*/
|
|
35
|
+
renderKey?: (option: T) => string | number | undefined,
|
|
36
|
+
/**
|
|
37
|
+
* Whether or not this input is required. This is also used to figure out if an empty option should be rendered or not.
|
|
38
|
+
*
|
|
39
|
+
* An empty option is rendered whenever "required" is false or "value" is "" or undefined.
|
|
40
|
+
* @default true
|
|
41
|
+
*/
|
|
42
|
+
required?: boolean,
|
|
43
|
+
/**
|
|
44
|
+
* Whether or not to show a loading feedback to the user.
|
|
45
|
+
*
|
|
46
|
+
* Attention: selects that are loading should normally also be disabled.
|
|
47
|
+
*/
|
|
48
|
+
loading?: boolean,
|
|
49
|
+
/**
|
|
50
|
+
* Whether or not this field is disabled.
|
|
51
|
+
* @default false
|
|
52
|
+
*/
|
|
53
|
+
disabled?: boolean,
|
|
54
|
+
/**
|
|
55
|
+
* Function to run when the element receives focus. If the native select element received focus, a Select element is the target, otherwise
|
|
56
|
+
* the wrapping div element is.
|
|
57
|
+
*
|
|
58
|
+
* If `rich` is false, the target will always be a select element.
|
|
59
|
+
*/
|
|
60
|
+
onFocus?: React.EventHandler<React.FocusEvent<HTMLDivElement | HTMLSelectElement>>,
|
|
61
|
+
/**
|
|
62
|
+
* Function to run when the element loses focus. If the native select element received focus, a Select element is the target, otherwise
|
|
63
|
+
* the wrapping div element is.
|
|
64
|
+
*
|
|
65
|
+
* If `rich` is false, the target will always be a select element.
|
|
66
|
+
*/
|
|
67
|
+
onBlur?: React.EventHandler<React.FocusEvent<HTMLDivElement | HTMLSelectElement>>,
|
|
68
|
+
/**
|
|
69
|
+
* If the type is "simple", the native select element is rendered.
|
|
70
|
+
*
|
|
71
|
+
* If the type is "rich", a custom select is rendered and more options are enabled, like "searchable".
|
|
72
|
+
*
|
|
73
|
+
* @default 'rich'
|
|
74
|
+
*/
|
|
75
|
+
type?: 'simple' | 'rich',
|
|
76
|
+
/**
|
|
77
|
+
* A text to show when no option is selected.
|
|
78
|
+
*/
|
|
79
|
+
placeholder?: string,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface SimpleSelectProps<T> extends CommonSelectProps<T> {
|
|
83
|
+
type: 'simple',
|
|
84
|
+
ref?: React.MutableRefObject<HTMLDivElement | HTMLSelectElement>,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface RichSelectProps<T> extends CommonSelectProps<T> {
|
|
88
|
+
type?: 'rich',
|
|
89
|
+
/**
|
|
90
|
+
* Whether or not to render a search field. The search is based on the value returned by `renderLabel`.
|
|
91
|
+
*
|
|
92
|
+
* Attention: when "searchable" is true, make sure to implement `renderLabel` if your options are not strings or number. The search will
|
|
93
|
+
* be performed on the result of this function.
|
|
94
|
+
*
|
|
95
|
+
* @default false
|
|
96
|
+
*/
|
|
97
|
+
searchable?: boolean,
|
|
98
|
+
/**
|
|
99
|
+
* A function to render the option in the selectable list.
|
|
100
|
+
*
|
|
101
|
+
* The `renderLabel` function is used if this is not provided.
|
|
102
|
+
* @param option the option.
|
|
103
|
+
* @returns the React Node.
|
|
104
|
+
*/
|
|
105
|
+
renderOption?: (option: T | undefined) => React.ReactNode,
|
|
106
|
+
/**
|
|
107
|
+
* A function to render the selected option in the header.
|
|
108
|
+
*
|
|
109
|
+
* The `renderOption` function is used if this is not provided.
|
|
110
|
+
* @param option the option.
|
|
111
|
+
* @returns the React Node.
|
|
112
|
+
*/
|
|
113
|
+
renderHeader?: (option: T | undefined) => React.ReactNode,
|
|
114
|
+
/**
|
|
115
|
+
* The maximum height of the options list in pixels.
|
|
116
|
+
*
|
|
117
|
+
* @default 300
|
|
118
|
+
*/
|
|
119
|
+
maxHeight?: number,
|
|
120
|
+
/**
|
|
121
|
+
* Whether or not to show the arrow inside the select's header.
|
|
122
|
+
* @default true
|
|
123
|
+
*/
|
|
124
|
+
showArrow?: boolean,
|
|
125
|
+
ref?: React.MutableRefObject<HTMLDivElement>,
|
|
126
|
+
/**
|
|
127
|
+
* Text to show in the header when no option is selected.
|
|
128
|
+
*/
|
|
129
|
+
placeholder?: string,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export type BaseSelectProps<T> = SimpleSelectProps<T> | RichSelectProps<T>
|
|
133
|
+
|
|
134
|
+
export type SelectProps<T> = Omit<React.JSX.IntrinsicElements['div'], 'ref' | 'onChange'> & BaseSelectProps<T>
|