@stack-spot/citric-react 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/dist/citric.css +30 -4
  2. package/dist/components/Accordion.d.ts +34 -0
  3. package/dist/components/Accordion.d.ts.map +1 -1
  4. package/dist/components/Accordion.js +34 -0
  5. package/dist/components/Accordion.js.map +1 -1
  6. package/dist/components/Alert.d.ts +8 -0
  7. package/dist/components/Alert.d.ts.map +1 -1
  8. package/dist/components/Alert.js +8 -0
  9. package/dist/components/Alert.js.map +1 -1
  10. package/dist/components/AsyncContent.d.ts +18 -4
  11. package/dist/components/AsyncContent.d.ts.map +1 -1
  12. package/dist/components/AsyncContent.js +18 -4
  13. package/dist/components/AsyncContent.js.map +1 -1
  14. package/dist/components/Avatar.d.ts +9 -0
  15. package/dist/components/Avatar.d.ts.map +1 -1
  16. package/dist/components/Avatar.js +11 -1
  17. package/dist/components/Avatar.js.map +1 -1
  18. package/dist/components/AvatarGroup.d.ts +8 -0
  19. package/dist/components/AvatarGroup.d.ts.map +1 -1
  20. package/dist/components/AvatarGroup.js +8 -0
  21. package/dist/components/AvatarGroup.js.map +1 -1
  22. package/dist/components/Badge.d.ts +13 -2
  23. package/dist/components/Badge.d.ts.map +1 -1
  24. package/dist/components/Badge.js +14 -3
  25. package/dist/components/Badge.js.map +1 -1
  26. package/dist/components/Blockquote.d.ts +8 -0
  27. package/dist/components/Blockquote.d.ts.map +1 -1
  28. package/dist/components/Blockquote.js +8 -0
  29. package/dist/components/Blockquote.js.map +1 -1
  30. package/dist/components/Breadcrumb.d.ts +8 -0
  31. package/dist/components/Breadcrumb.d.ts.map +1 -1
  32. package/dist/components/Breadcrumb.js +10 -1
  33. package/dist/components/Breadcrumb.js.map +1 -1
  34. package/dist/components/Button.d.ts +11 -0
  35. package/dist/components/Button.d.ts.map +1 -1
  36. package/dist/components/Button.js +14 -2
  37. package/dist/components/Button.js.map +1 -1
  38. package/dist/components/Card.d.ts +15 -4
  39. package/dist/components/Card.d.ts.map +1 -1
  40. package/dist/components/Card.js +13 -1
  41. package/dist/components/Card.js.map +1 -1
  42. package/dist/components/Checkbox.d.ts +14 -0
  43. package/dist/components/Checkbox.d.ts.map +1 -1
  44. package/dist/components/Checkbox.js +14 -0
  45. package/dist/components/Checkbox.js.map +1 -1
  46. package/dist/components/CheckboxGroup.d.ts +22 -3
  47. package/dist/components/CheckboxGroup.d.ts.map +1 -1
  48. package/dist/components/CheckboxGroup.js +23 -3
  49. package/dist/components/CheckboxGroup.js.map +1 -1
  50. package/dist/components/Circle.d.ts +16 -0
  51. package/dist/components/Circle.d.ts.map +1 -1
  52. package/dist/components/Circle.js +8 -0
  53. package/dist/components/Circle.js.map +1 -1
  54. package/dist/components/CitricComponent.d.ts +14 -0
  55. package/dist/components/CitricComponent.d.ts.map +1 -1
  56. package/dist/components/CitricComponent.js +14 -0
  57. package/dist/components/CitricComponent.js.map +1 -1
  58. package/dist/components/Divider.d.ts +4 -1
  59. package/dist/components/Divider.d.ts.map +1 -1
  60. package/dist/components/Divider.js +4 -1
  61. package/dist/components/Divider.js.map +1 -1
  62. package/dist/components/ErrorBoundary.d.ts +13 -0
  63. package/dist/components/ErrorBoundary.d.ts.map +1 -1
  64. package/dist/components/ErrorBoundary.js +13 -0
  65. package/dist/components/ErrorBoundary.js.map +1 -1
  66. package/dist/components/ErrorMessage.js +1 -1
  67. package/dist/components/ErrorMessage.js.map +1 -1
  68. package/dist/components/FallbackBoundary.d.ts +12 -1
  69. package/dist/components/FallbackBoundary.d.ts.map +1 -1
  70. package/dist/components/FallbackBoundary.js +12 -1
  71. package/dist/components/FallbackBoundary.js.map +1 -1
  72. package/dist/components/Favorite.d.ts +12 -0
  73. package/dist/components/Favorite.d.ts.map +1 -1
  74. package/dist/components/Favorite.js +12 -0
  75. package/dist/components/Favorite.js.map +1 -1
  76. package/dist/components/FieldGroup.d.ts +13 -0
  77. package/dist/components/FieldGroup.d.ts.map +1 -1
  78. package/dist/components/FieldGroup.js +13 -0
  79. package/dist/components/FieldGroup.js.map +1 -1
  80. package/dist/components/Form.d.ts +18 -0
  81. package/dist/components/Form.d.ts.map +1 -1
  82. package/dist/components/Form.js +18 -0
  83. package/dist/components/Form.js.map +1 -1
  84. package/dist/components/FormGroup.d.ts +12 -0
  85. package/dist/components/FormGroup.d.ts.map +1 -1
  86. package/dist/components/FormGroup.js +12 -0
  87. package/dist/components/FormGroup.js.map +1 -1
  88. package/dist/components/IconBox.d.ts +34 -9
  89. package/dist/components/IconBox.d.ts.map +1 -1
  90. package/dist/components/IconBox.js +37 -11
  91. package/dist/components/IconBox.js.map +1 -1
  92. package/dist/components/ImageBox.d.ts +32 -8
  93. package/dist/components/ImageBox.d.ts.map +1 -1
  94. package/dist/components/ImageBox.js +36 -11
  95. package/dist/components/ImageBox.js.map +1 -1
  96. package/dist/components/ImageWithFallback.d.ts +18 -0
  97. package/dist/components/ImageWithFallback.d.ts.map +1 -1
  98. package/dist/components/ImageWithFallback.js +11 -0
  99. package/dist/components/ImageWithFallback.js.map +1 -1
  100. package/dist/components/Input.d.ts +15 -3
  101. package/dist/components/Input.d.ts.map +1 -1
  102. package/dist/components/Input.js +16 -3
  103. package/dist/components/Input.js.map +1 -1
  104. package/dist/components/Link.d.ts +6 -0
  105. package/dist/components/Link.d.ts.map +1 -1
  106. package/dist/components/Link.js +6 -0
  107. package/dist/components/Link.js.map +1 -1
  108. package/dist/components/MenuOverlay/index.d.ts +20 -0
  109. package/dist/components/MenuOverlay/index.d.ts.map +1 -1
  110. package/dist/components/MenuOverlay/index.js +20 -0
  111. package/dist/components/MenuOverlay/index.js.map +1 -1
  112. package/dist/components/Overlay/index.d.ts +16 -0
  113. package/dist/components/Overlay/index.d.ts.map +1 -1
  114. package/dist/components/Overlay/index.js +16 -0
  115. package/dist/components/Overlay/index.js.map +1 -1
  116. package/dist/components/Pagination.d.ts +27 -8
  117. package/dist/components/Pagination.d.ts.map +1 -1
  118. package/dist/components/Pagination.js +18 -5
  119. package/dist/components/Pagination.js.map +1 -1
  120. package/dist/components/ProgressBar.d.ts +19 -1
  121. package/dist/components/ProgressBar.d.ts.map +1 -1
  122. package/dist/components/ProgressBar.js +15 -1
  123. package/dist/components/ProgressBar.js.map +1 -1
  124. package/dist/components/ProgressCircular.d.ts +14 -0
  125. package/dist/components/ProgressCircular.d.ts.map +1 -1
  126. package/dist/components/ProgressCircular.js +14 -0
  127. package/dist/components/ProgressCircular.js.map +1 -1
  128. package/dist/components/RadioGroup.d.ts +24 -3
  129. package/dist/components/RadioGroup.d.ts.map +1 -1
  130. package/dist/components/RadioGroup.js +25 -3
  131. package/dist/components/RadioGroup.js.map +1 -1
  132. package/dist/components/Rating.d.ts +10 -0
  133. package/dist/components/Rating.d.ts.map +1 -1
  134. package/dist/components/Rating.js +10 -0
  135. package/dist/components/Rating.js.map +1 -1
  136. package/dist/components/Select/RichSelect.d.ts +3 -5
  137. package/dist/components/Select/RichSelect.d.ts.map +1 -1
  138. package/dist/components/Select/RichSelect.js +4 -4
  139. package/dist/components/Select/RichSelect.js.map +1 -1
  140. package/dist/components/Select/SimpleSelect.d.ts +2 -3
  141. package/dist/components/Select/SimpleSelect.d.ts.map +1 -1
  142. package/dist/components/Select/SimpleSelect.js +2 -3
  143. package/dist/components/Select/SimpleSelect.js.map +1 -1
  144. package/dist/components/Select/index.d.ts +25 -2
  145. package/dist/components/Select/index.d.ts.map +1 -1
  146. package/dist/components/Select/index.js +26 -3
  147. package/dist/components/Select/index.js.map +1 -1
  148. package/dist/components/Select/types.d.ts +1 -2
  149. package/dist/components/Select/types.d.ts.map +1 -1
  150. package/dist/components/SelectBox.d.ts +31 -2
  151. package/dist/components/SelectBox.d.ts.map +1 -1
  152. package/dist/components/SelectBox.js +32 -3
  153. package/dist/components/SelectBox.js.map +1 -1
  154. package/dist/components/Skeleton.d.ts +11 -0
  155. package/dist/components/Skeleton.d.ts.map +1 -1
  156. package/dist/components/Skeleton.js +11 -0
  157. package/dist/components/Skeleton.js.map +1 -1
  158. package/dist/components/Slider.d.ts +12 -0
  159. package/dist/components/Slider.d.ts.map +1 -1
  160. package/dist/components/Slider.js +12 -0
  161. package/dist/components/Slider.js.map +1 -1
  162. package/dist/components/SmartTable.d.ts +36 -2
  163. package/dist/components/SmartTable.d.ts.map +1 -1
  164. package/dist/components/SmartTable.js +37 -3
  165. package/dist/components/SmartTable.js.map +1 -1
  166. package/dist/components/Stepper.d.ts +20 -3
  167. package/dist/components/Stepper.d.ts.map +1 -1
  168. package/dist/components/Stepper.js +21 -3
  169. package/dist/components/Stepper.js.map +1 -1
  170. package/dist/components/Table.d.ts +10 -0
  171. package/dist/components/Table.d.ts.map +1 -1
  172. package/dist/components/Table.js +10 -0
  173. package/dist/components/Table.js.map +1 -1
  174. package/dist/components/Tabs/TabController.d.ts +14 -0
  175. package/dist/components/Tabs/TabController.d.ts.map +1 -1
  176. package/dist/components/Tabs/TabController.js +14 -0
  177. package/dist/components/Tabs/TabController.js.map +1 -1
  178. package/dist/components/Tabs/index.d.ts +20 -3
  179. package/dist/components/Tabs/index.d.ts.map +1 -1
  180. package/dist/components/Tabs/index.js +23 -4
  181. package/dist/components/Tabs/index.js.map +1 -1
  182. package/dist/components/Tabs/types.d.ts +6 -0
  183. package/dist/components/Tabs/types.d.ts.map +1 -1
  184. package/dist/components/Text.d.ts +16 -2
  185. package/dist/components/Text.d.ts.map +1 -1
  186. package/dist/components/Text.js +17 -3
  187. package/dist/components/Text.js.map +1 -1
  188. package/dist/components/Textarea.d.ts +11 -1
  189. package/dist/components/Textarea.d.ts.map +1 -1
  190. package/dist/components/Textarea.js +12 -2
  191. package/dist/components/Textarea.js.map +1 -1
  192. package/dist/components/Tooltip.d.ts +14 -1
  193. package/dist/components/Tooltip.d.ts.map +1 -1
  194. package/dist/components/Tooltip.js +13 -0
  195. package/dist/components/Tooltip.js.map +1 -1
  196. package/dist/components/layout.d.ts +41 -7
  197. package/dist/components/layout.d.ts.map +1 -1
  198. package/dist/components/layout.js +44 -9
  199. package/dist/components/layout.js.map +1 -1
  200. package/dist/index.d.ts +0 -1
  201. package/dist/index.d.ts.map +1 -1
  202. package/dist/index.js +0 -1
  203. package/dist/index.js.map +1 -1
  204. package/package.json +2 -2
  205. package/src/components/Accordion.tsx +34 -0
  206. package/src/components/Alert.tsx +8 -0
  207. package/src/components/AsyncContent.tsx +18 -4
  208. package/src/components/Avatar.tsx +11 -1
  209. package/src/components/AvatarGroup.tsx +8 -0
  210. package/src/components/Badge.tsx +24 -8
  211. package/src/components/Blockquote.tsx +8 -0
  212. package/src/components/Breadcrumb.tsx +10 -1
  213. package/src/components/Button.tsx +17 -2
  214. package/src/components/Card.tsx +34 -14
  215. package/src/components/Checkbox.tsx +14 -0
  216. package/src/components/CheckboxGroup.tsx +61 -40
  217. package/src/components/Circle.tsx +16 -0
  218. package/src/components/CitricComponent.ts +14 -0
  219. package/src/components/Divider.tsx +6 -5
  220. package/src/components/ErrorBoundary.tsx +13 -0
  221. package/src/components/ErrorMessage.tsx +1 -1
  222. package/src/components/FallbackBoundary.tsx +12 -1
  223. package/src/components/Favorite.tsx +12 -0
  224. package/src/components/FieldGroup.tsx +13 -0
  225. package/src/components/Form.tsx +18 -0
  226. package/src/components/FormGroup.tsx +12 -0
  227. package/src/components/IconBox.tsx +62 -31
  228. package/src/components/ImageBox.tsx +60 -30
  229. package/src/components/ImageWithFallback.tsx +18 -0
  230. package/src/components/Input.tsx +28 -14
  231. package/src/components/Link.tsx +6 -0
  232. package/src/components/MenuOverlay/index.tsx +20 -0
  233. package/src/components/Overlay/index.tsx +17 -0
  234. package/src/components/Pagination.tsx +40 -17
  235. package/src/components/ProgressBar.tsx +20 -2
  236. package/src/components/ProgressCircular.tsx +14 -0
  237. package/src/components/RadioGroup.tsx +62 -39
  238. package/src/components/Rating.tsx +10 -0
  239. package/src/components/Select/RichSelect.tsx +183 -182
  240. package/src/components/Select/SimpleSelect.tsx +57 -57
  241. package/src/components/Select/index.tsx +29 -5
  242. package/src/components/Select/types.ts +1 -1
  243. package/src/components/SelectBox.tsx +92 -62
  244. package/src/components/Skeleton.tsx +11 -0
  245. package/src/components/Slider.tsx +12 -0
  246. package/src/components/SmartTable.tsx +91 -56
  247. package/src/components/Stepper.tsx +76 -57
  248. package/src/components/Table.tsx +10 -0
  249. package/src/components/Tabs/TabController.ts +14 -0
  250. package/src/components/Tabs/index.tsx +57 -37
  251. package/src/components/Tabs/types.ts +6 -0
  252. package/src/components/Text.ts +36 -21
  253. package/src/components/Textarea.tsx +14 -4
  254. package/src/components/Tooltip.tsx +14 -1
  255. package/src/components/layout.tsx +56 -13
  256. package/src/index.ts +0 -1
  257. package/dist/components/Switch.d.ts +0 -10
  258. package/dist/components/Switch.d.ts.map +0 -1
  259. package/dist/components/Switch.js +0 -8
  260. package/dist/components/Switch.js.map +0 -1
  261. package/src/components/Switch.tsx +0 -30
