@stack-spot/citric-react 0.35.1 → 0.37.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.
- package/dist/citric.css +2844 -2832
- package/dist/components/Accordion.d.ts +1 -1
- package/dist/components/Accordion.js +1 -1
- package/dist/components/Alert.d.ts +1 -1
- package/dist/components/Alert.js +1 -1
- package/dist/components/AsyncContent.d.ts +1 -1
- package/dist/components/AsyncContent.js +1 -1
- package/dist/components/Avatar.d.ts +1 -1
- package/dist/components/Avatar.js +1 -1
- package/dist/components/AvatarGroup.d.ts +1 -1
- package/dist/components/AvatarGroup.js +1 -1
- package/dist/components/Badge.d.ts +1 -1
- package/dist/components/Badge.js +1 -1
- package/dist/components/Blockquote.d.ts +1 -1
- package/dist/components/Blockquote.js +1 -1
- package/dist/components/Breadcrumb.d.ts +1 -1
- package/dist/components/Breadcrumb.js +1 -1
- package/dist/components/Button.d.ts +1 -1
- package/dist/components/Button.js +1 -1
- package/dist/components/ButtonLink.d.ts +1 -1
- package/dist/components/ButtonLink.js +1 -1
- package/dist/components/Card.d.ts +1 -1
- package/dist/components/Card.js +1 -1
- package/dist/components/Checkbox.d.ts +1 -1
- package/dist/components/Checkbox.js +1 -1
- package/dist/components/CheckboxGroup.d.ts +1 -1
- package/dist/components/CheckboxGroup.js +1 -1
- package/dist/components/Circle.d.ts +1 -1
- package/dist/components/Circle.js +1 -1
- package/dist/components/Divider.d.ts +1 -1
- package/dist/components/Divider.js +1 -1
- package/dist/components/ErrorBoundary.d.ts +1 -1
- package/dist/components/ErrorBoundary.js +1 -1
- package/dist/components/ErrorMessage.d.ts +1 -1
- package/dist/components/ErrorMessage.js +1 -1
- package/dist/components/FallbackBoundary.d.ts +1 -1
- package/dist/components/FallbackBoundary.js +1 -1
- package/dist/components/Favorite.d.ts +1 -1
- package/dist/components/Favorite.js +1 -1
- package/dist/components/FieldGroup.d.ts +1 -1
- package/dist/components/FieldGroup.js +1 -1
- package/dist/components/Form.d.ts +2 -2
- package/dist/components/Form.js +1 -1
- package/dist/components/FormGroup.d.ts +1 -1
- package/dist/components/FormGroup.js +1 -1
- package/dist/components/Icon.d.ts +1 -1
- package/dist/components/Icon.js +1 -1
- package/dist/components/IconBox.d.ts +3 -3
- package/dist/components/IconBox.js +1 -1
- package/dist/components/ImageBox.d.ts +3 -3
- package/dist/components/ImageBox.js +1 -1
- package/dist/components/ImageWithFallback.d.ts +1 -1
- package/dist/components/ImageWithFallback.js +1 -1
- package/dist/components/Input.d.ts +1 -1
- package/dist/components/Input.js +1 -1
- package/dist/components/Link.d.ts +1 -1
- package/dist/components/Link.js +1 -1
- package/dist/components/LoadingPanel.d.ts +1 -1
- package/dist/components/LoadingPanel.js +1 -1
- package/dist/components/MenuOverlay/Menu.d.ts +1 -1
- package/dist/components/MenuOverlay/Menu.js +1 -1
- package/dist/components/MenuOverlay/index.d.ts +1 -1
- package/dist/components/MenuOverlay/index.js +1 -1
- package/dist/components/Overlay/index.d.ts +1 -1
- package/dist/components/Overlay/index.js +1 -1
- package/dist/components/Pagination.d.ts +1 -1
- package/dist/components/Pagination.js +1 -1
- package/dist/components/ProgressBar.d.ts +1 -1
- package/dist/components/ProgressBar.js +1 -1
- package/dist/components/ProgressCircular.d.ts +1 -1
- package/dist/components/ProgressCircular.js +1 -1
- package/dist/components/RadioGroup.d.ts +1 -1
- package/dist/components/RadioGroup.js +1 -1
- package/dist/components/Rating.d.ts +17 -3
- package/dist/components/Rating.d.ts.map +1 -1
- package/dist/components/Rating.js +11 -3
- package/dist/components/Rating.js.map +1 -1
- package/dist/components/Select/MultiSelect.d.ts +1 -1
- package/dist/components/Select/MultiSelect.js +1 -1
- package/dist/components/Select/RichSelect.d.ts +1 -1
- package/dist/components/Select/RichSelect.js +1 -1
- package/dist/components/Select/SimpleSelect.d.ts +1 -1
- package/dist/components/Select/SimpleSelect.js +1 -1
- package/dist/components/Select/index.d.ts +1 -1
- package/dist/components/Select/index.js +1 -1
- package/dist/components/SelectBox.d.ts +1 -1
- package/dist/components/SelectBox.js +1 -1
- package/dist/components/Skeleton.d.ts +1 -1
- package/dist/components/Skeleton.js +1 -1
- package/dist/components/Slider.d.ts +1 -1
- package/dist/components/Slider.js +1 -1
- package/dist/components/SmartTable.d.ts +1 -1
- package/dist/components/SmartTable.js +1 -1
- package/dist/components/Stepper.d.ts +1 -1
- package/dist/components/Stepper.js +1 -1
- package/dist/components/Table.d.ts +3 -3
- package/dist/components/Table.js +1 -1
- package/dist/components/Tabs/index.d.ts +1 -1
- package/dist/components/Tabs/index.js +1 -1
- package/dist/components/Textarea.d.ts +1 -1
- package/dist/components/Textarea.js +1 -1
- package/dist/components/Tooltip.d.ts +1 -1
- package/dist/components/Tooltip.js +1 -1
- package/dist/context/CitricProvider.d.ts +1 -1
- package/dist/context/CitricProvider.js +1 -1
- package/dist/overlay.js +1 -1
- package/dist/theme.css +415 -415
- package/package.json +7 -6
- package/scripts/build-css.ts +49 -49
- package/src/components/Accordion.tsx +130 -130
- package/src/components/Alert.tsx +24 -24
- package/src/components/AsyncContent.tsx +70 -70
- package/src/components/Avatar.tsx +45 -45
- package/src/components/AvatarGroup.tsx +49 -49
- package/src/components/Badge.tsx +47 -47
- package/src/components/Blockquote.tsx +18 -18
- package/src/components/Breadcrumb.tsx +33 -33
- package/src/components/Button.tsx +105 -105
- package/src/components/ButtonLink.tsx +45 -45
- package/src/components/Card.tsx +68 -68
- package/src/components/Checkbox.tsx +51 -51
- package/src/components/CheckboxGroup.tsx +152 -152
- package/src/components/Circle.tsx +43 -43
- package/src/components/CitricComponent.ts +47 -47
- package/src/components/Divider.tsx +24 -24
- package/src/components/ErrorBoundary.tsx +75 -75
- package/src/components/ErrorMessage.tsx +11 -11
- package/src/components/FallbackBoundary.tsx +40 -40
- package/src/components/Favorite.tsx +57 -57
- package/src/components/FieldGroup.tsx +46 -46
- package/src/components/Form.tsx +36 -36
- package/src/components/FormGroup.tsx +57 -57
- package/src/components/Icon.tsx +35 -35
- package/src/components/IconBox.tsx +134 -134
- package/src/components/ImageBox.tsx +125 -125
- package/src/components/ImageWithFallback.tsx +65 -65
- package/src/components/Input.tsx +49 -49
- package/src/components/Link.tsx +55 -55
- package/src/components/LoadingPanel.tsx +8 -8
- package/src/components/MenuOverlay/Menu.tsx +158 -158
- package/src/components/MenuOverlay/context.ts +20 -20
- package/src/components/MenuOverlay/index.tsx +55 -55
- package/src/components/MenuOverlay/keyboard.ts +60 -60
- package/src/components/MenuOverlay/types.ts +171 -171
- package/src/components/Overlay/context.ts +10 -10
- package/src/components/Overlay/index.tsx +164 -164
- package/src/components/Overlay/types.ts +70 -70
- package/src/components/Pagination.tsx +113 -113
- package/src/components/ProgressBar.tsx +45 -45
- package/src/components/ProgressCircular.tsx +45 -45
- package/src/components/RadioGroup.tsx +146 -146
- package/src/components/Rating.tsx +98 -35
- package/src/components/Select/MultiSelect.tsx +217 -217
- package/src/components/Select/RichSelect.tsx +128 -128
- package/src/components/Select/SimpleSelect.tsx +73 -73
- package/src/components/Select/hooks.ts +133 -133
- package/src/components/Select/index.tsx +35 -35
- package/src/components/Select/types.ts +134 -134
- package/src/components/SelectBox.tsx +167 -167
- package/src/components/Skeleton.tsx +53 -53
- package/src/components/Slider.tsx +89 -89
- package/src/components/SmartTable.tsx +227 -227
- package/src/components/Stepper.tsx +163 -163
- package/src/components/Table.tsx +234 -234
- package/src/components/Tabs/TabController.ts +54 -54
- package/src/components/Tabs/index.tsx +87 -87
- package/src/components/Tabs/types.ts +54 -54
- package/src/components/Tabs/utils.ts +6 -6
- package/src/components/Text.ts +111 -111
- package/src/components/Textarea.tsx +27 -27
- package/src/components/Tooltip.tsx +72 -72
- package/src/components/layout.tsx +101 -101
- package/src/context/CitricContext.tsx +4 -4
- package/src/context/CitricProvider.tsx +14 -14
- package/src/context/hooks.ts +6 -6
- package/src/index.ts +58 -58
- package/src/overlay.ts +341 -341
- package/src/types.ts +216 -216
- package/src/utils/ValueController.ts +28 -28
- package/src/utils/acessibility.ts +92 -92
- package/src/utils/checkbox.ts +121 -121
- package/src/utils/css.ts +119 -119
- package/src/utils/options.ts +9 -9
- package/src/utils/radio.ts +93 -93
- package/src/utils/react.ts +6 -6
- package/tsconfig.json +10 -10
|
@@ -1,164 +1,164 @@
|
|
|
1
|
-
import { useEffect, useRef } from 'react'
|
|
2
|
-
import { showOverlay } from '../../overlay'
|
|
3
|
-
import { HTMLTag } from '../../types'
|
|
4
|
-
import { focusFirstChild } from '../../utils/acessibility'
|
|
5
|
-
import { OverlayProvider } from './context'
|
|
6
|
-
import { OverlayController, OverlayProps } from './types'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* An arbitrary time to wait for the next React render to be performed
|
|
10
|
-
*/
|
|
11
|
-
const arbitraryRenderTime = 20
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* These todos are in order of priority.
|
|
15
|
-
*
|
|
16
|
-
* TODO: update position when the size changes. Currently, the top position seems out of place whenever the height changes. The same is
|
|
17
|
-
* probably true for the left position if the width changes.
|
|
18
|
-
* TODO: hoverDelayMS
|
|
19
|
-
* TODO: reposition the overlay when it's under a scrollable element other then the body and this element is scrolled.
|
|
20
|
-
* TODO: close the overlay when it's under a scrollable element other then the body and the element that triggered the tooltip becomes
|
|
21
|
-
* hidden by the scroll.
|
|
22
|
-
* TODO: use React Portal to implement overlays. The current implementation will lose every React context in the tree.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Creates an overlay for the child component. The overlay can be any React element. The overlay can be triggered by "click" or "hover"
|
|
27
|
-
* (default).
|
|
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
|
-
...props
|
|
52
|
-
}: OverlayProps<T>,
|
|
53
|
-
) {
|
|
54
|
-
const controller = useRef<OverlayController>({ close: () => Promise.resolve() })
|
|
55
|
-
const wrapper = useRef<HTMLDivElement | null>(null)
|
|
56
|
-
// props that don't require removing and reattaching the event listeners
|
|
57
|
-
const dynamic = useRef({ tag, content, position, alignment, attributes })
|
|
58
|
-
|
|
59
|
-
useEffect(() => {
|
|
60
|
-
dynamic.current = { tag, content, position, alignment, attributes }
|
|
61
|
-
}, [tag, content, position, alignment, attributes])
|
|
62
|
-
|
|
63
|
-
useEffect(() => {
|
|
64
|
-
let visible = false
|
|
65
|
-
let hideOnClickOutside: ((event: Event) => void) | undefined
|
|
66
|
-
let hideOnEsc: ((event: Event) => void) | undefined
|
|
67
|
-
let hideOverlay: (() => Promise<void>) | undefined
|
|
68
|
-
let removeRefocusTargetListener: (() => void) | undefined
|
|
69
|
-
|
|
70
|
-
function getTarget() {
|
|
71
|
-
const target = wrapper.current?.firstChild
|
|
72
|
-
return target instanceof HTMLElement ? target : undefined
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (onRenderChild) {
|
|
76
|
-
const target = getTarget()
|
|
77
|
-
if (target) onRenderChild(target)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async function show(event: Event) {
|
|
81
|
-
if (visible) return
|
|
82
|
-
visible = true
|
|
83
|
-
const target = getTarget()
|
|
84
|
-
if (!target) return
|
|
85
|
-
const { overlay, hide: hideFn } = showOverlay({
|
|
86
|
-
tag: dynamic.current.tag,
|
|
87
|
-
content: ['string', 'number', 'boolean'].includes(typeof dynamic.current.content)
|
|
88
|
-
? dynamic.current.content
|
|
89
|
-
: <OverlayProvider value={controller.current}>{dynamic.current.content}</OverlayProvider>,
|
|
90
|
-
target,
|
|
91
|
-
position: dynamic.current.position,
|
|
92
|
-
alignment: dynamic.current.alignment,
|
|
93
|
-
attributes: dynamic.current.attributes,
|
|
94
|
-
})
|
|
95
|
-
hideOverlay = hideFn
|
|
96
|
-
|
|
97
|
-
function onHide(condition: (event: Event) => boolean) {
|
|
98
|
-
return (event: Event) => {
|
|
99
|
-
if (condition(event)) controller.current.close()
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (event.type === 'click') {
|
|
104
|
-
hideOnEsc = onHide(e => e instanceof KeyboardEvent && e.key === 'Escape')
|
|
105
|
-
setTimeout(() => {
|
|
106
|
-
hideOnClickOutside = onHide(e => e instanceof MouseEvent && e.button === 0 && !overlay.contains(e.target as HTMLElement))
|
|
107
|
-
document.addEventListener('click', hideOnClickOutside)
|
|
108
|
-
}, arbitraryRenderTime)
|
|
109
|
-
document.addEventListener('keydown', hideOnEsc)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
//focus target when the last overlay element loses focus
|
|
113
|
-
function refocusTarget(e: KeyboardEvent) {
|
|
114
|
-
if (e.key === 'Tab' && e.target instanceof HTMLElement) {
|
|
115
|
-
const allItems = Array.from(e.target.closest('[data-citric="menu"]')?.querySelectorAll('a, button') ?? [])
|
|
116
|
-
if (e.target === allItems[allItems.length - 1]) {
|
|
117
|
-
getTarget()?.focus()
|
|
118
|
-
e.preventDefault()
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
overlay.addEventListener('keydown', refocusTarget)
|
|
123
|
-
removeRefocusTargetListener = () => overlay.removeEventListener('keydown', refocusTarget)
|
|
124
|
-
|
|
125
|
-
// auto-focus
|
|
126
|
-
const openedWithMouse = event instanceof MouseEvent && event.detail > 0
|
|
127
|
-
if (autoFocusBehavior === 'always' || (autoFocusBehavior === 'keyboard' && !openedWithMouse)) {
|
|
128
|
-
setTimeout(() => focusFirstChild(overlay), arbitraryRenderTime)
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
controller.current.close = async () => {
|
|
133
|
-
visible = false
|
|
134
|
-
if (hideOnClickOutside) document.removeEventListener('click', hideOnClickOutside)
|
|
135
|
-
if (hideOnEsc) document.removeEventListener('keydown', hideOnEsc)
|
|
136
|
-
removeRefocusTargetListener?.()
|
|
137
|
-
await hideOverlay?.()
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (triggerOn === 'hover') {
|
|
141
|
-
getTarget()?.addEventListener('mouseenter', show)
|
|
142
|
-
getTarget()?.addEventListener('mouseleave', controller.current.close)
|
|
143
|
-
getTarget()?.addEventListener('focus', show)
|
|
144
|
-
getTarget()?.addEventListener('blur', controller.current.close)
|
|
145
|
-
return () => {
|
|
146
|
-
getTarget()?.removeEventListener('mouseenter', show)
|
|
147
|
-
getTarget()?.removeEventListener('mouseleave', controller.current.close)
|
|
148
|
-
getTarget()?.removeEventListener('focus', show)
|
|
149
|
-
getTarget()?.removeEventListener('blur', controller.current.close)
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (triggerOn === 'click') {
|
|
154
|
-
getTarget()?.addEventListener('click', show)
|
|
155
|
-
return () => {
|
|
156
|
-
controller.current.close()
|
|
157
|
-
getTarget()?.removeEventListener('click', show)
|
|
158
|
-
if (hideOnClickOutside) document.removeEventListener('click', hideOnClickOutside)
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}, [wrapper.current, triggerOn])
|
|
162
|
-
|
|
163
|
-
return <div ref={wrapper} {...props}>{children}</div>
|
|
164
|
-
}
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
import { showOverlay } from '../../overlay'
|
|
3
|
+
import { HTMLTag } from '../../types'
|
|
4
|
+
import { focusFirstChild } from '../../utils/acessibility'
|
|
5
|
+
import { OverlayProvider } from './context'
|
|
6
|
+
import { OverlayController, OverlayProps } from './types'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* An arbitrary time to wait for the next React render to be performed
|
|
10
|
+
*/
|
|
11
|
+
const arbitraryRenderTime = 20
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* These todos are in order of priority.
|
|
15
|
+
*
|
|
16
|
+
* TODO: update position when the size changes. Currently, the top position seems out of place whenever the height changes. The same is
|
|
17
|
+
* probably true for the left position if the width changes.
|
|
18
|
+
* TODO: hoverDelayMS
|
|
19
|
+
* TODO: reposition the overlay when it's under a scrollable element other then the body and this element is scrolled.
|
|
20
|
+
* TODO: close the overlay when it's under a scrollable element other then the body and the element that triggered the tooltip becomes
|
|
21
|
+
* hidden by the scroll.
|
|
22
|
+
* TODO: use React Portal to implement overlays. The current implementation will lose every React context in the tree.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Creates an overlay for the child component. The overlay can be any React element. The overlay can be triggered by "click" or "hover"
|
|
27
|
+
* (default).
|
|
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
|
+
...props
|
|
52
|
+
}: OverlayProps<T>,
|
|
53
|
+
) {
|
|
54
|
+
const controller = useRef<OverlayController>({ close: () => Promise.resolve() })
|
|
55
|
+
const wrapper = useRef<HTMLDivElement | null>(null)
|
|
56
|
+
// props that don't require removing and reattaching the event listeners
|
|
57
|
+
const dynamic = useRef({ tag, content, position, alignment, attributes })
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
dynamic.current = { tag, content, position, alignment, attributes }
|
|
61
|
+
}, [tag, content, position, alignment, attributes])
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
let visible = false
|
|
65
|
+
let hideOnClickOutside: ((event: Event) => void) | undefined
|
|
66
|
+
let hideOnEsc: ((event: Event) => void) | undefined
|
|
67
|
+
let hideOverlay: (() => Promise<void>) | undefined
|
|
68
|
+
let removeRefocusTargetListener: (() => void) | undefined
|
|
69
|
+
|
|
70
|
+
function getTarget() {
|
|
71
|
+
const target = wrapper.current?.firstChild
|
|
72
|
+
return target instanceof HTMLElement ? target : undefined
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (onRenderChild) {
|
|
76
|
+
const target = getTarget()
|
|
77
|
+
if (target) onRenderChild(target)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function show(event: Event) {
|
|
81
|
+
if (visible) return
|
|
82
|
+
visible = true
|
|
83
|
+
const target = getTarget()
|
|
84
|
+
if (!target) return
|
|
85
|
+
const { overlay, hide: hideFn } = showOverlay({
|
|
86
|
+
tag: dynamic.current.tag,
|
|
87
|
+
content: ['string', 'number', 'boolean'].includes(typeof dynamic.current.content)
|
|
88
|
+
? dynamic.current.content
|
|
89
|
+
: <OverlayProvider value={controller.current}>{dynamic.current.content}</OverlayProvider>,
|
|
90
|
+
target,
|
|
91
|
+
position: dynamic.current.position,
|
|
92
|
+
alignment: dynamic.current.alignment,
|
|
93
|
+
attributes: dynamic.current.attributes,
|
|
94
|
+
})
|
|
95
|
+
hideOverlay = hideFn
|
|
96
|
+
|
|
97
|
+
function onHide(condition: (event: Event) => boolean) {
|
|
98
|
+
return (event: Event) => {
|
|
99
|
+
if (condition(event)) controller.current.close()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (event.type === 'click') {
|
|
104
|
+
hideOnEsc = onHide(e => e instanceof KeyboardEvent && e.key === 'Escape')
|
|
105
|
+
setTimeout(() => {
|
|
106
|
+
hideOnClickOutside = onHide(e => e instanceof MouseEvent && e.button === 0 && !overlay.contains(e.target as HTMLElement))
|
|
107
|
+
document.addEventListener('click', hideOnClickOutside)
|
|
108
|
+
}, arbitraryRenderTime)
|
|
109
|
+
document.addEventListener('keydown', hideOnEsc)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
//focus target when the last overlay element loses focus
|
|
113
|
+
function refocusTarget(e: KeyboardEvent) {
|
|
114
|
+
if (e.key === 'Tab' && e.target instanceof HTMLElement) {
|
|
115
|
+
const allItems = Array.from(e.target.closest('[data-citric="menu"]')?.querySelectorAll('a, button') ?? [])
|
|
116
|
+
if (e.target === allItems[allItems.length - 1]) {
|
|
117
|
+
getTarget()?.focus()
|
|
118
|
+
e.preventDefault()
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
overlay.addEventListener('keydown', refocusTarget)
|
|
123
|
+
removeRefocusTargetListener = () => overlay.removeEventListener('keydown', refocusTarget)
|
|
124
|
+
|
|
125
|
+
// auto-focus
|
|
126
|
+
const openedWithMouse = event instanceof MouseEvent && event.detail > 0
|
|
127
|
+
if (autoFocusBehavior === 'always' || (autoFocusBehavior === 'keyboard' && !openedWithMouse)) {
|
|
128
|
+
setTimeout(() => focusFirstChild(overlay), arbitraryRenderTime)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
controller.current.close = async () => {
|
|
133
|
+
visible = false
|
|
134
|
+
if (hideOnClickOutside) document.removeEventListener('click', hideOnClickOutside)
|
|
135
|
+
if (hideOnEsc) document.removeEventListener('keydown', hideOnEsc)
|
|
136
|
+
removeRefocusTargetListener?.()
|
|
137
|
+
await hideOverlay?.()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (triggerOn === 'hover') {
|
|
141
|
+
getTarget()?.addEventListener('mouseenter', show)
|
|
142
|
+
getTarget()?.addEventListener('mouseleave', controller.current.close)
|
|
143
|
+
getTarget()?.addEventListener('focus', show)
|
|
144
|
+
getTarget()?.addEventListener('blur', controller.current.close)
|
|
145
|
+
return () => {
|
|
146
|
+
getTarget()?.removeEventListener('mouseenter', show)
|
|
147
|
+
getTarget()?.removeEventListener('mouseleave', controller.current.close)
|
|
148
|
+
getTarget()?.removeEventListener('focus', show)
|
|
149
|
+
getTarget()?.removeEventListener('blur', controller.current.close)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (triggerOn === 'click') {
|
|
154
|
+
getTarget()?.addEventListener('click', show)
|
|
155
|
+
return () => {
|
|
156
|
+
controller.current.close()
|
|
157
|
+
getTarget()?.removeEventListener('click', show)
|
|
158
|
+
if (hideOnClickOutside) document.removeEventListener('click', hideOnClickOutside)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}, [wrapper.current, triggerOn])
|
|
162
|
+
|
|
163
|
+
return <div ref={wrapper} {...props}>{children}</div>
|
|
164
|
+
}
|
|
@@ -1,70 +1,70 @@
|
|
|
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
|
-
* TODO: not implemented yet.
|
|
19
|
-
*
|
|
20
|
-
* Only valid if `triggerOn` is "hover".
|
|
21
|
-
*
|
|
22
|
-
* If the overlay is hidden right after the mouse leaves the element, then it becomes impossible to interact with anything inside the
|
|
23
|
-
* tooltip. This sets a delay for the overlay to disappear, giving it time for the user to hover the overlay or gocus one of its children,
|
|
24
|
-
* which will prevent it from closing.
|
|
25
|
-
*
|
|
26
|
-
* When `hoverDelayMS` is greater then zero, the hover effect is also applied to the overlay itself instead of just the child element.
|
|
27
|
-
*
|
|
28
|
-
* When set to "auto", a hover delay of 1 second will be used if the overlay contains any focusable element.
|
|
29
|
-
*
|
|
30
|
-
* @default 'auto'
|
|
31
|
-
*/
|
|
32
|
-
hoverDelayMS?: number | 'auto',
|
|
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
|
-
close: () => Promise<void>,
|
|
70
|
-
}
|
|
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
|
+
* TODO: not implemented yet.
|
|
19
|
+
*
|
|
20
|
+
* Only valid if `triggerOn` is "hover".
|
|
21
|
+
*
|
|
22
|
+
* If the overlay is hidden right after the mouse leaves the element, then it becomes impossible to interact with anything inside the
|
|
23
|
+
* tooltip. This sets a delay for the overlay to disappear, giving it time for the user to hover the overlay or gocus one of its children,
|
|
24
|
+
* which will prevent it from closing.
|
|
25
|
+
*
|
|
26
|
+
* When `hoverDelayMS` is greater then zero, the hover effect is also applied to the overlay itself instead of just the child element.
|
|
27
|
+
*
|
|
28
|
+
* When set to "auto", a hover delay of 1 second will be used if the overlay contains any focusable element.
|
|
29
|
+
*
|
|
30
|
+
* @default 'auto'
|
|
31
|
+
*/
|
|
32
|
+
hoverDelayMS?: number | 'auto',
|
|
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
|
+
close: () => Promise<void>,
|
|
70
|
+
}
|