@stack-spot/citric-react 0.1.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 +2580 -0
- package/dist/components/Accordion.d.ts +33 -0
- package/dist/components/Accordion.d.ts.map +1 -0
- package/dist/components/Accordion.js +19 -0
- package/dist/components/Accordion.js.map +1 -0
- package/dist/components/Alert.d.ts +11 -0
- package/dist/components/Alert.d.ts.map +1 -0
- package/dist/components/Alert.js +5 -0
- package/dist/components/Alert.js.map +1 -0
- package/dist/components/AsyncContent.d.ts +30 -0
- package/dist/components/AsyncContent.d.ts.map +1 -0
- package/dist/components/AsyncContent.js +33 -0
- package/dist/components/AsyncContent.js.map +1 -0
- package/dist/components/Avatar.d.ts +22 -0
- package/dist/components/Avatar.d.ts.map +1 -0
- package/dist/components/Avatar.js +9 -0
- package/dist/components/Avatar.js.map +1 -0
- package/dist/components/AvatarGroup.d.ts +25 -0
- package/dist/components/AvatarGroup.d.ts.map +1 -0
- package/dist/components/AvatarGroup.js +9 -0
- package/dist/components/AvatarGroup.js.map +1 -0
- package/dist/components/Badge.d.ts +18 -0
- package/dist/components/Badge.d.ts.map +1 -0
- package/dist/components/Badge.js +7 -0
- package/dist/components/Badge.js.map +1 -0
- package/dist/components/Blockquote.d.ts +5 -0
- package/dist/components/Blockquote.d.ts.map +1 -0
- package/dist/components/Blockquote.js +4 -0
- package/dist/components/Blockquote.js.map +1 -0
- package/dist/components/Breadcrumb.d.ts +12 -0
- package/dist/components/Breadcrumb.d.ts.map +1 -0
- package/dist/components/Breadcrumb.js +8 -0
- package/dist/components/Breadcrumb.js.map +1 -0
- package/dist/components/Button.d.ts +42 -0
- package/dist/components/Button.d.ts.map +1 -0
- package/dist/components/Button.js +25 -0
- package/dist/components/Button.js.map +1 -0
- package/dist/components/Card.d.ts +19 -0
- package/dist/components/Card.d.ts.map +1 -0
- package/dist/components/Card.js +5 -0
- package/dist/components/Card.js.map +1 -0
- package/dist/components/Checkbox.d.ts +14 -0
- package/dist/components/Checkbox.d.ts.map +1 -0
- package/dist/components/Checkbox.js +7 -0
- package/dist/components/Checkbox.js.map +1 -0
- package/dist/components/CheckboxGroup.d.ts +53 -0
- package/dist/components/CheckboxGroup.d.ts.map +1 -0
- package/dist/components/CheckboxGroup.js +17 -0
- package/dist/components/CheckboxGroup.js.map +1 -0
- package/dist/components/Circle.d.ts +18 -0
- package/dist/components/Circle.d.ts.map +1 -0
- package/dist/components/Circle.js +5 -0
- package/dist/components/Circle.js.map +1 -0
- package/dist/components/CitricComponent.d.ts +14 -0
- package/dist/components/CitricComponent.d.ts.map +1 -0
- package/dist/components/CitricComponent.js +15 -0
- package/dist/components/CitricComponent.js.map +1 -0
- package/dist/components/Divider.d.ts +14 -0
- package/dist/components/Divider.d.ts.map +1 -0
- package/dist/components/Divider.js +5 -0
- package/dist/components/Divider.js.map +1 -0
- package/dist/components/ErrorBoundary.d.ts +32 -0
- package/dist/components/ErrorBoundary.d.ts.map +1 -0
- package/dist/components/ErrorBoundary.js +46 -0
- package/dist/components/ErrorBoundary.js.map +1 -0
- package/dist/components/ErrorMessage.d.ts +4 -0
- package/dist/components/ErrorMessage.d.ts.map +1 -0
- package/dist/components/ErrorMessage.js +7 -0
- package/dist/components/ErrorMessage.js.map +1 -0
- package/dist/components/FallbackBoundary.d.ts +13 -0
- package/dist/components/FallbackBoundary.d.ts.map +1 -0
- package/dist/components/FallbackBoundary.js +11 -0
- package/dist/components/FallbackBoundary.js.map +1 -0
- package/dist/components/Favorite.d.ts +23 -0
- package/dist/components/Favorite.d.ts.map +1 -0
- package/dist/components/Favorite.js +5 -0
- package/dist/components/Favorite.js.map +1 -0
- package/dist/components/FieldGroup.d.ts +14 -0
- package/dist/components/FieldGroup.d.ts.map +1 -0
- package/dist/components/FieldGroup.js +5 -0
- package/dist/components/FieldGroup.js.map +1 -0
- package/dist/components/Form.d.ts +5 -0
- package/dist/components/Form.d.ts.map +1 -0
- package/dist/components/Form.js +6 -0
- package/dist/components/Form.js.map +1 -0
- package/dist/components/FormGroup.d.ts +22 -0
- package/dist/components/FormGroup.d.ts.map +1 -0
- package/dist/components/FormGroup.js +8 -0
- package/dist/components/FormGroup.js.map +1 -0
- package/dist/components/IconBox.d.ts +46 -0
- package/dist/components/IconBox.d.ts.map +1 -0
- package/dist/components/IconBox.js +29 -0
- package/dist/components/IconBox.js.map +1 -0
- package/dist/components/Input.d.ts +15 -0
- package/dist/components/Input.d.ts.map +1 -0
- package/dist/components/Input.js +18 -0
- package/dist/components/Input.js.map +1 -0
- package/dist/components/Link.d.ts +20 -0
- package/dist/components/Link.d.ts.map +1 -0
- package/dist/components/Link.js +21 -0
- package/dist/components/Link.js.map +1 -0
- package/dist/components/LoadingPanel.d.ts +2 -0
- package/dist/components/LoadingPanel.d.ts.map +1 -0
- package/dist/components/LoadingPanel.js +5 -0
- package/dist/components/LoadingPanel.js.map +1 -0
- package/dist/components/MenuOverlay/Menu.d.ts +6 -0
- package/dist/components/MenuOverlay/Menu.d.ts.map +1 -0
- package/dist/components/MenuOverlay/Menu.js +100 -0
- package/dist/components/MenuOverlay/Menu.js.map +1 -0
- package/dist/components/MenuOverlay/context.d.ts +6 -0
- package/dist/components/MenuOverlay/context.d.ts.map +1 -0
- package/dist/components/MenuOverlay/context.js +16 -0
- package/dist/components/MenuOverlay/context.js.map +1 -0
- package/dist/components/MenuOverlay/index.d.ts +3 -0
- package/dist/components/MenuOverlay/index.d.ts.map +1 -0
- package/dist/components/MenuOverlay/index.js +23 -0
- package/dist/components/MenuOverlay/index.js.map +1 -0
- package/dist/components/MenuOverlay/keyboard.d.ts +2 -0
- package/dist/components/MenuOverlay/keyboard.d.ts.map +1 -0
- package/dist/components/MenuOverlay/keyboard.js +66 -0
- package/dist/components/MenuOverlay/keyboard.js.map +1 -0
- package/dist/components/MenuOverlay/types.d.ts +166 -0
- package/dist/components/MenuOverlay/types.d.ts.map +1 -0
- package/dist/components/MenuOverlay/types.js +2 -0
- package/dist/components/MenuOverlay/types.js.map +1 -0
- package/dist/components/Overlay/context.d.ts +4 -0
- package/dist/components/Overlay/context.d.ts.map +1 -0
- package/dist/components/Overlay/context.js +7 -0
- package/dist/components/Overlay/context.js.map +1 -0
- package/dist/components/Overlay/index.d.ts +14 -0
- package/dist/components/Overlay/index.d.ts.map +1 -0
- package/dist/components/Overlay/index.js +120 -0
- package/dist/components/Overlay/index.js.map +1 -0
- package/dist/components/Overlay/types.d.ts +67 -0
- package/dist/components/Overlay/types.d.ts.map +1 -0
- package/dist/components/Overlay/types.js +2 -0
- package/dist/components/Overlay/types.js.map +1 -0
- package/dist/components/Pagination.d.ts +28 -0
- package/dist/components/Pagination.d.ts.map +1 -0
- package/dist/components/Pagination.js +30 -0
- package/dist/components/Pagination.js.map +1 -0
- package/dist/components/ProgressBar.d.ts +12 -0
- package/dist/components/ProgressBar.d.ts.map +1 -0
- package/dist/components/ProgressBar.js +7 -0
- package/dist/components/ProgressBar.js.map +1 -0
- package/dist/components/ProgressCircular.d.ts +16 -0
- package/dist/components/ProgressCircular.d.ts.map +1 -0
- package/dist/components/ProgressCircular.js +7 -0
- package/dist/components/ProgressCircular.js.map +1 -0
- package/dist/components/RadioGroup.d.ts +48 -0
- package/dist/components/RadioGroup.d.ts.map +1 -0
- package/dist/components/RadioGroup.js +17 -0
- package/dist/components/RadioGroup.js.map +1 -0
- package/dist/components/Rating.d.ts +13 -0
- package/dist/components/Rating.d.ts.map +1 -0
- package/dist/components/Rating.js +4 -0
- package/dist/components/Rating.js.map +1 -0
- package/dist/components/Select/RichSelect.d.ts +5 -0
- package/dist/components/Select/RichSelect.d.ts.map +1 -0
- package/dist/components/Select/RichSelect.js +152 -0
- package/dist/components/Select/RichSelect.js.map +1 -0
- package/dist/components/Select/SimpleSelect.d.ts +5 -0
- package/dist/components/Select/SimpleSelect.d.ts.map +1 -0
- package/dist/components/Select/SimpleSelect.js +24 -0
- package/dist/components/Select/SimpleSelect.js.map +1 -0
- package/dist/components/Select/index.d.ts +4 -0
- package/dist/components/Select/index.d.ts.map +1 -0
- package/dist/components/Select/index.js +7 -0
- package/dist/components/Select/index.js.map +1 -0
- package/dist/components/Select/types.d.ts +118 -0
- package/dist/components/Select/types.d.ts.map +1 -0
- package/dist/components/Select/types.js +2 -0
- package/dist/components/Select/types.js.map +1 -0
- package/dist/components/SelectBox.d.ts +65 -0
- package/dist/components/SelectBox.d.ts.map +1 -0
- package/dist/components/SelectBox.js +26 -0
- package/dist/components/SelectBox.js.map +1 -0
- package/dist/components/Skeleton.d.ts +30 -0
- package/dist/components/Skeleton.d.ts.map +1 -0
- package/dist/components/Skeleton.js +5 -0
- package/dist/components/Skeleton.js.map +1 -0
- package/dist/components/Slider.d.ts +32 -0
- package/dist/components/Slider.d.ts.map +1 -0
- package/dist/components/Slider.js +19 -0
- package/dist/components/Slider.js.map +1 -0
- package/dist/components/SmartTable.d.ts +87 -0
- package/dist/components/SmartTable.d.ts.map +1 -0
- package/dist/components/SmartTable.js +16 -0
- package/dist/components/SmartTable.js.map +1 -0
- package/dist/components/Stepper.d.ts +52 -0
- package/dist/components/Stepper.d.ts.map +1 -0
- package/dist/components/Stepper.js +53 -0
- package/dist/components/Stepper.js.map +1 -0
- package/dist/components/Switch.d.ts +10 -0
- package/dist/components/Switch.d.ts.map +1 -0
- package/dist/components/Switch.js +7 -0
- package/dist/components/Switch.js.map +1 -0
- package/dist/components/Table.d.ts +106 -0
- package/dist/components/Table.d.ts.map +1 -0
- package/dist/components/Table.js +86 -0
- package/dist/components/Table.js.map +1 -0
- package/dist/components/Tabs/TabController.d.ts +11 -0
- package/dist/components/Tabs/TabController.d.ts.map +1 -0
- package/dist/components/Tabs/TabController.js +39 -0
- package/dist/components/Tabs/TabController.js.map +1 -0
- package/dist/components/Tabs/index.d.ts +5 -0
- package/dist/components/Tabs/index.d.ts.map +1 -0
- package/dist/components/Tabs/index.js +37 -0
- package/dist/components/Tabs/index.js.map +1 -0
- package/dist/components/Tabs/types.d.ts +46 -0
- package/dist/components/Tabs/types.d.ts.map +1 -0
- package/dist/components/Tabs/types.js +2 -0
- package/dist/components/Tabs/types.js.map +1 -0
- package/dist/components/Tabs/utils.d.ts +3 -0
- package/dist/components/Tabs/utils.d.ts.map +1 -0
- package/dist/components/Tabs/utils.js +5 -0
- package/dist/components/Tabs/utils.js.map +1 -0
- package/dist/components/Text.d.ts +27 -0
- package/dist/components/Text.d.ts.map +1 -0
- package/dist/components/Text.js +45 -0
- package/dist/components/Text.js.map +1 -0
- package/dist/components/Textarea.d.ts +8 -0
- package/dist/components/Textarea.d.ts.map +1 -0
- package/dist/components/Textarea.js +4 -0
- package/dist/components/Textarea.js.map +1 -0
- package/dist/components/Tooltip.d.ts +25 -0
- package/dist/components/Tooltip.d.ts.map +1 -0
- package/dist/components/Tooltip.js +18 -0
- package/dist/components/Tooltip.js.map +1 -0
- package/dist/components/layout.d.ts +46 -0
- package/dist/components/layout.d.ts.map +1 -0
- package/dist/components/layout.js +18 -0
- package/dist/components/layout.js.map +1 -0
- package/dist/context/CitricContext.d.ts +3 -0
- package/dist/context/CitricContext.d.ts.map +1 -0
- package/dist/context/CitricContext.js +3 -0
- package/dist/context/CitricContext.js.map +1 -0
- package/dist/context/CitricProvider.d.ts +9 -0
- package/dist/context/CitricProvider.d.ts.map +1 -0
- package/dist/context/CitricProvider.js +8 -0
- package/dist/context/CitricProvider.js.map +1 -0
- package/dist/context/hooks.d.ts +2 -0
- package/dist/context/hooks.d.ts.map +1 -0
- package/dist/context/hooks.js +6 -0
- package/dist/context/hooks.js.map +1 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +48 -0
- package/dist/index.js.map +1 -0
- package/dist/overlay.d.ts +83 -0
- package/dist/overlay.d.ts.map +1 -0
- package/dist/overlay.js +199 -0
- package/dist/overlay.js.map +1 -0
- package/dist/theme.css +419 -0
- package/dist/types.d.ts +175 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/ValueController.d.ts +10 -0
- package/dist/utils/ValueController.d.ts.map +1 -0
- package/dist/utils/ValueController.js +32 -0
- package/dist/utils/ValueController.js.map +1 -0
- package/dist/utils/acessibility.d.ts +52 -0
- package/dist/utils/acessibility.d.ts.map +1 -0
- package/dist/utils/acessibility.js +80 -0
- package/dist/utils/acessibility.js.map +1 -0
- package/dist/utils/css.d.ts +12 -0
- package/dist/utils/css.d.ts.map +1 -0
- package/dist/utils/css.js +72 -0
- package/dist/utils/css.js.map +1 -0
- package/dist/utils/options.d.ts +3 -0
- package/dist/utils/options.d.ts.map +1 -0
- package/dist/utils/options.js +7 -0
- package/dist/utils/options.js.map +1 -0
- package/package.json +51 -0
- package/scripts/build-css.ts +49 -0
- package/src/components/Accordion.tsx +74 -0
- package/src/components/Alert.tsx +16 -0
- package/src/components/AsyncContent.tsx +54 -0
- package/src/components/Avatar.tsx +34 -0
- package/src/components/AvatarGroup.tsx +40 -0
- package/src/components/Badge.tsx +28 -0
- package/src/components/Blockquote.tsx +9 -0
- package/src/components/Breadcrumb.tsx +24 -0
- package/src/components/Button.tsx +88 -0
- package/src/components/Card.tsx +32 -0
- package/src/components/Checkbox.tsx +36 -0
- package/src/components/CheckboxGroup.tsx +93 -0
- package/src/components/Circle.tsx +26 -0
- package/src/components/CitricComponent.ts +34 -0
- package/src/components/Divider.tsx +22 -0
- package/src/components/ErrorBoundary.tsx +62 -0
- package/src/components/ErrorMessage.tsx +11 -0
- package/src/components/FallbackBoundary.tsx +29 -0
- package/src/components/Favorite.tsx +37 -0
- package/src/components/FieldGroup.tsx +22 -0
- package/src/components/Form.tsx +17 -0
- package/src/components/FormGroup.tsx +45 -0
- package/src/components/IconBox.tsx +78 -0
- package/src/components/Input.tsx +32 -0
- package/src/components/Link.tsx +40 -0
- package/src/components/LoadingPanel.tsx +8 -0
- package/src/components/MenuOverlay/Menu.tsx +157 -0
- package/src/components/MenuOverlay/context.ts +20 -0
- package/src/components/MenuOverlay/index.tsx +35 -0
- package/src/components/MenuOverlay/keyboard.ts +60 -0
- package/src/components/MenuOverlay/types.ts +178 -0
- package/src/components/Overlay/context.ts +10 -0
- package/src/components/Overlay/index.tsx +137 -0
- package/src/components/Overlay/types.ts +71 -0
- package/src/components/Pagination.tsx +90 -0
- package/src/components/ProgressBar.tsx +25 -0
- package/src/components/ProgressCircular.tsx +29 -0
- package/src/components/RadioGroup.tsx +87 -0
- package/src/components/Rating.tsx +25 -0
- package/src/components/Select/RichSelect.tsx +214 -0
- package/src/components/Select/SimpleSelect.tsx +66 -0
- package/src/components/Select/index.tsx +8 -0
- package/src/components/Select/types.ts +121 -0
- package/src/components/SelectBox.tsx +134 -0
- package/src/components/Skeleton.tsx +41 -0
- package/src/components/Slider.tsx +77 -0
- package/src/components/SmartTable.tsx +148 -0
- package/src/components/Stepper.tsx +142 -0
- package/src/components/Switch.tsx +29 -0
- package/src/components/Table.tsx +219 -0
- package/src/components/Tabs/TabController.ts +40 -0
- package/src/components/Tabs/index.tsx +64 -0
- package/src/components/Tabs/types.ts +48 -0
- package/src/components/Tabs/utils.ts +6 -0
- package/src/components/Text.ts +75 -0
- package/src/components/Textarea.tsx +12 -0
- package/src/components/Tooltip.tsx +53 -0
- package/src/components/layout.tsx +53 -0
- package/src/context/CitricContext.tsx +4 -0
- package/src/context/CitricProvider.tsx +14 -0
- package/src/context/hooks.ts +6 -0
- package/src/index.ts +47 -0
- package/src/overlay.ts +276 -0
- package/src/types.ts +226 -0
- package/src/utils/ValueController.ts +28 -0
- package/src/utils/acessibility.ts +92 -0
- package/src/utils/css.ts +106 -0
- package/src/utils/options.ts +7 -0
- package/tsconfig.json +10 -0
package/src/overlay.ts
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { createRoot } from 'react-dom/client'
|
|
2
|
+
import { WithDataAttributes } from './types'
|
|
3
|
+
import { styleObjectToCssString } from './utils/css'
|
|
4
|
+
|
|
5
|
+
export type RelativePosition = 'top' | 'bottom' | 'left' | 'right'
|
|
6
|
+
|
|
7
|
+
export interface OverlayOptions<T extends keyof React.JSX.IntrinsicElements> {
|
|
8
|
+
/**
|
|
9
|
+
* The tag to render when creating the overlay element.
|
|
10
|
+
*
|
|
11
|
+
* @default 'div'
|
|
12
|
+
*/
|
|
13
|
+
tag?: T,
|
|
14
|
+
/**
|
|
15
|
+
* The content of the overlay element.
|
|
16
|
+
*
|
|
17
|
+
* When this is a string, a number or a boolean, the content will be appended to the overlay element. Otherwise, a new React concurrent
|
|
18
|
+
* instance will be creted to render the React tree node inside the overlay element.
|
|
19
|
+
*/
|
|
20
|
+
content: React.ReactNode,
|
|
21
|
+
/**
|
|
22
|
+
* Which element will be used as a reference to position the overlay element.
|
|
23
|
+
* This can be either an HTMLElement or an event. If this is a event, the element in `event.target` will be used unless `reference` is
|
|
24
|
+
* set to `'mouse'`, in this case, the reference will be the current cursor position.
|
|
25
|
+
*/
|
|
26
|
+
target: Event | HTMLElement,
|
|
27
|
+
/**
|
|
28
|
+
* Takes effect when `target` is a MouseEvent. If this is set to `'mouse'`, then the position to be used as a reference to place the
|
|
29
|
+
* overlay will be the current cursor position.
|
|
30
|
+
*
|
|
31
|
+
* @default 'element'
|
|
32
|
+
*/
|
|
33
|
+
reference?: 'element' | 'mouse',
|
|
34
|
+
/**
|
|
35
|
+
* Where, relative to `target`, should we place the overlay?
|
|
36
|
+
*
|
|
37
|
+
* @default 'top'
|
|
38
|
+
*/
|
|
39
|
+
position?: RelativePosition,
|
|
40
|
+
/**
|
|
41
|
+
* TODO: implement this.
|
|
42
|
+
*
|
|
43
|
+
* Affects the positioning of the overlay relative to `target`. While `position` defines the overlay position in the main axis,
|
|
44
|
+
* `alignment` defines the position in the cross axis.
|
|
45
|
+
*
|
|
46
|
+
* @default 'center'
|
|
47
|
+
*/
|
|
48
|
+
alignment?: 'start' | 'center' | 'end',
|
|
49
|
+
/**
|
|
50
|
+
* TODO: implement this (currently, only 'fade' works).
|
|
51
|
+
*
|
|
52
|
+
* - fade: the overlay fades in to appear and fades out to disappear.
|
|
53
|
+
* - accordion: the element grows to appear and shrinks to disappear. The direction of the growth depends on `position`.
|
|
54
|
+
* - bubble: grows from scale(0) to scale(1) from its center to appear. Shrinks from scale(1) to scale(0) to disappear.
|
|
55
|
+
* - none: no animation is used.
|
|
56
|
+
*
|
|
57
|
+
* @default 'fade'
|
|
58
|
+
*/
|
|
59
|
+
animation?: 'fade' | 'accordion' | 'bubble' | 'none',
|
|
60
|
+
/**
|
|
61
|
+
* The attributes for the HTMLElement that will be created for the overlay.
|
|
62
|
+
*/
|
|
63
|
+
attributes?: JSX.IntrinsicElements[T] & WithDataAttributes & { inert?: boolean },
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface Position {
|
|
67
|
+
top: number,
|
|
68
|
+
left: number,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface PositionWithRelativeData extends Position {
|
|
72
|
+
relativeTo: RelativePosition,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type CalculatePosOptions = Pick<Required<OverlayOptions<any>>, 'position' | 'target' | 'reference'>
|
|
76
|
+
& { overlay: HTMLElement }
|
|
77
|
+
|
|
78
|
+
const animationDurationMS = 300
|
|
79
|
+
|
|
80
|
+
function hasMargins(element: HTMLElement) {
|
|
81
|
+
const s = element.style
|
|
82
|
+
return s.margin || s.marginTop || s.marginBottom || s.marginLeft || s.marginRight
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function calculatePosition({
|
|
86
|
+
overlay,
|
|
87
|
+
reference,
|
|
88
|
+
target,
|
|
89
|
+
position: relativePosition,
|
|
90
|
+
}: CalculatePosOptions): Position & { overlayWidth: number, overlayHeight: number } {
|
|
91
|
+
const overlayDimensions = overlay.getBoundingClientRect()
|
|
92
|
+
if (hasMargins(overlay)) {
|
|
93
|
+
const style = overlay.computedStyleMap()
|
|
94
|
+
const mt = parseInt(style.get('margin-top')?.toString() ?? '0')
|
|
95
|
+
const mb = parseInt(style.get('margin-bottom')?.toString() ?? '0')
|
|
96
|
+
const ml = parseInt(style.get('margin-left')?.toString() ?? '0')
|
|
97
|
+
const mr = parseInt(style.get('margin-right')?.toString() ?? '0')
|
|
98
|
+
if (mt) overlayDimensions.height += mt
|
|
99
|
+
if (mb) overlayDimensions.height += mb
|
|
100
|
+
if (ml) overlayDimensions.width += ml
|
|
101
|
+
if (mr) overlayDimensions.width += mr
|
|
102
|
+
}
|
|
103
|
+
const referencePosition = { top: 0, left: 0, overlayWidth: overlayDimensions.width, overlayHeight: overlayDimensions.height }
|
|
104
|
+
if (reference === 'mouse' && target instanceof MouseEvent) {
|
|
105
|
+
referencePosition.top = target.clientY
|
|
106
|
+
referencePosition.left = target.clientX
|
|
107
|
+
} else {
|
|
108
|
+
const element = target instanceof Event ? target.target : target
|
|
109
|
+
if (!(element instanceof HTMLElement)) return referencePosition
|
|
110
|
+
const elementDimensions = element.getBoundingClientRect()
|
|
111
|
+
switch (relativePosition) {
|
|
112
|
+
case 'top':
|
|
113
|
+
referencePosition.top = elementDimensions.top + window.scrollY
|
|
114
|
+
referencePosition.left = elementDimensions.left + elementDimensions.width / 2 + window.scrollX
|
|
115
|
+
break
|
|
116
|
+
case 'bottom':
|
|
117
|
+
referencePosition.top = elementDimensions.bottom + window.scrollY
|
|
118
|
+
referencePosition.left = elementDimensions.left + elementDimensions.width / 2 + window.scrollX
|
|
119
|
+
break
|
|
120
|
+
case 'left':
|
|
121
|
+
referencePosition.top = elementDimensions.top + elementDimensions.height / 2 + window.scrollY
|
|
122
|
+
referencePosition.left = elementDimensions.left + window.scrollX
|
|
123
|
+
break
|
|
124
|
+
case 'right':
|
|
125
|
+
referencePosition.top = elementDimensions.top + elementDimensions.height / 2 + window.scrollY
|
|
126
|
+
referencePosition.left = elementDimensions.right + window.scrollX
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const position = { ...referencePosition }
|
|
130
|
+
switch (relativePosition) {
|
|
131
|
+
case 'top':
|
|
132
|
+
position.top -= overlayDimensions.height
|
|
133
|
+
position.left -= overlayDimensions.width / 2
|
|
134
|
+
break
|
|
135
|
+
case 'bottom':
|
|
136
|
+
position.left -= overlayDimensions.width / 2
|
|
137
|
+
break
|
|
138
|
+
case 'left':
|
|
139
|
+
position.top -= overlayDimensions.height / 2
|
|
140
|
+
position.left -= overlayDimensions.width
|
|
141
|
+
break
|
|
142
|
+
case 'right':
|
|
143
|
+
position.top -= overlayDimensions.height / 2
|
|
144
|
+
}
|
|
145
|
+
return position
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function getSafeOverlayPosition(
|
|
149
|
+
options: Omit<CalculatePosOptions, 'position'>, positionPriority: RelativePosition[], fallback?: PositionWithRelativeData,
|
|
150
|
+
): PositionWithRelativeData {
|
|
151
|
+
if (!positionPriority.length) return fallback ?? { top: 0, left: 0, relativeTo: 'top' }
|
|
152
|
+
const [relativePosition, ...remainingRelativePositions] = positionPriority
|
|
153
|
+
const position = { ...calculatePosition({ ...options, position: relativePosition }), relativeTo: relativePosition }
|
|
154
|
+
switch (relativePosition) {
|
|
155
|
+
case 'top':
|
|
156
|
+
if (position.left < 0) position.left = 0
|
|
157
|
+
if (position.left + position.overlayWidth > document.body.clientWidth) {
|
|
158
|
+
position.left = document.body.clientWidth - position.overlayWidth
|
|
159
|
+
}
|
|
160
|
+
if (position.top < 0) return getSafeOverlayPosition(options, remainingRelativePositions, fallback ?? position)
|
|
161
|
+
break
|
|
162
|
+
case 'bottom':
|
|
163
|
+
if (position.left < 0) position.left = 0
|
|
164
|
+
if (position.left + position.overlayWidth > document.body.clientWidth) {
|
|
165
|
+
position.left = document.body.clientWidth - position.overlayWidth
|
|
166
|
+
}
|
|
167
|
+
if (position.top + position.overlayHeight > document.body.clientHeight) {
|
|
168
|
+
return getSafeOverlayPosition(options, remainingRelativePositions, fallback ?? position)
|
|
169
|
+
}
|
|
170
|
+
break
|
|
171
|
+
case 'left':
|
|
172
|
+
if (position.top < 0) position.top = 0
|
|
173
|
+
if (position.top + position.overlayHeight > document.body.clientHeight) {
|
|
174
|
+
position.top = document.body.clientHeight - position.overlayHeight
|
|
175
|
+
}
|
|
176
|
+
if (position.left < 0) return getSafeOverlayPosition(options, remainingRelativePositions, fallback ?? position)
|
|
177
|
+
break
|
|
178
|
+
case 'right':
|
|
179
|
+
if (position.top < 0) position.top = 0
|
|
180
|
+
if (position.top + position.overlayHeight > document.body.clientHeight) {
|
|
181
|
+
position.top = document.body.clientHeight - position.overlayHeight
|
|
182
|
+
}
|
|
183
|
+
if (position.left + position.overlayWidth > document.body.clientWidth) {
|
|
184
|
+
return getSafeOverlayPosition(options, remainingRelativePositions, fallback ?? position)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return position
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function invert(position: RelativePosition): RelativePosition {
|
|
191
|
+
switch (position) {
|
|
192
|
+
case 'bottom': return 'top'
|
|
193
|
+
case 'top': return 'bottom'
|
|
194
|
+
case 'left': return 'right'
|
|
195
|
+
case 'right': return 'left'
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function oppositeAxis(position: RelativePosition): [RelativePosition, RelativePosition] {
|
|
200
|
+
return (position === 'top' || position === 'bottom') ? ['left', 'right'] : ['top', 'bottom']
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function reactAttributeToHTML(attribute: string) {
|
|
204
|
+
return attribute === 'className' ? 'class' : attribute
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function setElementAttributes(element: HTMLElement, attributes: Record<string, any> | undefined, ignore: string[] = []) {
|
|
208
|
+
for (const attr in attributes) {
|
|
209
|
+
if (attributes[attr] !== undefined && !ignore.includes(attr)) element.setAttribute(reactAttributeToHTML(attr), attributes[attr])
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Appends a new HTML Element to the tag "body". This element is absolutely positioned and its position is calculated according to the
|
|
215
|
+
* options passed as parameter.
|
|
216
|
+
*
|
|
217
|
+
* This function returns both the newly created HTML Element and a function to remove it.
|
|
218
|
+
* @param options {@link OverlayOptions}.
|
|
219
|
+
* @returns an object with two keys: "overlay" (the HTML Element created) and "hide" (a function to remove the element from the document).
|
|
220
|
+
*/
|
|
221
|
+
export function showOverlay<T extends keyof React.JSX.IntrinsicElements = 'div'>(
|
|
222
|
+
{ tag, content, target, reference = 'element', position = 'top', attributes }: OverlayOptions<T>,
|
|
223
|
+
) {
|
|
224
|
+
const overlay = document.createElement(tag || 'div')
|
|
225
|
+
overlay.style = `z-index: 9999; pointer-events: none; position: absolute; opacity: 0; transition: opacity ${animationDurationMS / 1000}s; ${styleObjectToCssString(attributes?.style)}`
|
|
226
|
+
overlay.inert = true
|
|
227
|
+
setElementAttributes(overlay, attributes, ['style', 'inert'])
|
|
228
|
+
document.body.append(overlay)
|
|
229
|
+
let unmount: (() => void) | undefined
|
|
230
|
+
if (['string', 'number', 'boolean'].includes(typeof content)) {
|
|
231
|
+
overlay.append(`${content}`)
|
|
232
|
+
unmount = () => document.body.removeChild(overlay)
|
|
233
|
+
} else {
|
|
234
|
+
const root = createRoot(overlay)
|
|
235
|
+
root.render(content)
|
|
236
|
+
unmount = () => {
|
|
237
|
+
root.unmount()
|
|
238
|
+
document.body.removeChild(overlay)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
setTimeout(() => {
|
|
242
|
+
const overlayPos = getSafeOverlayPosition(
|
|
243
|
+
{ overlay, reference, target },
|
|
244
|
+
[position, invert(position), ...oppositeAxis(position)],
|
|
245
|
+
)
|
|
246
|
+
overlay.style = `z-index: 9999; position: absolute; opacity: 1; transition: opacity ${animationDurationMS / 1000}s; top: ${overlayPos.top}px; left: ${overlayPos.left}px; ${styleObjectToCssString(attributes?.style)}`
|
|
247
|
+
if (attributes && 'inert' in attributes && attributes.inert) overlay.style.pointerEvents = 'none'
|
|
248
|
+
else overlay.inert = false
|
|
249
|
+
overlay.classList.add(overlayPos.relativeTo)
|
|
250
|
+
}, 0)
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
/**
|
|
254
|
+
* The overlay element created.
|
|
255
|
+
*/
|
|
256
|
+
overlay,
|
|
257
|
+
/**
|
|
258
|
+
* Removes the overlay element.
|
|
259
|
+
* @returns a promise that completes when the element is fully removed (after any animation).
|
|
260
|
+
*/
|
|
261
|
+
hide: () => new Promise<void>((resolve) => {
|
|
262
|
+
overlay.style.opacity = '0'
|
|
263
|
+
overlay.style.pointerEvents = 'none'
|
|
264
|
+
overlay.inert = true
|
|
265
|
+
setTimeout(() => {
|
|
266
|
+
try {
|
|
267
|
+
unmount()
|
|
268
|
+
} catch { /* empty */ }
|
|
269
|
+
resolve()
|
|
270
|
+
}, animationDurationMS)
|
|
271
|
+
}),
|
|
272
|
+
ready: new Promise<void>((resolve) => {
|
|
273
|
+
setTimeout(resolve, animationDurationMS)
|
|
274
|
+
}),
|
|
275
|
+
}
|
|
276
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { ColorKey, ColorPaletteName, ColorSchemeName } from '@stack-spot/portal-theme'
|
|
2
|
+
import { RadiusKey, SpacingKey } from '@stack-spot/portal-theme/dist/definition'
|
|
3
|
+
|
|
4
|
+
export interface WithColorScheme {
|
|
5
|
+
colorScheme?: ColorSchemeName,
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface WithColorPalette {
|
|
9
|
+
colorPalette?: ColorPaletteName,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface WithColor {
|
|
13
|
+
color?: ColorKey,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type WithDataAttributes = { [key: `data-${string}`]: string | undefined }
|
|
17
|
+
|
|
18
|
+
export type TextAppearance = 'body1' | 'body2' | 'code1' | 'code2' | 'display1' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'microtext1' |
|
|
19
|
+
'overheader1' | 'overheader2' | 'subtitle1' | 'subtitle2' | 'subtitle3' | 'subtitle4'
|
|
20
|
+
|
|
21
|
+
// increase this list as needed. Generic uses of `JSX.IntrinsicElements` may lead to expensive operations.
|
|
22
|
+
// export interface HTMLTag {
|
|
23
|
+
// a: HTMLAnchorElement,
|
|
24
|
+
// blockquote: HTMLQuoteElement,
|
|
25
|
+
// button: HTMLButtonElement,
|
|
26
|
+
// div: HTMLDivElement,
|
|
27
|
+
// form: HTMLFormElement,
|
|
28
|
+
// h1: HTMLHeadingElement,
|
|
29
|
+
// h2: HTMLHeadingElement,
|
|
30
|
+
// h3: HTMLHeadingElement,
|
|
31
|
+
// h4: HTMLHeadingElement,
|
|
32
|
+
// h5: HTMLHeadingElement,
|
|
33
|
+
// h6: HTMLHeadingElement,
|
|
34
|
+
// hr: HTMLHeadingElement,
|
|
35
|
+
// i: HTMLElement,
|
|
36
|
+
// image: HTMLImageElement,
|
|
37
|
+
// input: HTMLInputElement,
|
|
38
|
+
// label: HTMLLabelElement,
|
|
39
|
+
// li: HTMLLIElement,
|
|
40
|
+
// nav: HTMLElement,
|
|
41
|
+
// p: HTMLParagraphElement,
|
|
42
|
+
// pre: HTMLPreElement,
|
|
43
|
+
// select: HTMLSelectElement,
|
|
44
|
+
// small: HTMLElement,
|
|
45
|
+
// span: HTMLSpanElement,
|
|
46
|
+
// table: HTMLTableElement,
|
|
47
|
+
// td: HTMLTableCellElement,
|
|
48
|
+
// th: HTMLTableCellElement,
|
|
49
|
+
// tr: HTMLTableRowElement,
|
|
50
|
+
// textarea: HTMLTextAreaElement,
|
|
51
|
+
// ul: HTMLUListElement,
|
|
52
|
+
// }
|
|
53
|
+
|
|
54
|
+
// export type HTMLExtension<K extends keyof HTMLTag, Props, Omitted extends string = ''> =
|
|
55
|
+
// (Omitted extends ''
|
|
56
|
+
// ? React.DetailedHTMLProps<React.HTMLAttributes<HTMLTag[K]>, HTMLTag[K]>
|
|
57
|
+
// : Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLTag[K]>, HTMLTag[K]>, Omitted>
|
|
58
|
+
// ) & Props
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
export type HTMLExtension<K extends keyof React.JSX.IntrinsicElements, Props, Omitted extends string = ''> =
|
|
63
|
+
(Omitted extends ''
|
|
64
|
+
? React.JSX.IntrinsicElements[K]
|
|
65
|
+
: Omit<React.JSX.IntrinsicElements[K], Omitted>
|
|
66
|
+
) & Props
|
|
67
|
+
|
|
68
|
+
export interface WithStyleShortcuts {
|
|
69
|
+
/**
|
|
70
|
+
* Sets a theme color for the background.
|
|
71
|
+
*/
|
|
72
|
+
bg?: ColorKey,
|
|
73
|
+
/**
|
|
74
|
+
* Sets a theme color for the foreground.
|
|
75
|
+
*/
|
|
76
|
+
fg?: ColorKey,
|
|
77
|
+
/**
|
|
78
|
+
* Sets a theme color for the border and set it to 1px width.
|
|
79
|
+
*/
|
|
80
|
+
border?: ColorKey,
|
|
81
|
+
/**
|
|
82
|
+
* Sets the border radius.
|
|
83
|
+
*/
|
|
84
|
+
radius?: RadiusKey,
|
|
85
|
+
/**
|
|
86
|
+
* Sets the margin for every side.
|
|
87
|
+
*
|
|
88
|
+
* When a number is provided, the theme variable is used "spacing-1, ..., spacing-19". When a list of numbers is provided, the theme
|
|
89
|
+
* variable is used for each of the positions, following the CSS order ([top, right, bottom, left]) When a string is provided, this acts
|
|
90
|
+
* as a shortcut for `style.margin`.
|
|
91
|
+
*/
|
|
92
|
+
m?: SpacingKey | SpacingKey[] | string,
|
|
93
|
+
/**
|
|
94
|
+
* Sets the margin from the top.
|
|
95
|
+
*
|
|
96
|
+
* When a number is provided, the theme variable is used "spacing-1, ..., spacing-19". When a string is provided, this acts as a shortcut
|
|
97
|
+
* for `style.marginTop`.
|
|
98
|
+
*/
|
|
99
|
+
mt?: SpacingKey | string,
|
|
100
|
+
/**
|
|
101
|
+
* Sets the margin from the bottom.
|
|
102
|
+
*
|
|
103
|
+
* When a number is provided, the theme variable is used "spacing-1, ..., spacing-19". When a string is provided, this acts as a shortcut
|
|
104
|
+
* for `style.marginBottom`.
|
|
105
|
+
*/
|
|
106
|
+
mb?: SpacingKey | string,
|
|
107
|
+
/**
|
|
108
|
+
* Sets the margin from the left.
|
|
109
|
+
*
|
|
110
|
+
* When a number is provided, the theme variable is used "spacing-1, ..., spacing-19". When a string is provided, this acts as a shortcut
|
|
111
|
+
* for `style.marginLeft`.
|
|
112
|
+
*/
|
|
113
|
+
ml?: SpacingKey | string,
|
|
114
|
+
/**
|
|
115
|
+
* Sets the margin from the right.
|
|
116
|
+
*
|
|
117
|
+
* When a number is provided, the theme variable is used "spacing-1, ..., spacing-19". When a string is provided, this acts as a shortcut
|
|
118
|
+
* for `style.marginRight`.
|
|
119
|
+
*/
|
|
120
|
+
mr?: SpacingKey | string,
|
|
121
|
+
/**
|
|
122
|
+
* Sets the padding for every side.
|
|
123
|
+
*
|
|
124
|
+
* When a number is provided, the theme variable is used "spacing-1, ..., spacing-19". When a list of numbers is provided, the theme
|
|
125
|
+
* variable is used for each of the positions, following the CSS order ([top, right, bottom, left]) When a string is provided, this acts
|
|
126
|
+
* as a shortcut for `style.padding`.
|
|
127
|
+
*/
|
|
128
|
+
p?: SpacingKey | SpacingKey[] | string,
|
|
129
|
+
/**
|
|
130
|
+
* Sets the padding from the top.
|
|
131
|
+
*
|
|
132
|
+
* When a number is provided, the theme variable is used "spacing-1, ..., spacing-19". When a string is provided, this acts as a shortcut
|
|
133
|
+
* for `style.paddingTop`.
|
|
134
|
+
*/
|
|
135
|
+
pt?: SpacingKey | string,
|
|
136
|
+
/**
|
|
137
|
+
* Sets the padding from the bottom.
|
|
138
|
+
*
|
|
139
|
+
* When a number is provided, the theme variable is used "spacing-1, ..., spacing-19". When a string is provided, this acts as a shortcut
|
|
140
|
+
* for `style.paddingBottom`.
|
|
141
|
+
*/
|
|
142
|
+
pb?: SpacingKey | string,
|
|
143
|
+
/**
|
|
144
|
+
* Sets the padding from the left.
|
|
145
|
+
*
|
|
146
|
+
* When a number is provided, the theme variable is used "spacing-1, ..., spacing-19". When a string is provided, this acts as a shortcut
|
|
147
|
+
* for `style.paddingLeft`.
|
|
148
|
+
*/
|
|
149
|
+
pl?: SpacingKey | string,
|
|
150
|
+
/**
|
|
151
|
+
* Sets the padding from the right.
|
|
152
|
+
*
|
|
153
|
+
* When a number is provided, the theme variable is used "spacing-1, ..., spacing-19". When a string is provided, this acts as a shortcut
|
|
154
|
+
* for `style.paddingRight`.
|
|
155
|
+
*/
|
|
156
|
+
pr?: SpacingKey | string,
|
|
157
|
+
/**
|
|
158
|
+
* Shortcut for `style.width`.
|
|
159
|
+
*/
|
|
160
|
+
w?: string | number,
|
|
161
|
+
/**
|
|
162
|
+
* Shortcut for `style.height`.
|
|
163
|
+
*/
|
|
164
|
+
h?: string | number,
|
|
165
|
+
/**
|
|
166
|
+
* Shortcut for `style.flex`.
|
|
167
|
+
*/
|
|
168
|
+
flex?: React.CSSProperties['flex'],
|
|
169
|
+
/**
|
|
170
|
+
* Shortcut for `style.justifyContent`.
|
|
171
|
+
*/
|
|
172
|
+
justifyContent?: React.CSSProperties['justifyContent'],
|
|
173
|
+
/**
|
|
174
|
+
* Shortcut for `style.alignItems`.
|
|
175
|
+
*/
|
|
176
|
+
alignItems?: React.CSSProperties['alignItems'],
|
|
177
|
+
/**
|
|
178
|
+
* Shortcut for `style.gap`.
|
|
179
|
+
*/
|
|
180
|
+
gap?: React.CSSProperties['gap'],
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface CitricController {
|
|
184
|
+
/**
|
|
185
|
+
* If set, instead of rendering the HTML tag "a", the Link component will render the return value of this function.
|
|
186
|
+
* @param props the props for the anchor element
|
|
187
|
+
* @returns
|
|
188
|
+
*/
|
|
189
|
+
renderLink?: (props: JSX.IntrinsicElements['a']) => React.ReactElement,
|
|
190
|
+
/**
|
|
191
|
+
* A function to run whenever the component Button is clicked.
|
|
192
|
+
* @param event the click event.
|
|
193
|
+
* @param analytics true if analytics are enabled for this button, false otherwise.
|
|
194
|
+
*/
|
|
195
|
+
onClickButton?: (event: React.MouseEvent<HTMLButtonElement>, analytics: boolean) => void,
|
|
196
|
+
/**
|
|
197
|
+
* A function to run whenever the component Link is clicked.
|
|
198
|
+
* @param event the click event.
|
|
199
|
+
* @param analytics true if analytics are enabled for this link, false otherwise.
|
|
200
|
+
*/
|
|
201
|
+
onClickLink?: (event: React.MouseEvent<HTMLAnchorElement>, analytics: boolean) => void,
|
|
202
|
+
/**
|
|
203
|
+
* A custom renderer for error feedbacks.
|
|
204
|
+
*
|
|
205
|
+
* By default, a simple text with the error message is rendered.
|
|
206
|
+
*
|
|
207
|
+
* Errors are rendered when they're caught in an ErrorBoundary or AsyncContent.
|
|
208
|
+
* @param error the error caught.
|
|
209
|
+
* @returns the UI for the feedback.
|
|
210
|
+
*/
|
|
211
|
+
renderError?: (error: any) => React.ReactElement,
|
|
212
|
+
/**
|
|
213
|
+
* A custom renderer for loading feedbacks.
|
|
214
|
+
*
|
|
215
|
+
* By default, the component "ProgressCircular" is rendered with some padding.
|
|
216
|
+
*
|
|
217
|
+
* Loadings are rendered in Suspended calls or in AsyncContent.
|
|
218
|
+
* @returns the UI for the feedback.
|
|
219
|
+
*/
|
|
220
|
+
renderLoading?: () => void,
|
|
221
|
+
/**
|
|
222
|
+
* Function to run whenever an Error is caught.
|
|
223
|
+
* @param error the error caught.
|
|
224
|
+
*/
|
|
225
|
+
onError?: (error: any) => void,
|
|
226
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { pull } from 'lodash'
|
|
2
|
+
|
|
3
|
+
export type ValueListener<T> = (value: T) => void
|
|
4
|
+
|
|
5
|
+
export class ValueController<T> {
|
|
6
|
+
protected value: T
|
|
7
|
+
private listeners: ValueListener<T>[] = []
|
|
8
|
+
|
|
9
|
+
constructor(value: T) {
|
|
10
|
+
this.value = value
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
onChange(listener: ValueListener<T>) {
|
|
14
|
+
this.listeners.push(listener)
|
|
15
|
+
return () => {
|
|
16
|
+
pull(this.listeners, listener)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
setValue(value: T) {
|
|
21
|
+
this.value = value
|
|
22
|
+
this.listeners.forEach(l => l(value))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
getValue() {
|
|
26
|
+
return this.value
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export type TagPriority = 'a' | 'button' | 'input' | 'textarea' | 'select' | 'other'
|
|
2
|
+
export type TagPriorityElement = TagPriority | TagPriority[]
|
|
3
|
+
|
|
4
|
+
interface FocusOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Instead of focusing the first element overall, focus the first according to this list of priorities.
|
|
7
|
+
*
|
|
8
|
+
* 'other' means elements that are normally not focusable, but have positive tabIndex values.
|
|
9
|
+
*/
|
|
10
|
+
priority?: TagPriorityElement[],
|
|
11
|
+
/**
|
|
12
|
+
* Ignores any element that matches this query selector.
|
|
13
|
+
*/
|
|
14
|
+
ignore?: string,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const selectors: Record<TagPriority, string> = {
|
|
18
|
+
a: 'a[href]:not(:disabled)',
|
|
19
|
+
button: 'button:not(:disabled)',
|
|
20
|
+
input: 'input:not(:disabled):not([type="hidden"])',
|
|
21
|
+
select: 'textarea:not(:disabled)',
|
|
22
|
+
textarea: 'select:not(:disabled)',
|
|
23
|
+
other: '[tabindex]:not([tabindex="-1"])',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Focus the first focusable child of the element provided. If the element has no focusable child, nothing happens.
|
|
28
|
+
*
|
|
29
|
+
* A priority list can be passed in the second parameter, as an option. If it's provided, it will focus the first element according to the
|
|
30
|
+
* list.
|
|
31
|
+
*
|
|
32
|
+
* An ignore query selector can also be passed in the options parameter. If the first focusable element matches the query selector, the
|
|
33
|
+
* next element is focused instead.
|
|
34
|
+
*
|
|
35
|
+
* Elements with `auto-focus={false}` will be ignored.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* Suppose the children of element are: h1, button, p, input, select.
|
|
39
|
+
* 1. We don't pass a priority list. The focused element will be the button.
|
|
40
|
+
* 2. Our priority list is ['button']. The focused element will be the button.
|
|
41
|
+
* 3. Our priority list is ['input', 'button']. The focused element will be the input.
|
|
42
|
+
* 4. Our priority list is ['select', 'input']. The focused element will be the select.
|
|
43
|
+
* 5. Our priority list is [['select', 'input'], 'button']. The focused element will be the input.
|
|
44
|
+
*
|
|
45
|
+
* @param element the element to search a child to focus.
|
|
46
|
+
* @param options optional.
|
|
47
|
+
*/
|
|
48
|
+
export function focusFirstChild(element: HTMLElement | Document | null | undefined, { priority = [], ignore }: FocusOptions = {}) {
|
|
49
|
+
const allFocusableTags: TagPriority[] = ['a', 'button', 'input', 'other', 'select', 'textarea']
|
|
50
|
+
const focusableList: (NodeListOf<HTMLElement> | undefined)[] = [
|
|
51
|
+
element?.querySelectorAll(allFocusableTags.map(t => selectors[t]).join(', ')),
|
|
52
|
+
]
|
|
53
|
+
for (const p of priority) {
|
|
54
|
+
const tags = Array.isArray(p) ? p : [p]
|
|
55
|
+
const querySelectors = tags.map(t => selectors[t])
|
|
56
|
+
focusableList.unshift(element?.querySelectorAll(querySelectors.join(', ')))
|
|
57
|
+
}
|
|
58
|
+
for (const focusable of focusableList ?? []) {
|
|
59
|
+
for (const f of focusable ?? []) {
|
|
60
|
+
if (f.getAttribute('auto-focus') !== 'false' && (!ignore || !f.matches(ignore))) {
|
|
61
|
+
const styles = window.getComputedStyle(f)
|
|
62
|
+
if (styles.display != 'none' && styles.visibility != 'hidden') return f.focus()
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Checks if an element can receive focus.
|
|
70
|
+
*
|
|
71
|
+
* Elements can receive focus only if:
|
|
72
|
+
* - they exist;
|
|
73
|
+
* - they're visible;
|
|
74
|
+
* - they're not disabled;
|
|
75
|
+
* - they are a focusable tag name or have a positive tab index;
|
|
76
|
+
* - they don't have a negative tab index.
|
|
77
|
+
* @param element the element to check.
|
|
78
|
+
* @returns true if the element is focusable, false otherwise.
|
|
79
|
+
*/
|
|
80
|
+
export function isFocusable(element?: Element | null) {
|
|
81
|
+
if (!element) return false
|
|
82
|
+
// is disabled: return false
|
|
83
|
+
if (element.ariaDisabled || element.getAttribute('disabled') !== null) return false
|
|
84
|
+
// is invisible: return false
|
|
85
|
+
if (!element.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true })) return false
|
|
86
|
+
// has tab index: return false if negative, true otherwise
|
|
87
|
+
const tabIndexStr = element.getAttribute('tabindex')
|
|
88
|
+
const tabIndex = tabIndexStr ? parseInt(tabIndexStr) : undefined
|
|
89
|
+
if (tabIndex !== undefined) return tabIndex >= 0
|
|
90
|
+
// check the tag name
|
|
91
|
+
return ['a', 'button', 'input', 'iframe', 'select', 'textarea'].includes(element.tagName.toLowerCase() ?? '')
|
|
92
|
+
}
|