@stack-spot/citric-react 0.41.2 → 0.42.0-beta.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 (204) hide show
  1. package/CHANGELOG.md +13 -13
  2. package/dist/citric.css +3090 -2846
  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/Autocomplete.d.ts +211 -0
  10. package/dist/components/Autocomplete/Autocomplete.d.ts.map +1 -0
  11. package/dist/components/Autocomplete/Autocomplete.js +409 -0
  12. package/dist/components/Autocomplete/Autocomplete.js.map +1 -0
  13. package/dist/components/Autocomplete/index.d.ts +3 -0
  14. package/dist/components/Autocomplete/index.d.ts.map +1 -0
  15. package/dist/components/Autocomplete/index.js +2 -0
  16. package/dist/components/Autocomplete/index.js.map +1 -0
  17. package/dist/components/Avatar.d.ts +1 -1
  18. package/dist/components/Avatar.js +1 -1
  19. package/dist/components/AvatarGroup.d.ts +1 -1
  20. package/dist/components/AvatarGroup.js +1 -1
  21. package/dist/components/Badge.d.ts +1 -1
  22. package/dist/components/Badge.js +1 -1
  23. package/dist/components/Blockquote.d.ts +1 -1
  24. package/dist/components/Blockquote.js +1 -1
  25. package/dist/components/Breadcrumb.d.ts +1 -1
  26. package/dist/components/Breadcrumb.js +1 -1
  27. package/dist/components/Button.d.ts +1 -1
  28. package/dist/components/Button.js +1 -1
  29. package/dist/components/ButtonLink.d.ts +1 -1
  30. package/dist/components/ButtonLink.js +1 -1
  31. package/dist/components/Card.d.ts +1 -1
  32. package/dist/components/Card.js +1 -1
  33. package/dist/components/Checkbox.d.ts +1 -1
  34. package/dist/components/Checkbox.d.ts.map +1 -1
  35. package/dist/components/Checkbox.js +2 -2
  36. package/dist/components/Checkbox.js.map +1 -1
  37. package/dist/components/CheckboxGroup.d.ts +1 -1
  38. package/dist/components/CheckboxGroup.js +1 -1
  39. package/dist/components/Circle.d.ts +1 -1
  40. package/dist/components/Circle.js +1 -1
  41. package/dist/components/CitricComponent.d.ts +1 -1
  42. package/dist/components/CitricComponent.d.ts.map +1 -1
  43. package/dist/components/Divider.d.ts +1 -1
  44. package/dist/components/Divider.js +1 -1
  45. package/dist/components/ErrorBoundary.d.ts +1 -1
  46. package/dist/components/ErrorBoundary.js +1 -1
  47. package/dist/components/ErrorMessage.d.ts +1 -1
  48. package/dist/components/ErrorMessage.js +1 -1
  49. package/dist/components/FallbackBoundary.d.ts +1 -1
  50. package/dist/components/FallbackBoundary.js +1 -1
  51. package/dist/components/Favorite.d.ts +1 -1
  52. package/dist/components/Favorite.js +1 -1
  53. package/dist/components/FieldGroup.d.ts +1 -1
  54. package/dist/components/FieldGroup.js +1 -1
  55. package/dist/components/Form.d.ts +2 -2
  56. package/dist/components/Form.js +1 -1
  57. package/dist/components/FormGroup.d.ts +1 -1
  58. package/dist/components/FormGroup.js +1 -1
  59. package/dist/components/Icon.d.ts +1 -1
  60. package/dist/components/Icon.js +1 -1
  61. package/dist/components/IconBox.d.ts +3 -3
  62. package/dist/components/IconBox.js +1 -1
  63. package/dist/components/ImageBox.d.ts +3 -3
  64. package/dist/components/ImageBox.js +1 -1
  65. package/dist/components/ImageWithFallback.d.ts +1 -1
  66. package/dist/components/ImageWithFallback.js +1 -1
  67. package/dist/components/Input.d.ts +1 -1
  68. package/dist/components/Input.js +1 -1
  69. package/dist/components/Link.d.ts +1 -1
  70. package/dist/components/Link.js +1 -1
  71. package/dist/components/LoadingPanel.d.ts +1 -1
  72. package/dist/components/LoadingPanel.js +1 -1
  73. package/dist/components/MenuOverlay/Menu.d.ts +1 -1
  74. package/dist/components/MenuOverlay/Menu.js +1 -1
  75. package/dist/components/MenuOverlay/index.d.ts +1 -1
  76. package/dist/components/MenuOverlay/index.js +1 -1
  77. package/dist/components/Overlay/index.d.ts +1 -1
  78. package/dist/components/Overlay/index.js +1 -1
  79. package/dist/components/Pagination.d.ts +1 -1
  80. package/dist/components/Pagination.js +1 -1
  81. package/dist/components/ProgressBar.d.ts +1 -1
  82. package/dist/components/ProgressBar.js +1 -1
  83. package/dist/components/ProgressCircular.d.ts +1 -1
  84. package/dist/components/ProgressCircular.js +1 -1
  85. package/dist/components/RadioGroup.d.ts +1 -1
  86. package/dist/components/RadioGroup.js +1 -1
  87. package/dist/components/Rating.d.ts +1 -1
  88. package/dist/components/Rating.js +1 -1
  89. package/dist/components/Select/MultiSelect.d.ts +1 -1
  90. package/dist/components/Select/MultiSelect.js +1 -1
  91. package/dist/components/Select/RichSelect.d.ts +1 -1
  92. package/dist/components/Select/RichSelect.js +1 -1
  93. package/dist/components/Select/SimpleSelect.d.ts +1 -1
  94. package/dist/components/Select/SimpleSelect.js +1 -1
  95. package/dist/components/Select/index.d.ts +1 -1
  96. package/dist/components/Select/index.js +1 -1
  97. package/dist/components/SelectBox.d.ts +1 -1
  98. package/dist/components/SelectBox.js +1 -1
  99. package/dist/components/Skeleton.d.ts +1 -1
  100. package/dist/components/Skeleton.js +1 -1
  101. package/dist/components/Slider.d.ts +1 -1
  102. package/dist/components/Slider.js +1 -1
  103. package/dist/components/SmartTable.d.ts +1 -1
  104. package/dist/components/SmartTable.js +1 -1
  105. package/dist/components/Stepper.d.ts +1 -1
  106. package/dist/components/Stepper.js +1 -1
  107. package/dist/components/Table.d.ts +3 -3
  108. package/dist/components/Table.js +1 -1
  109. package/dist/components/Tabs/index.d.ts +1 -1
  110. package/dist/components/Tabs/index.js +1 -1
  111. package/dist/components/Textarea.d.ts +1 -1
  112. package/dist/components/Textarea.js +1 -1
  113. package/dist/components/Tooltip.d.ts +1 -1
  114. package/dist/components/Tooltip.js +1 -1
  115. package/dist/context/CitricProvider.d.ts +1 -1
  116. package/dist/context/CitricProvider.js +1 -1
  117. package/dist/index.d.ts +1 -0
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +1 -0
  120. package/dist/index.js.map +1 -1
  121. package/dist/overlay.js +1 -1
  122. package/dist/theme.css +415 -415
  123. package/package.json +1 -1
  124. package/scripts/build-css.ts +49 -49
  125. package/src/components/Accordion.tsx +130 -130
  126. package/src/components/Alert.tsx +24 -24
  127. package/src/components/AsyncContent.tsx +75 -75
  128. package/src/components/Autocomplete/Autocomplete.tsx +794 -0
  129. package/src/components/Autocomplete/index.ts +3 -0
  130. package/src/components/Avatar.tsx +45 -45
  131. package/src/components/AvatarGroup.tsx +49 -49
  132. package/src/components/Badge.tsx +47 -47
  133. package/src/components/Blockquote.tsx +18 -18
  134. package/src/components/Breadcrumb.tsx +33 -33
  135. package/src/components/Button.tsx +105 -105
  136. package/src/components/ButtonLink.tsx +45 -45
  137. package/src/components/Card.tsx +68 -68
  138. package/src/components/Checkbox.tsx +52 -51
  139. package/src/components/CheckboxGroup.tsx +153 -153
  140. package/src/components/Circle.tsx +43 -43
  141. package/src/components/CitricComponent.ts +47 -47
  142. package/src/components/Divider.tsx +24 -24
  143. package/src/components/ErrorBoundary.tsx +75 -75
  144. package/src/components/ErrorMessage.tsx +11 -11
  145. package/src/components/FallbackBoundary.tsx +40 -40
  146. package/src/components/Favorite.tsx +57 -57
  147. package/src/components/FieldGroup.tsx +46 -46
  148. package/src/components/Form.tsx +36 -36
  149. package/src/components/FormGroup.tsx +57 -57
  150. package/src/components/Icon.tsx +35 -35
  151. package/src/components/IconBox.tsx +134 -134
  152. package/src/components/ImageBox.tsx +125 -125
  153. package/src/components/ImageWithFallback.tsx +65 -65
  154. package/src/components/Input.tsx +49 -49
  155. package/src/components/Link.tsx +55 -55
  156. package/src/components/LoadingPanel.tsx +12 -12
  157. package/src/components/MenuOverlay/Menu.tsx +158 -158
  158. package/src/components/MenuOverlay/context.ts +20 -20
  159. package/src/components/MenuOverlay/index.tsx +55 -55
  160. package/src/components/MenuOverlay/keyboard.ts +60 -60
  161. package/src/components/MenuOverlay/types.ts +171 -171
  162. package/src/components/Overlay/context.ts +10 -10
  163. package/src/components/Overlay/index.tsx +182 -182
  164. package/src/components/Overlay/types.ts +75 -75
  165. package/src/components/Pagination.tsx +133 -133
  166. package/src/components/ProgressBar.tsx +45 -45
  167. package/src/components/ProgressCircular.tsx +45 -45
  168. package/src/components/RadioGroup.tsx +147 -147
  169. package/src/components/Rating.tsx +98 -98
  170. package/src/components/Select/MultiSelect.tsx +217 -217
  171. package/src/components/Select/RichSelect.tsx +128 -128
  172. package/src/components/Select/SimpleSelect.tsx +73 -73
  173. package/src/components/Select/hooks.ts +133 -133
  174. package/src/components/Select/index.tsx +35 -35
  175. package/src/components/Select/types.ts +134 -134
  176. package/src/components/SelectBox.tsx +167 -167
  177. package/src/components/Skeleton.tsx +53 -53
  178. package/src/components/Slider.tsx +89 -89
  179. package/src/components/SmartTable.tsx +227 -227
  180. package/src/components/Stepper.tsx +163 -163
  181. package/src/components/Table.tsx +234 -234
  182. package/src/components/Tabs/TabController.ts +54 -54
  183. package/src/components/Tabs/index.tsx +106 -106
  184. package/src/components/Tabs/types.ts +67 -67
  185. package/src/components/Tabs/utils.ts +6 -6
  186. package/src/components/Text.ts +111 -111
  187. package/src/components/Textarea.tsx +27 -27
  188. package/src/components/Tooltip.tsx +83 -83
  189. package/src/components/layout.tsx +101 -101
  190. package/src/context/CitricContext.tsx +4 -4
  191. package/src/context/CitricProvider.tsx +14 -14
  192. package/src/context/hooks.ts +6 -6
  193. package/src/index.ts +59 -58
  194. package/src/overlay.ts +348 -348
  195. package/src/types.ts +235 -235
  196. package/src/utils/ValueController.ts +28 -28
  197. package/src/utils/acessibility.ts +92 -92
  198. package/src/utils/checkbox.ts +121 -121
  199. package/src/utils/css.ts +119 -119
  200. package/src/utils/options.ts +9 -9
  201. package/src/utils/radio.ts +93 -93
  202. package/src/utils/react.ts +6 -6
  203. package/src/utils/time.ts +5 -5
  204. package/tsconfig.json +10 -10
@@ -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
+ }