@stack-spot/citric-react 0.42.0-beta.0 → 0.43.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 (209) hide show
  1. package/CHANGELOG.md +13 -13
  2. package/dist/citric.css +2926 -2920
  3. package/dist/components/Accordion.d.ts +1 -1
  4. package/dist/components/Accordion.js +1 -1
  5. package/dist/components/Alert.d.ts +1 -1
  6. package/dist/components/Alert.js +1 -1
  7. package/dist/components/AsyncContent.d.ts +1 -1
  8. package/dist/components/AsyncContent.js +1 -1
  9. package/dist/components/Autocomplete.d.ts +370 -0
  10. package/dist/components/Autocomplete.d.ts.map +1 -0
  11. package/dist/components/{Autocomplete/Autocomplete.js → Autocomplete.js} +163 -98
  12. package/dist/components/Autocomplete.js.map +1 -0
  13. package/dist/components/Avatar.d.ts +1 -1
  14. package/dist/components/Avatar.js +1 -1
  15. package/dist/components/AvatarGroup.d.ts +1 -1
  16. package/dist/components/AvatarGroup.js +1 -1
  17. package/dist/components/Badge.d.ts +1 -1
  18. package/dist/components/Badge.js +1 -1
  19. package/dist/components/Blockquote.d.ts +1 -1
  20. package/dist/components/Blockquote.js +1 -1
  21. package/dist/components/Breadcrumb.d.ts +1 -1
  22. package/dist/components/Breadcrumb.js +1 -1
  23. package/dist/components/Button.d.ts +1 -1
  24. package/dist/components/Button.js +1 -1
  25. package/dist/components/ButtonLink.d.ts +1 -1
  26. package/dist/components/ButtonLink.js +1 -1
  27. package/dist/components/Card.d.ts +1 -1
  28. package/dist/components/Card.js +1 -1
  29. package/dist/components/Checkbox.d.ts +1 -1
  30. package/dist/components/Checkbox.d.ts.map +1 -1
  31. package/dist/components/Checkbox.js +2 -2
  32. package/dist/components/Checkbox.js.map +1 -1
  33. package/dist/components/CheckboxGroup.d.ts +1 -1
  34. package/dist/components/CheckboxGroup.js +1 -1
  35. package/dist/components/Circle.d.ts +1 -1
  36. package/dist/components/Circle.js +1 -1
  37. package/dist/components/Divider.d.ts +1 -1
  38. package/dist/components/Divider.js +1 -1
  39. package/dist/components/ErrorBoundary.d.ts +1 -1
  40. package/dist/components/ErrorBoundary.js +1 -1
  41. package/dist/components/ErrorMessage.d.ts +1 -1
  42. package/dist/components/ErrorMessage.js +1 -1
  43. package/dist/components/FallbackBoundary.d.ts +1 -1
  44. package/dist/components/FallbackBoundary.js +1 -1
  45. package/dist/components/Favorite.d.ts +1 -1
  46. package/dist/components/Favorite.js +1 -1
  47. package/dist/components/FieldGroup.d.ts +1 -1
  48. package/dist/components/FieldGroup.js +1 -1
  49. package/dist/components/Form.d.ts +2 -2
  50. package/dist/components/Form.js +1 -1
  51. package/dist/components/FormGroup.d.ts +1 -1
  52. package/dist/components/FormGroup.js +1 -1
  53. package/dist/components/Icon.d.ts +1 -1
  54. package/dist/components/Icon.js +1 -1
  55. package/dist/components/IconBox.d.ts +3 -3
  56. package/dist/components/IconBox.js +1 -1
  57. package/dist/components/ImageBox.d.ts +3 -3
  58. package/dist/components/ImageBox.js +1 -1
  59. package/dist/components/ImageWithFallback.d.ts +1 -1
  60. package/dist/components/ImageWithFallback.js +1 -1
  61. package/dist/components/Input.d.ts +1 -1
  62. package/dist/components/Input.js +1 -1
  63. package/dist/components/Link.d.ts +1 -1
  64. package/dist/components/Link.js +1 -1
  65. package/dist/components/LoadingPanel.d.ts +1 -1
  66. package/dist/components/LoadingPanel.js +1 -1
  67. package/dist/components/MenuOverlay/Menu.d.ts +1 -1
  68. package/dist/components/MenuOverlay/Menu.js +1 -1
  69. package/dist/components/MenuOverlay/index.d.ts +1 -1
  70. package/dist/components/MenuOverlay/index.js +1 -1
  71. package/dist/components/Overlay/index.d.ts +1 -1
  72. package/dist/components/Overlay/index.js +1 -1
  73. package/dist/components/Pagination.d.ts +1 -1
  74. package/dist/components/Pagination.js +1 -1
  75. package/dist/components/ProgressBar.d.ts +1 -1
  76. package/dist/components/ProgressBar.js +1 -1
  77. package/dist/components/ProgressCircular.d.ts +1 -1
  78. package/dist/components/ProgressCircular.js +1 -1
  79. package/dist/components/RadioGroup.d.ts +1 -1
  80. package/dist/components/RadioGroup.js +1 -1
  81. package/dist/components/Rating.d.ts +1 -1
  82. package/dist/components/Rating.js +1 -1
  83. package/dist/components/Select/MultiSelect.d.ts +1 -1
  84. package/dist/components/Select/MultiSelect.js +1 -1
  85. package/dist/components/Select/RichSelect.d.ts +1 -1
  86. package/dist/components/Select/RichSelect.js +1 -1
  87. package/dist/components/Select/SimpleSelect.d.ts +1 -1
  88. package/dist/components/Select/SimpleSelect.js +1 -1
  89. package/dist/components/Select/index.d.ts +1 -1
  90. package/dist/components/Select/index.js +1 -1
  91. package/dist/components/SelectBox.d.ts +9 -1
  92. package/dist/components/SelectBox.d.ts.map +1 -1
  93. package/dist/components/SelectBox.js +6 -5
  94. package/dist/components/SelectBox.js.map +1 -1
  95. package/dist/components/Skeleton.d.ts +1 -1
  96. package/dist/components/Skeleton.js +1 -1
  97. package/dist/components/Slider.d.ts +1 -1
  98. package/dist/components/Slider.js +1 -1
  99. package/dist/components/SmartTable.d.ts +1 -1
  100. package/dist/components/SmartTable.js +1 -1
  101. package/dist/components/Stepper.d.ts +1 -1
  102. package/dist/components/Stepper.js +1 -1
  103. package/dist/components/Table.d.ts +3 -3
  104. package/dist/components/Table.js +1 -1
  105. package/dist/components/Tabs/index.d.ts +1 -1
  106. package/dist/components/Tabs/index.js +1 -1
  107. package/dist/components/Textarea.d.ts +1 -1
  108. package/dist/components/Textarea.js +1 -1
  109. package/dist/components/Tooltip.d.ts +1 -1
  110. package/dist/components/Tooltip.js +1 -1
  111. package/dist/context/CitricProvider.d.ts +1 -1
  112. package/dist/context/CitricProvider.js +1 -1
  113. package/dist/index.d.ts +2 -1
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.js +2 -1
  116. package/dist/index.js.map +1 -1
  117. package/dist/overlay.js +1 -1
  118. package/dist/theme.css +415 -415
  119. package/dist/utils/css.js +1 -1
  120. package/dist/utils/css.js.map +1 -1
  121. package/package.json +1 -1
  122. package/scripts/build-css.ts +49 -49
  123. package/src/components/Accordion.tsx +130 -130
  124. package/src/components/Alert.tsx +24 -24
  125. package/src/components/AsyncContent.tsx +75 -75
  126. package/src/components/{Autocomplete/Autocomplete.tsx → Autocomplete.tsx} +403 -159
  127. package/src/components/Avatar.tsx +45 -45
  128. package/src/components/AvatarGroup.tsx +49 -49
  129. package/src/components/Badge.tsx +47 -47
  130. package/src/components/Blockquote.tsx +18 -18
  131. package/src/components/Breadcrumb.tsx +33 -33
  132. package/src/components/Button.tsx +105 -105
  133. package/src/components/ButtonLink.tsx +45 -45
  134. package/src/components/Card.tsx +68 -68
  135. package/src/components/Checkbox.tsx +51 -52
  136. package/src/components/CheckboxGroup.tsx +153 -153
  137. package/src/components/Circle.tsx +43 -43
  138. package/src/components/CitricComponent.ts +47 -47
  139. package/src/components/Divider.tsx +24 -24
  140. package/src/components/ErrorBoundary.tsx +75 -75
  141. package/src/components/ErrorMessage.tsx +11 -11
  142. package/src/components/FallbackBoundary.tsx +40 -40
  143. package/src/components/Favorite.tsx +57 -57
  144. package/src/components/FieldGroup.tsx +46 -46
  145. package/src/components/Form.tsx +36 -36
  146. package/src/components/FormGroup.tsx +57 -57
  147. package/src/components/Icon.tsx +35 -35
  148. package/src/components/IconBox.tsx +134 -134
  149. package/src/components/ImageBox.tsx +125 -125
  150. package/src/components/ImageWithFallback.tsx +65 -65
  151. package/src/components/Input.tsx +49 -49
  152. package/src/components/Link.tsx +55 -55
  153. package/src/components/LoadingPanel.tsx +12 -12
  154. package/src/components/MenuOverlay/Menu.tsx +158 -158
  155. package/src/components/MenuOverlay/context.ts +20 -20
  156. package/src/components/MenuOverlay/index.tsx +55 -55
  157. package/src/components/MenuOverlay/keyboard.ts +60 -60
  158. package/src/components/MenuOverlay/types.ts +171 -171
  159. package/src/components/Overlay/context.ts +10 -10
  160. package/src/components/Overlay/index.tsx +182 -182
  161. package/src/components/Overlay/types.ts +75 -75
  162. package/src/components/Pagination.tsx +133 -133
  163. package/src/components/ProgressBar.tsx +45 -45
  164. package/src/components/ProgressCircular.tsx +45 -45
  165. package/src/components/RadioGroup.tsx +147 -147
  166. package/src/components/Rating.tsx +98 -98
  167. package/src/components/Select/MultiSelect.tsx +217 -217
  168. package/src/components/Select/RichSelect.tsx +128 -128
  169. package/src/components/Select/SimpleSelect.tsx +73 -73
  170. package/src/components/Select/hooks.ts +133 -133
  171. package/src/components/Select/index.tsx +35 -35
  172. package/src/components/Select/types.ts +134 -134
  173. package/src/components/SelectBox.tsx +181 -167
  174. package/src/components/Skeleton.tsx +53 -53
  175. package/src/components/Slider.tsx +89 -89
  176. package/src/components/SmartTable.tsx +227 -227
  177. package/src/components/Stepper.tsx +163 -163
  178. package/src/components/Table.tsx +234 -234
  179. package/src/components/Tabs/TabController.ts +54 -54
  180. package/src/components/Tabs/index.tsx +106 -106
  181. package/src/components/Tabs/types.ts +67 -67
  182. package/src/components/Tabs/utils.ts +6 -6
  183. package/src/components/Text.ts +111 -111
  184. package/src/components/Textarea.tsx +27 -27
  185. package/src/components/Tooltip.tsx +83 -83
  186. package/src/components/layout.tsx +101 -101
  187. package/src/context/CitricContext.tsx +4 -4
  188. package/src/context/CitricProvider.tsx +14 -14
  189. package/src/context/hooks.ts +6 -6
  190. package/src/index.ts +59 -59
  191. package/src/overlay.ts +348 -348
  192. package/src/types.ts +235 -235
  193. package/src/utils/ValueController.ts +28 -28
  194. package/src/utils/acessibility.ts +92 -92
  195. package/src/utils/checkbox.ts +121 -121
  196. package/src/utils/css.ts +119 -119
  197. package/src/utils/options.ts +9 -9
  198. package/src/utils/radio.ts +93 -93
  199. package/src/utils/react.ts +6 -6
  200. package/src/utils/time.ts +5 -5
  201. package/tsconfig.json +10 -10
  202. package/dist/components/Autocomplete/Autocomplete.d.ts +0 -211
  203. package/dist/components/Autocomplete/Autocomplete.d.ts.map +0 -1
  204. package/dist/components/Autocomplete/Autocomplete.js.map +0 -1
  205. package/dist/components/Autocomplete/index.d.ts +0 -3
  206. package/dist/components/Autocomplete/index.d.ts.map +0 -1
  207. package/dist/components/Autocomplete/index.js +0 -2
  208. package/dist/components/Autocomplete/index.js.map +0 -1
  209. package/src/components/Autocomplete/index.ts +0 -3