@@ -67,42 +67,65 @@ export interface BaseRadioGroupProps<T> extends WithColorScheme {
67
67
 
68
68
  export type RadioGroupProps<T> = Omit<React.JSX.IntrinsicElements['div'], 'onChange' | 'children'> & BaseRadioGroupProps<T>
69
69
 
70
- function _RadioGroup<T>({
71
- name,
72
- value,
73
- options,
74
- onChange,
75
- renderLabel = defaultRenderLabel,
76
- renderKey = defaultRenderKey,
77
- renderItem,
78
- isDisabled,
79
- colorScheme,
80
- style,
81
- ...props
82
- }: RadioGroupProps<T>) {
83
- const items = useMemo(() => {
84
- const valueKey = value ? renderKey(value) : undefined
85
- return options.map((o) => {
86
- const key = renderKey(o)
87
- const radio = <CitricComponent
88
- tag="input"
89
- component="radio"
90
- type="radio"
91
- name={name}
92
- value={key}
93
- checked={value === o || (!isNil(key) && valueKey === key)}
94
- onChange={() => onChange?.(o)}
95
- disabled={isDisabled?.(o)}
96
- />
97
- return renderItem ? renderItem(radio, o) : (
98
- <CitricComponent tag="label" component="radio-row" key={key} colorScheme={colorScheme}>
99
- {radio}
100
- {renderLabel(o)}
101
- </CitricComponent>
102
- )
103
- })
104
- }, [options, value, name, colorScheme])
105
- return <Column {...props} style={{ gap: '8px', ...style }}>{items}</Column>
106
- }
107
-
108
- export const RadioGroup = withRef(_RadioGroup)
70
+ /**
71
+ * Renders a list of radio button for single-selection. One radio button is rendered for each option.
72
+ *
73
+ * Attention: single radio buttons don't make sense, this is why they're not provided by this library.
74
+ *
75
+ * This component can be heavily customized via its properties. Check the storybook for complex examples.
76
+ *
77
+ * Tip: if you need to implement features like "search", use the hook `useRadioGroupControls`.
78
+ *
79
+ * @example
80
+ *
81
+ * ```
82
+ * const options = useMemo(() => [
83
+ * { id: 1, name: 'Option 1' },
84
+ * { id: 2, name: 'Option 2' },
85
+ * { id: 3, name: 'Option 3' },
86
+ * ], [])
87
+ *
88
+ * const [value, setValue] = useState<(typeof options)[number] | undefined>()
89
+ *
90
+ * return <RadioGroup options={options} renderLabel={o => o.name} renderKey={o => o.id} value={value} setValue={setValue} />
91
+ * ```
92
+ */
93
+ export const RadioGroup = withRef(
94
+ function RadioGroup<T>({
95
+ name,
96
+ value,
97
+ options,
98
+ onChange,
99
+ renderLabel = defaultRenderLabel,
100
+ renderKey = defaultRenderKey,
101
+ renderItem,
102
+ isDisabled,
103
+ colorScheme,
104
+ style,
105
+ ...props
106
+ }: RadioGroupProps<T>) {
107
+ const items = useMemo(() => {
108
+ const valueKey = value ? renderKey(value) : undefined
109
+ return options.map((o) => {
110
+ const key = renderKey(o)
111
+ const radio = <CitricComponent
112
+ tag="input"
113
+ component="radio"
114
+ type="radio"
115
+ name={name}
116
+ value={key}
117
+ checked={value === o || (!isNil(key) && valueKey === key)}
118
+ onChange={() => onChange?.(o)}
119
+ disabled={isDisabled?.(o)}
120
+ />
121
+ return renderItem ? renderItem(radio, o) : (
122
+ <CitricComponent tag="label" component="radio-row" key={key} colorScheme={colorScheme}>
123
+ {radio}
124
+ {renderLabel(o)}
125
+ </CitricComponent>
126
+ )
127
+ })
128
+ }, [options, value, name, colorScheme])
129
+ return <Column {...props} style={{ gap: '8px', ...style }}>{items}</Column>
130
+ },
131
+ )
@@ -14,6 +14,16 @@ export interface BaseRatingProps {
14
14
 
15
15
  export type RatingProps = Omit<React.JSX.IntrinsicElements['div'], 'onChange' | 'children'> & BaseRatingProps
16
16
 
17
+ /**
18
+ * Renders a 5 star rating UI. The user can select 1 to 5 stars.
19
+ *
20
+ * @example
21
+ * ```
22
+ * const [value, setValue] = useState<RatingValue | undefined>()
23
+ * return <Rating value={value} setValue={setValue} />
24
+ * ```
25
+ * In the example above, `RatingValue` is any integer from 1 to 5.
26
+ */
17
27
  export const Rating = withRef(({ value, onChange, name, ...props }: RatingProps) => (
18
28
  <CitricComponent tag="div" component="rating" {...props}>
19
29
  <input type="radio" value="1" name={name} checked={value === 5} onChange={() => onChange(5)} />
@@ -10,204 +10,207 @@ import { ProgressCircular } from '../ProgressCircular'
10
10
  import { SimpleSelect } from './SimpleSelect'
11
11
  import { SelectProps } from './types'
12
12
 
13
- function _RichSelect<T>({
14
- ref,
15
- options,
16
- value,
17
- onChange,
18
- renderLabel = defaultRenderLabel,
19
- renderKey = defaultRenderKey,
20
- required = true,
21
- disabled,
22
- loading,
23
- renderOption,
24
- renderHeader,
25
- searchable,
26
- maxHeight,
27
- style,
28
- className,
29
- onFocus,
30
- onBlur,
31
- showArrow,
32
- ...props
33
- }: SelectProps<T> & { type?: 'rich' },
34
- ) {
35
- const [search, setSearch] = useState('')
36
- const _element = useRef<HTMLDivElement | null>(null)
37
- const element = ref ?? _element
38
- const [open, setOpen] = useState(false)
39
- const [focused, setFocused] = useState(false)
40
- const t = useTranslate(dictionary)
13
+ export const RichSelect = withRef(
14
+ function RichSelect<T>({
15
+ ref,
16
+ options,
17
+ value,
18
+ onChange,
19
+ renderLabel = defaultRenderLabel,
20
+ renderKey = defaultRenderKey,
21
+ required = true,
22
+ disabled,
23
+ loading,
24
+ renderOption,
25
+ renderHeader,
26
+ searchable,
27
+ maxHeight,
28
+ style,
29
+ className,
30
+ onFocus,
31
+ onBlur,
32
+ showArrow,
33
+ ...props
34
+ }: SelectProps<T> & { type?: 'rich' },
35
+ ) {
36
+ const [search, setSearch] = useState('')
37
+ const _element = useRef<HTMLDivElement | null>(null)
38
+ const element = ref ?? _element
39
+ const [open, setOpen] = useState(false)
40
+ const [focused, setFocused] = useState(false)
41
+ const t = useTranslate(dictionary)
41
42
 
42
- const change = useCallback((option: T | undefined) => () => {
43
- onChange?.(option)
44
- setOpen(false)
45
- }, [])
46
-
47
- const renderedOptions = useMemo(() => {
48
- const items = required ? [] : [<li key="" className="empty" onClick={change(undefined)}>{renderLabel(undefined) || t.empty}</li>]
49
- options.forEach((o) => {
50
- const key = renderKey(o)
51
- const label = renderLabel(o)
52
- if (!search.trim() || label.toLocaleLowerCase().includes(search.trim().toLocaleLowerCase())) {
53
- items.push(
54
- <li key={key} onClick={change(o)}>
55
- {renderOption?.(o) ?? label}
56
- </li>,
57
- )
58
- }
59
- })
60
- return items
61
- }, [options, value, required, search])
43
+ const change = useCallback((option: T | undefined) => () => {
44
+ onChange?.(option)
45
+ setOpen(false)
46
+ }, [])
62
47
 
63
- /* this runs whenever the selection panel is opened */
64
- useEffect(() => {
65
- if (open) {
66
- setSearch('')
67
- const selectionPanel = element.current?.querySelector('.selection-panel') as HTMLElement | undefined
68
- selectionPanel?.querySelector('ul')?.scrollTo({ top: 0 })
69
- const getCurrent = () => selectionPanel?.querySelector('li.focused') as HTMLElement | undefined
70
- const scrollTo = (li: HTMLElement) => {
71
- const ul = li.closest('ul')
72
- if (!ul) return
73
- const { top: ulTop, height: ulHeight } = ul.getBoundingClientRect()
74
- const { height: liHeight, top: liTop } = li.getBoundingClientRect()
75
- const offset = liTop + ul.scrollTop - ulTop
76
- if ((ul.scrollTop + ulHeight < offset + liHeight) || ul.scrollTop > offset) {
77
- ul.scrollTo({ top: offset })
78
- }
79
- }
80
- /* keyboard and mouse controls */
81
- const listenToMouse = (event: Event) => {
82
- if (!selectionPanel?.contains(event.target as HTMLElement)) {
83
- setOpen(false)
84
- }
85
- }
86
- const listenToKeyboard = (event: KeyboardEvent) => {
87
- const isCharacter = event.key.length === 1
88
- if (['Escape', 'ArrowUp', 'ArrowDown', 'Enter'].includes(event.key) || (searchable && (isCharacter || event.key === 'Backspace'))) {
89
- event.preventDefault()
90
- event.stopPropagation()
48
+ const renderedOptions = useMemo(() => {
49
+ const items = required ? [] : [<li key="" className="empty" onClick={change(undefined)}>{renderLabel(undefined) || t.empty}</li>]
50
+ options.forEach((o) => {
51
+ const key = renderKey(o)
52
+ const label = renderLabel(o)
53
+ if (!search.trim() || label.toLocaleLowerCase().includes(search.trim().toLocaleLowerCase())) {
54
+ items.push(
55
+ <li key={key} onClick={change(o)}>
56
+ {renderOption?.(o) ?? label}
57
+ </li>,
58
+ )
91
59
  }
92
- if (event.key === 'Escape') setOpen(false)
93
- if (searchable) {
94
- if (isCharacter) setSearch(v => `${v}${event.key}`)
95
- if (event.key === 'Backspace') setSearch(v => v.substring(0, v.length - 1))
60
+ })
61
+ return items
62
+ }, [options, value, required, search])
63
+
64
+ /* this runs whenever the selection panel is opened */
65
+ useEffect(() => {
66
+ if (open) {
67
+ setSearch('')
68
+ const selectionPanel = element.current?.querySelector('.selection-panel') as HTMLElement | undefined
69
+ selectionPanel?.querySelector('ul')?.scrollTo({ top: 0 })
70
+ const getCurrent = () => selectionPanel?.querySelector('li.focused') as HTMLElement | undefined
71
+ const scrollTo = (li: HTMLElement) => {
72
+ const ul = li.closest('ul')
73
+ if (!ul) return
74
+ const { top: ulTop, height: ulHeight } = ul.getBoundingClientRect()
75
+ const { height: liHeight, top: liTop } = li.getBoundingClientRect()
76
+ const offset = liTop + ul.scrollTop - ulTop
77
+ if ((ul.scrollTop + ulHeight < offset + liHeight) || ul.scrollTop > offset) {
78
+ ul.scrollTo({ top: offset })
79
+ }
96
80
  }
97
- if (event.key === 'ArrowDown') {
98
- const current = getCurrent()
99
- const next = (current?.nextElementSibling ?? selectionPanel?.querySelector('li')) as HTMLAreaElement | undefined
100
- if (next) {
101
- current?.classList.remove('focused')
102
- next.classList.add('focused')
103
- scrollTo(next)
81
+ /* keyboard and mouse controls */
82
+ const listenToMouse = (event: Event) => {
83
+ if (!selectionPanel?.contains(event.target as HTMLElement)) {
84
+ setOpen(false)
104
85
  }
105
86
  }
106
- if (event.key === 'ArrowUp') {
107
- const current = getCurrent()
108
- const prev = (current?.previousElementSibling ?? selectionPanel?.querySelector('li:last-child')) as HTMLAreaElement | undefined
109
- if (prev) {
110
- current?.classList.remove('focused')
111
- prev.classList.add('focused')
112
- scrollTo(prev)
87
+ const listenToKeyboard = (event: KeyboardEvent) => {
88
+ const isCharacter = event.key.length === 1
89
+ if (['Escape', 'ArrowUp', 'ArrowDown', 'Enter'].includes(event.key) ||
90
+ (searchable && (isCharacter || event.key === 'Backspace'))) {
91
+ event.preventDefault()
92
+ event.stopPropagation()
93
+ }
94
+ if (event.key === 'Escape') setOpen(false)
95
+ if (searchable) {
96
+ if (isCharacter) setSearch(v => `${v}${event.key}`)
97
+ if (event.key === 'Backspace') setSearch(v => v.substring(0, v.length - 1))
98
+ }
99
+ if (event.key === 'ArrowDown') {
100
+ const current = getCurrent()
101
+ const next = (current?.nextElementSibling ?? selectionPanel?.querySelector('li')) as HTMLAreaElement | undefined
102
+ if (next) {
103
+ current?.classList.remove('focused')
104
+ next.classList.add('focused')
105
+ scrollTo(next)
106
+ }
107
+ }
108
+ if (event.key === 'ArrowUp') {
109
+ const current = getCurrent()
110
+ const prev = (current?.previousElementSibling ?? selectionPanel?.querySelector('li:last-child')) as HTMLAreaElement | undefined
111
+ if (prev) {
112
+ current?.classList.remove('focused')
113
+ prev.classList.add('focused')
114
+ scrollTo(prev)
115
+ }
116
+ }
117
+ if (event.key === 'Enter') {
118
+ setTimeout(() => getCurrent()?.click(), 0)
113
119
  }
114
120
  }
115
- if (event.key === 'Enter') {
116
- setTimeout(() => getCurrent()?.click(), 0)
121
+ // below, we wait 20ms so the same click that opened the select doesn't close it. Removing it will cause problems with selects under
122
+ // labels.
123
+ setTimeout(() => document.addEventListener('click', listenToMouse), 20)
124
+ document.addEventListener('keydown', listenToKeyboard)
125
+ return () => {
126
+ document.removeEventListener('click', listenToMouse)
127
+ document.removeEventListener('keydown', listenToKeyboard)
128
+ getCurrent()?.classList.remove('focused')
117
129
  }
118
130
  }
119
- // below, we wait 20ms so the same click that opened the select doesn't close it. Removing it will cause problems with selects under
120
- // labels.
121
- setTimeout(() => document.addEventListener('click', listenToMouse), 20)
122
- document.addEventListener('keydown', listenToKeyboard)
123
- return () => {
124
- document.removeEventListener('click', listenToMouse)
125
- document.removeEventListener('keydown', listenToKeyboard)
126
- getCurrent()?.classList.remove('focused')
127
- }
128
- }
129
- }, [open])
131
+ }, [open])
130
132
 
131
- /* this runs whenever the select is focused */
132
- useEffect(() => {
133
- if (focused) {
134
- const listenToMouse = (event: MouseEvent) => {
135
- if (!element.current?.contains(event.target as HTMLElement)) {
136
- setFocused(false)
133
+ /* this runs whenever the select is focused */
134
+ useEffect(() => {
135
+ if (focused) {
136
+ const listenToMouse = (event: MouseEvent) => {
137
+ if (!element.current?.contains(event.target as HTMLElement)) {
138
+ setFocused(false)
139
+ }
137
140
  }
138
- }
139
- const listenToKeyboard = (event: KeyboardEvent) => {
140
- if (['Enter', 'ArrowDown', 'ArrowUp'].includes(event.key)) {
141
- event.preventDefault()
142
- if (!element.current?.classList.contains('open')) setOpen(true)
141
+ const listenToKeyboard = (event: KeyboardEvent) => {
142
+ if (['Enter', 'ArrowDown', 'ArrowUp'].includes(event.key)) {
143
+ event.preventDefault()
144
+ if (!element.current?.classList.contains('open')) setOpen(true)
145
+ }
146
+ if (event.key === 'Tab') {
147
+ setFocused(false)
148
+ if (element.current?.classList.contains('open')) setOpen(false)
149
+ }
143
150
  }
144
- if (event.key === 'Tab') {
145
- setFocused(false)
146
- if (element.current?.classList.contains('open')) setOpen(false)
151
+ document.addEventListener('click', listenToMouse)
152
+ document.addEventListener('keydown', listenToKeyboard)
153
+ return () => {
154
+ document.removeEventListener('click', listenToMouse)
155
+ document.removeEventListener('keydown', listenToKeyboard)
147
156
  }
148
157
  }
149
- document.addEventListener('click', listenToMouse)
150
- document.addEventListener('keydown', listenToKeyboard)
151
- return () => {
152
- document.removeEventListener('click', listenToMouse)
153
- document.removeEventListener('keydown', listenToKeyboard)
154
- }
155
- }
156
- }, [focused])
158
+ }, [focused])
157
159
 
158
- useEffect(() => {
159
- if (disabled) {
160
- setOpen(false)
161
- setFocused(false)
162
- }
163
- }, [disabled])
160
+ useEffect(() => {
161
+ if (disabled) {
162
+ setOpen(false)
163
+ setFocused(false)
164
+ }
165
+ }, [disabled])
164
166
 
165
- return (
166
- <CitricComponent
167
- tag="div"
168
- component="rich-select"
169
- style={maxHeight ? applyCSSVariable(style, 'max-height', `${maxHeight}px`) : style}
170
- className={listToClass([className, showArrow === false && 'hide-arrow', open && 'open', focused && 'focused'])}
171
- ref={element}
172
- aria-busy={loading}
173
- {...props}
174
- >
175
- <SimpleSelect
176
- options={options}
177
- value={value}
178
- renderLabel={renderLabel}
179
- renderKey={renderKey}
180
- required={required}
181
- disabled={disabled}
182
- onChange={onChange}
183
- onFocus={onFocus}
184
- onBlur={onBlur}
185
- wrap={false}
186
- />
187
- <header
188
- onClick={(e) => {
189
- if (disabled) return
190
- if (!open) e.stopPropagation()
191
- setFocused(true)
192
- setOpen(true)
193
- }}
194
- aria-hidden
167
+ return (
168
+ <CitricComponent
169
+ tag="div"
170
+ component="rich-select"
171
+ style={maxHeight ? applyCSSVariable(style, 'max-height', `${maxHeight}px`) : style}
172
+ className={listToClass([className, showArrow === false && 'hide-arrow', open && 'open', focused && 'focused'])}
173
+ ref={element}
174
+ aria-busy={loading}
175
+ {...props}
195
176
  >
196
- {(renderHeader?.(value) ?? renderOption?.(value) ?? renderLabel(value)) || <span></span>}
197
- {loading && <ProgressCircular size="xs" className="loader" />}
198
- </header>
199
- <div className="selection-panel" aria-hidden>
200
- {searchable && <div className="search-bar">
201
- <div data-citric="field-group" className="auto">
202
- <i data-citric="icon-box" className="citric-icon outline Search"></i>
203
- <Input type="search" value={search} onChange={setSearch} tabIndex={-1} />
204
- </div>
205
- </div>}
206
- <ul>{renderedOptions}</ul>
207
- </div>
208
- </CitricComponent>
209
- )
210
- }
177
+ <SimpleSelect
178
+ options={options}
179
+ value={value}
180
+ renderLabel={renderLabel}
181
+ renderKey={renderKey}
182
+ required={required}
183
+ disabled={disabled}
184
+ onChange={onChange}
185
+ onFocus={onFocus}
186
+ onBlur={onBlur}
187
+ wrap={false}
188
+ />
189
+ <header
190
+ onClick={(e) => {
191
+ if (disabled) return
192
+ if (!open) e.stopPropagation()
193
+ setFocused(true)
194
+ setOpen(true)
195
+ }}
196
+ aria-hidden
197
+ >
198
+ {(renderHeader?.(value) ?? renderOption?.(value) ?? renderLabel(value)) || <span></span>}
199
+ {loading && <ProgressCircular size="xs" className="loader" />}
200
+ </header>
201
+ <div className="selection-panel" aria-hidden>
202
+ {searchable && <div className="search-bar">
203
+ <div data-citric="field-group" className="auto">
204
+ <i data-citric="icon-box" className="citric-icon outline Search"></i>
205
+ <Input type="search" value={search} onChange={setSearch} tabIndex={-1} />
206
+ </div>
207
+ </div>}
208
+ <ul>{renderedOptions}</ul>
209
+ </div>
210
+ </CitricComponent>
211
+ )
212
+ },
213
+ )
211
214
 
212
215
  const dictionary = {
213
216
  en: {
@@ -217,5 +220,3 @@ const dictionary = {
217
220
  empty: 'Vazio',
218
221
  },
219
222
  }
220
-
221
- export const RichSelect = withRef(_RichSelect)
@@ -5,69 +5,69 @@ import { CitricComponent } from '../CitricComponent'
5
5
  import { ProgressCircular } from '../ProgressCircular'
6
6
  import { SelectProps } from './types'
7
7
 
8
- export function _SimpleSelect<T>({
9
- ref,
10
- options,
11
- value,
12
- onChange,
13
- renderLabel = defaultRenderLabel,
14
- renderKey = defaultRenderKey,
15
- required = true,
16
- loading,
17
- disabled,
18
- onBlur,
19
- onFocus,
20
- wrap,
21
- ...props
22
- }: SelectProps<T> & { wrap?: boolean },
23
- ) {
24
- const handleChange = useCallback((e: React.ChangeEvent<HTMLSelectElement>) => {
25
- const selectedIndex = e.target.options.selectedIndex - (e.target.options.length > options.length ? 1 : 0)
26
- onChange?.(selectedIndex >= 0 ? options[selectedIndex] : undefined)
27
- }, [options])
8
+ export const SimpleSelect = withRef(
9
+ function SimpleSelect<T>({
10
+ ref,
11
+ options,
12
+ value,
13
+ onChange,
14
+ renderLabel = defaultRenderLabel,
15
+ renderKey = defaultRenderKey,
16
+ required = true,
17
+ loading,
18
+ disabled,
19
+ onBlur,
20
+ onFocus,
21
+ wrap,
22
+ ...props
23
+ }: SelectProps<T> & { wrap?: boolean },
24
+ ) {
25
+ const handleChange = useCallback((e: React.ChangeEvent<HTMLSelectElement>) => {
26
+ const selectedIndex = e.target.options.selectedIndex - (e.target.options.length > options.length ? 1 : 0)
27
+ onChange?.(selectedIndex >= 0 ? options[selectedIndex] : undefined)
28
+ }, [options])
28
29
 
29
- const renderedOptions = useMemo(() => {
30
- const items = (!value || !required) ? [<option key="">{renderLabel(undefined)}</option>] : []
31
- options.forEach((o) => {
32
- const key = renderKey(o)
33
- items.push(
34
- <option key={key} value={key}>
35
- {renderLabel(o)}
36
- </option>,
37
- )
38
- })
39
- return items
40
- }, [options, value, required])
30
+ const renderedOptions = useMemo(() => {
31
+ const items = (!value || !required) ? [<option key="">{renderLabel(undefined)}</option>] : []
32
+ options.forEach((o) => {
33
+ const key = renderKey(o)
34
+ items.push(
35
+ <option key={key} value={key}>
36
+ {renderLabel(o)}
37
+ </option>,
38
+ )
39
+ })
40
+ return items
41
+ }, [options, value, required])
41
42
 
42
- return wrap === false ? (
43
- <CitricComponent
44
- tag="select"
45
- ref={ref as MutableRefObject<HTMLSelectElement | null>}
46
- required={required}
47
- onChange={handleChange}
48
- disabled={disabled || loading}
49
- component="select"
50
- onFocus={onFocus}
51
- onBlur={onBlur}
52
- value={value ? renderKey(value) : ''}
53
- >
54
- {renderedOptions}
55
- </CitricComponent>
56
- ) : (
57
- <CitricComponent ref={ref as MutableRefObject<HTMLDivElement | null>} tag="div" component="select" aria-busy={loading} {...props}>
58
- <select
59
- value={value ? renderKey(value) : undefined}
43
+ return wrap === false ? (
44
+ <CitricComponent
45
+ tag="select"
46
+ ref={ref as MutableRefObject<HTMLSelectElement | null>}
60
47
  required={required}
61
48
  onChange={handleChange}
62
49
  disabled={disabled || loading}
50
+ component="select"
63
51
  onFocus={onFocus}
64
52
  onBlur={onBlur}
53
+ value={value ? renderKey(value) : ''}
65
54
  >
66
55
  {renderedOptions}
67
- </select>
68
- {loading && <ProgressCircular className="loader" size="xs" />}
69
- </CitricComponent>
70
- )
71
- }
72
-
73
- export const SimpleSelect = withRef(_SimpleSelect)
56
+ </CitricComponent>
57
+ ) : (
58
+ <CitricComponent ref={ref as MutableRefObject<HTMLDivElement | null>} tag="div" component="select" aria-busy={loading} {...props}>
59
+ <select
60
+ value={value ? renderKey(value) : undefined}
61
+ required={required}
62
+ onChange={handleChange}
63
+ disabled={disabled || loading}
64
+ onFocus={onFocus}
65
+ onBlur={onBlur}
66
+ >
67
+ {renderedOptions}
68
+ </select>
69
+ {loading && <ProgressCircular className="loader" size="xs" />}
70
+ </CitricComponent>
71
+ )
72
+ },
73
+ )