@@ -1,182 +1,182 @@
1
- import { useEffect, useRef } from 'react'
2
- import { showOverlay } from '../../overlay'
3
- import { HTMLTag } from '../../types'
4
- import { focusFirstChild } from '../../utils/acessibility'
5
- import { delay } from '../../utils/time'
6
- import { OverlayProvider } from './context'
7
- import { OverlayController, OverlayProps } from './types'
8
- export { useOverlayController } from './context'
9
-
10
- /**
11
- * An arbitrary time to wait for the next React render to be performed
12
- */
13
- const arbitraryRenderTime = 20
14
-
15
- /**
16
- * These todos are in order of priority.
17
- *
18
- * TODO: update position when the size changes. Currently, the top position seems out of place whenever the height changes. The same is
19
- * probably true for the left position if the width changes.
20
- * TODO (minor): use React Portal to implement overlays. The current implementation will lose every React context in the tree.
21
- */
22
-
23
- /**
24
- * Creates an overlay for the child component. The overlay can be any React element. The overlay can be triggered by "click" or "hover"
25
- * (default).
26
- *
27
- * The overlay can be hidden from within its content through the hook `useOverlayController()`.
28
- *
29
- * @example
30
- *
31
- * ```
32
- * const overlay = <Card>Hey, this is my overlay!</Card>
33
- *
34
- * return (
35
- * <Overlay content={overlay} attributes={{ style: { margin: '20px' } }}>
36
- * <Button>Hover to see the overlay</Button>
37
- * </Overlay>
38
- * )
39
- * ```
40
- */
41
- export function Overlay<T extends keyof HTMLTag>({
42
- tag,
43
- children,
44
- content,
45
- position = 'top',
46
- triggerOn = 'hover',
47
- alignment = 'center',
48
- attributes,
49
- onRenderChild,
50
- autoFocusBehavior = 'keyboard',
51
- openDelayMS,
52
- closeDelayMS,
53
- ...props
54
- }: OverlayProps<T>,
55
- ) {
56
- const controller = useRef<OverlayController>({ close: () => Promise.resolve() })
57
- const wrapper = useRef<HTMLDivElement | null>(null)
58
- // props that don't require removing and reattaching the event listeners
59
- const dynamic = useRef({ tag, content, position, alignment, attributes })
60
-
61
- useEffect(() => {
62
- dynamic.current = { tag, content, position, alignment, attributes }
63
- }, [tag, content, position, alignment, attributes])
64
-
65
- useEffect(() => {
66
- let visible = false
67
- let hideOnClickOutside: ((event: Event) => void) | undefined
68
- let hideOnEsc: ((event: Event) => void) | undefined
69
- let hideOverlay: ((immediately?: boolean) => Promise<void>) | undefined
70
- let removeRefocusTargetListener: (() => void) | undefined
71
-
72
- function getTarget() {
73
- const target = wrapper.current?.firstChild
74
- return target instanceof HTMLElement ? target : undefined
75
- }
76
-
77
- if (onRenderChild) {
78
- const target = getTarget()
79
- if (target) onRenderChild(target)
80
- }
81
-
82
- async function show(event: Event) {
83
- if (visible) return
84
- visible = true
85
- const target = getTarget()
86
- if (openDelayMS && triggerOn === 'hover') {
87
- await delay(openDelayMS)
88
- if (!target?.matches(':hover, :focus')) {
89
- visible = false
90
- return
91
- }
92
- }
93
- if (!target) return
94
- const { overlay, hide: hideFn } = showOverlay({
95
- tag: dynamic.current.tag,
96
- content: ['string', 'number', 'boolean'].includes(typeof dynamic.current.content)
97
- ? dynamic.current.content
98
- : <OverlayProvider value={controller.current}>{dynamic.current.content}</OverlayProvider>,
99
- target,
100
- position: dynamic.current.position,
101
- alignment: dynamic.current.alignment,
102
- attributes: dynamic.current.attributes,
103
- })
104
- hideOverlay?.(true) // ensures there's no lost opened overlay.
105
- hideOverlay = hideFn
106
-
107
- function onHide(condition: (event: Event) => boolean) {
108
- return (event: Event) => {
109
- if (condition(event)) controller.current.close()
110
- }
111
- }
112
-
113
- if (event.type === 'click') {
114
- hideOnEsc = onHide(e => e instanceof KeyboardEvent && e.key === 'Escape')
115
- setTimeout(() => {
116
- hideOnClickOutside = onHide(e => e instanceof MouseEvent && e.button === 0 && !overlay.contains(e.target as HTMLElement))
117
- document.addEventListener('click', hideOnClickOutside)
118
- }, arbitraryRenderTime)
119
- document.addEventListener('keydown', hideOnEsc)
120
- }
121
-
122
- // focus target when the last overlay element loses focus
123
- function refocusTarget(e: KeyboardEvent) {
124
- if (e.key === 'Tab' && e.target instanceof HTMLElement) {
125
- const allItems = Array.from(e.target.closest('[data-citric="menu"]')?.querySelectorAll('a, button') ?? [])
126
- if (e.target === allItems[allItems.length - 1]) {
127
- getTarget()?.focus()
128
- e.preventDefault()
129
- }
130
- }
131
- }
132
- overlay.addEventListener('keydown', refocusTarget)
133
- removeRefocusTargetListener = () => overlay.removeEventListener('keydown', refocusTarget)
134
-
135
- // auto-focus
136
- const openedWithMouse = event instanceof MouseEvent && event.detail > 0
137
- if (autoFocusBehavior === 'always' || (autoFocusBehavior === 'keyboard' && !openedWithMouse)) {
138
- setTimeout(() => focusFirstChild(overlay), arbitraryRenderTime)
139
- }
140
- }
141
-
142
- controller.current.close = async (immediately = false) => {
143
- if (!immediately && (closeDelayMS ?? 0) > 0) {
144
- await delay(closeDelayMS ?? 0)
145
- }
146
- visible = false
147
- if (hideOnClickOutside) document.removeEventListener('click', hideOnClickOutside)
148
- if (hideOnEsc) document.removeEventListener('keydown', hideOnEsc)
149
- removeRefocusTargetListener?.()
150
- await hideOverlay?.()
151
- }
152
-
153
- if (triggerOn === 'hover') {
154
- const close = () => controller.current.close()
155
- getTarget()?.addEventListener('mouseenter', show)
156
- getTarget()?.addEventListener('mouseleave', close)
157
- getTarget()?.addEventListener('focus', show)
158
- getTarget()?.addEventListener('blur', close)
159
- return () => {
160
- getTarget()?.removeEventListener('mouseenter', show)
161
- getTarget()?.removeEventListener('mouseleave', close)
162
- getTarget()?.removeEventListener('focus', show)
163
- getTarget()?.removeEventListener('blur', close)
164
- }
165
- }
166
-
167
- if (triggerOn === 'click') {
168
- getTarget()?.addEventListener('click', show)
169
- return () => {
170
- controller.current.close()
171
- getTarget()?.removeEventListener('click', show)
172
- if (hideOnClickOutside) document.removeEventListener('click', hideOnClickOutside)
173
- }
174
- }
175
- }, [wrapper.current, triggerOn])
176
-
177
- useEffect(() => () => {
178
- controller.current?.close(true)
179
- }, [])
180
-
181
- return <div ref={wrapper} {...props}>{children}</div>
182
- }
1
+ import { useEffect, useRef } from 'react'
2
+ import { showOverlay } from '../../overlay'
3
+ import { HTMLTag } from '../../types'
4
+ import { focusFirstChild } from '../../utils/acessibility'
5
+ import { delay } from '../../utils/time'
6
+ import { OverlayProvider } from './context'
7
+ import { OverlayController, OverlayProps } from './types'
8
+ export { useOverlayController } from './context'
9
+
10
+ /**
11
+ * An arbitrary time to wait for the next React render to be performed
12
+ */
13
+ const arbitraryRenderTime = 20
14
+
15
+ /**
16
+ * These todos are in order of priority.
17
+ *
18
+ * TODO: update position when the size changes. Currently, the top position seems out of place whenever the height changes. The same is
19
+ * probably true for the left position if the width changes.
20
+ * TODO (minor): use React Portal to implement overlays. The current implementation will lose every React context in the tree.
21
+ */
22
+
23
+ /**
24
+ * Creates an overlay for the child component. The overlay can be any React element. The overlay can be triggered by "click" or "hover"
25
+ * (default).
26
+ *
27
+ * The overlay can be hidden from within its content through the hook `useOverlayController()`.
28
+ *
29
+ * @example
30
+ *
31
+ * ```
32
+ * const overlay = <Card>Hey, this is my overlay!</Card>
33
+ *
34
+ * return (
35
+ * <Overlay content={overlay} attributes={{ style: { margin: '20px' } }}>
36
+ * <Button>Hover to see the overlay</Button>
37
+ * </Overlay>
38
+ * )
39
+ * ```
40
+ */
41
+ export function Overlay<T extends keyof HTMLTag>({
42
+ tag,
43
+ children,
44
+ content,
45
+ position = 'top',
46
+ triggerOn = 'hover',
47
+ alignment = 'center',
48
+ attributes,
49
+ onRenderChild,
50
+ autoFocusBehavior = 'keyboard',
51
+ openDelayMS,
52
+ closeDelayMS,
53
+ ...props
54
+ }: OverlayProps<T>,
55
+ ) {
56
+ const controller = useRef<OverlayController>({ close: () => Promise.resolve() })
57
+ const wrapper = useRef<HTMLDivElement | null>(null)
58
+ // props that don't require removing and reattaching the event listeners
59
+ const dynamic = useRef({ tag, content, position, alignment, attributes })
60
+
61
+ useEffect(() => {
62
+ dynamic.current = { tag, content, position, alignment, attributes }
63
+ }, [tag, content, position, alignment, attributes])
64
+
65
+ useEffect(() => {
66
+ let visible = false
67
+ let hideOnClickOutside: ((event: Event) => void) | undefined
68
+ let hideOnEsc: ((event: Event) => void) | undefined
69
+ let hideOverlay: ((immediately?: boolean) => Promise<void>) | undefined
70
+ let removeRefocusTargetListener: (() => void) | undefined
71
+
72
+ function getTarget() {
73
+ const target = wrapper.current?.firstChild
74
+ return target instanceof HTMLElement ? target : undefined
75
+ }
76
+
77
+ if (onRenderChild) {
78
+ const target = getTarget()
79
+ if (target) onRenderChild(target)
80
+ }
81
+
82
+ async function show(event: Event) {
83
+ if (visible) return
84
+ visible = true
85
+ const target = getTarget()
86
+ if (openDelayMS && triggerOn === 'hover') {
87
+ await delay(openDelayMS)
88
+ if (!target?.matches(':hover, :focus')) {
89
+ visible = false
90
+ return
91
+ }
92
+ }
93
+ if (!target) return
94
+ const { overlay, hide: hideFn } = showOverlay({
95
+ tag: dynamic.current.tag,
96
+ content: ['string', 'number', 'boolean'].includes(typeof dynamic.current.content)
97
+ ? dynamic.current.content
98
+ : <OverlayProvider value={controller.current}>{dynamic.current.content}</OverlayProvider>,
99
+ target,
100
+ position: dynamic.current.position,
101
+ alignment: dynamic.current.alignment,
102
+ attributes: dynamic.current.attributes,
103
+ })
104
+ hideOverlay?.(true) // ensures there's no lost opened overlay.
105
+ hideOverlay = hideFn
106
+
107
+ function onHide(condition: (event: Event) => boolean) {
108
+ return (event: Event) => {
109
+ if (condition(event)) controller.current.close()
110
+ }
111
+ }
112
+
113
+ if (event.type === 'click') {
114
+ hideOnEsc = onHide(e => e instanceof KeyboardEvent && e.key === 'Escape')
115
+ setTimeout(() => {
116
+ hideOnClickOutside = onHide(e => e instanceof MouseEvent && e.button === 0 && !overlay.contains(e.target as HTMLElement))
117
+ document.addEventListener('click', hideOnClickOutside)
118
+ }, arbitraryRenderTime)
119
+ document.addEventListener('keydown', hideOnEsc)
120
+ }
121
+
122
+ // focus target when the last overlay element loses focus
123
+ function refocusTarget(e: KeyboardEvent) {
124
+ if (e.key === 'Tab' && e.target instanceof HTMLElement) {
125
+ const allItems = Array.from(e.target.closest('[data-citric="menu"]')?.querySelectorAll('a, button') ?? [])
126
+ if (e.target === allItems[allItems.length - 1]) {
127
+ getTarget()?.focus()
128
+ e.preventDefault()
129
+ }
130
+ }
131
+ }
132
+ overlay.addEventListener('keydown', refocusTarget)
133
+ removeRefocusTargetListener = () => overlay.removeEventListener('keydown', refocusTarget)
134
+
135
+ // auto-focus
136
+ const openedWithMouse = event instanceof MouseEvent && event.detail > 0
137
+ if (autoFocusBehavior === 'always' || (autoFocusBehavior === 'keyboard' && !openedWithMouse)) {
138
+ setTimeout(() => focusFirstChild(overlay), arbitraryRenderTime)
139
+ }
140
+ }
141
+
142
+ controller.current.close = async (immediately = false) => {
143
+ if (!immediately && (closeDelayMS ?? 0) > 0) {
144
+ await delay(closeDelayMS ?? 0)
145
+ }
146
+ visible = false
147
+ if (hideOnClickOutside) document.removeEventListener('click', hideOnClickOutside)
148
+ if (hideOnEsc) document.removeEventListener('keydown', hideOnEsc)
149
+ removeRefocusTargetListener?.()
150
+ await hideOverlay?.()
151
+ }
152
+
153
+ if (triggerOn === 'hover') {
154
+ const close = () => controller.current.close()
155
+ getTarget()?.addEventListener('mouseenter', show)
156
+ getTarget()?.addEventListener('mouseleave', close)
157
+ getTarget()?.addEventListener('focus', show)
158
+ getTarget()?.addEventListener('blur', close)
159
+ return () => {
160
+ getTarget()?.removeEventListener('mouseenter', show)
161
+ getTarget()?.removeEventListener('mouseleave', close)
162
+ getTarget()?.removeEventListener('focus', show)
163
+ getTarget()?.removeEventListener('blur', close)
164
+ }
165
+ }
166
+
167
+ if (triggerOn === 'click') {
168
+ getTarget()?.addEventListener('click', show)
169
+ return () => {
170
+ controller.current.close()
171
+ getTarget()?.removeEventListener('click', show)
172
+ if (hideOnClickOutside) document.removeEventListener('click', hideOnClickOutside)
173
+ }
174
+ }
175
+ }, [wrapper.current, triggerOn])
176
+
177
+ useEffect(() => () => {
178
+ controller.current?.close(true)
179
+ }, [])
180
+
181
+ return <div ref={wrapper} {...props}>{children}</div>
182
+ }
@@ -1,75 +1,75 @@
1
- import { OverlayOptions } from '../../overlay'
2
- import { HTMLTag } from '../../types'
3
-
4
- export type TriggerOn = 'hover' | 'click'
5
-
6
- export interface BaseOverlayProps<T extends keyof HTMLTag> extends Omit<OverlayOptions<T>, 'target'> {
7
- /**
8
- * When should the overlay element be created? When the child is clicked or when the child is hovered?
9
- *
10
- * Accessibility:
11
- * Click = focus + press enter to open; focus + enter to close OR esc.
12
- * Hover = focus to open; blur to close.
13
- *
14
- * @default 'hover'
15
- */
16
- triggerOn?: TriggerOn,
17
- /**
18
- * Only valid if `triggerOn` is "hover".
19
- *
20
- * Waits the given amount of milliseconds before closing the overlay.
21
- *
22
- * @default 0
23
- */
24
- closeDelayMS?: number,
25
- /**
26
- * Only valid if `triggerOn` is "hover".
27
- *
28
- * Waits the given amount of milliseconds before opening the overlay.
29
- *
30
- * @default 0
31
- */
32
- openDelayMS?: number,
33
- /**
34
- * TODO: not implemented yet.
35
- *
36
- * - Never: the focus won't changes when the overlay opens.
37
- * - All: the focus always changes when the overlay opens (given there's a focusable element in the overlay).
38
- * - Keyboard (default): the focus only changes when the overlay is opened via the keyboard.
39
- *
40
- * The first focusable element in the overlay will be focused as soon as it's rendered. When it's closed, the child element
41
- * (`children`) regains focus.
42
- *
43
- * The focus control will be such that, after the last element in the overlay is focused, the next focus will move to the child element
44
- * (`children`).
45
- *
46
- * If the overlay has no focusable element, this properties makes no difference.
47
- *
48
- * Attention: focusable elements inside the overlay can be ignored by setting `auto-focus` to false.
49
- *
50
- * @default 'keyboard'
51
- */
52
- autoFocusBehavior?: 'never' | 'always' | 'keyboard',
53
- /**
54
- * The element to receive the overlay.
55
- */
56
- children: React.ReactElement,
57
- /**
58
- * Function to run when the child is rendered. It receives the child element as a parameter.
59
- *
60
- * This is useful for easily adding attributes to the element (mainly accessibility ones).
61
- * @param element the child element, the one that receives the overlay.
62
- */
63
- onRenderChild?: (element: HTMLElement) => void,
64
- }
65
-
66
- export type OverlayProps<T extends keyof HTMLTag> = Omit<React.JSX.IntrinsicElements['div'], 'content'> & BaseOverlayProps<T>
67
-
68
- export interface OverlayController {
69
- /**
70
- * Closes the tooltip.
71
- * @param immediately when true, ignores the closeDelayMS parameter.
72
- * @returns a promise that resolves when the overlay is removed from the DOM.
73
- */
74
- close: (immediately?: boolean) => Promise<void>,
75
- }
1
+ import { OverlayOptions } from '../../overlay'
2
+ import { HTMLTag } from '../../types'
3
+
4
+ export type TriggerOn = 'hover' | 'click'
5
+
6
+ export interface BaseOverlayProps<T extends keyof HTMLTag> extends Omit<OverlayOptions<T>, 'target'> {
7
+ /**
8
+ * When should the overlay element be created? When the child is clicked or when the child is hovered?
9
+ *
10
+ * Accessibility:
11
+ * Click = focus + press enter to open; focus + enter to close OR esc.
12
+ * Hover = focus to open; blur to close.
13
+ *
14
+ * @default 'hover'
15
+ */
16
+ triggerOn?: TriggerOn,
17
+ /**
18
+ * Only valid if `triggerOn` is "hover".
19
+ *
20
+ * Waits the given amount of milliseconds before closing the overlay.
21
+ *
22
+ * @default 0
23
+ */
24
+ closeDelayMS?: number,
25
+ /**
26
+ * Only valid if `triggerOn` is "hover".
27
+ *
28
+ * Waits the given amount of milliseconds before opening the overlay.
29
+ *
30
+ * @default 0
31
+ */
32
+ openDelayMS?: number,
33
+ /**
34
+ * TODO: not implemented yet.
35
+ *
36
+ * - Never: the focus won't changes when the overlay opens.
37
+ * - All: the focus always changes when the overlay opens (given there's a focusable element in the overlay).
38
+ * - Keyboard (default): the focus only changes when the overlay is opened via the keyboard.
39
+ *
40
+ * The first focusable element in the overlay will be focused as soon as it's rendered. When it's closed, the child element
41
+ * (`children`) regains focus.
42
+ *
43
+ * The focus control will be such that, after the last element in the overlay is focused, the next focus will move to the child element
44
+ * (`children`).
45
+ *
46
+ * If the overlay has no focusable element, this properties makes no difference.
47
+ *
48
+ * Attention: focusable elements inside the overlay can be ignored by setting `auto-focus` to false.
49
+ *
50
+ * @default 'keyboard'
51
+ */
52
+ autoFocusBehavior?: 'never' | 'always' | 'keyboard',
53
+ /**
54
+ * The element to receive the overlay.
55
+ */
56
+ children: React.ReactElement,
57
+ /**
58
+ * Function to run when the child is rendered. It receives the child element as a parameter.
59
+ *
60
+ * This is useful for easily adding attributes to the element (mainly accessibility ones).
61
+ * @param element the child element, the one that receives the overlay.
62
+ */
63
+ onRenderChild?: (element: HTMLElement) => void,
64
+ }
65
+
66
+ export type OverlayProps<T extends keyof HTMLTag> = Omit<React.JSX.IntrinsicElements['div'], 'content'> & BaseOverlayProps<T>
67
+
68
+ export interface OverlayController {
69
+ /**
70
+ * Closes the tooltip.
71
+ * @param immediately when true, ignores the closeDelayMS parameter.
72
+ * @returns a promise that resolves when the overlay is removed from the DOM.
73
+ */
74
+ close: (immediately?: boolean) => Promise<void>,
75
+ }