@stack-spot/citric-react 0.37.1 → 0.39.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/CHANGELOG.md +13 -0
- package/dist/citric.css +2844 -2844
- 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.d.ts.map +1 -1
- package/dist/components/CheckboxGroup.js +2 -2
- package/dist/components/CheckboxGroup.js.map +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 +4 -1
- package/dist/components/Overlay/index.d.ts.map +1 -1
- package/dist/components/Overlay/index.js +4 -1
- package/dist/components/Overlay/index.js.map +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 +1 -1
- package/dist/components/Rating.js +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 +2 -1
- 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 +153 -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 +167 -164
- package/src/components/Overlay/types.ts +70 -70
- package/src/components/Pagination.tsx +133 -133
- 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 -98
- 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,92 +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
|
-
}
|
|
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
|
+
}
|
package/src/utils/checkbox.ts
CHANGED
|
@@ -1,121 +1,121 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
-
import { defaultRenderKey } from './options'
|
|
3
|
-
|
|
4
|
-
export interface CheckboxGroupHookParams<T, F = string> {
|
|
5
|
-
/**
|
|
6
|
-
* The initial value for the checkbox group.
|
|
7
|
-
*
|
|
8
|
-
* @default []
|
|
9
|
-
*/
|
|
10
|
-
initialValue?: T[],
|
|
11
|
-
/**
|
|
12
|
-
* A function to apply a filter to an option. Must return true if the option respects the filter and false otherwise.
|
|
13
|
-
* @param filter the current filter.
|
|
14
|
-
* @param option the current option.
|
|
15
|
-
* @returns true if the option should pass the filter, false if the option should be discarded.
|
|
16
|
-
*/
|
|
17
|
-
applyFilter?: (filter: F, option: T) => boolean,
|
|
18
|
-
/**
|
|
19
|
-
* The full set of options for the checkbox group.
|
|
20
|
-
*/
|
|
21
|
-
options: T[],
|
|
22
|
-
/**
|
|
23
|
-
* A function that produces a unique id for an option.
|
|
24
|
-
* @param option the current option.
|
|
25
|
-
* @returns a unique key.
|
|
26
|
-
*/
|
|
27
|
-
renderKey: (option: T) => string | number | undefined,
|
|
28
|
-
/**
|
|
29
|
-
* A function to call whenever the value changes.
|
|
30
|
-
* @param newValue the new value.
|
|
31
|
-
* @param previousValue the previous value.
|
|
32
|
-
*/
|
|
33
|
-
onChange?: (newValue: T[], previousValue: T[]) => void,
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Use this hook to easily implement filtering and selection controls for a checkbox group.
|
|
38
|
-
* @param params the parameters to create the controls.
|
|
39
|
-
* @returns the checkbox controls.
|
|
40
|
-
*/
|
|
41
|
-
export function useCheckboxGroupControls<T, F = string>(params: CheckboxGroupHookParams<T, F>) {
|
|
42
|
-
const [value, setValue] = useState(params.initialValue ?? [])
|
|
43
|
-
const [filter, setFilter] = useState<F | undefined>()
|
|
44
|
-
const previousValue = useRef(value)
|
|
45
|
-
const renderKey = params.renderKey ?? defaultRenderKey
|
|
46
|
-
|
|
47
|
-
useEffect(() => {
|
|
48
|
-
params.onChange?.(value, previousValue.current)
|
|
49
|
-
previousValue.current = value
|
|
50
|
-
}, [value])
|
|
51
|
-
|
|
52
|
-
const { options, isUnfilteredButChecked } = useMemo(() => {
|
|
53
|
-
if (!params.applyFilter || !filter) return { options: params.options, isUnfilteredButChecked: () => false }
|
|
54
|
-
const filtered: T[] = []
|
|
55
|
-
const unfilteredButChecked: T[] = []
|
|
56
|
-
const map = new Map<number | string | undefined, boolean>()
|
|
57
|
-
const valueKeys = value.map(o => renderKey(o))
|
|
58
|
-
for (const o of params.options) {
|
|
59
|
-
const key = renderKey(o)
|
|
60
|
-
if (params.applyFilter(filter, o)) filtered.push(o)
|
|
61
|
-
else if (valueKeys.includes(key)) {
|
|
62
|
-
unfilteredButChecked.push(o)
|
|
63
|
-
map.set(key, true)
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return { options: [...unfilteredButChecked, ...filtered], isUnfilteredButChecked: (o: T) => map.has(renderKey(o)) }
|
|
67
|
-
}, [params.options, filter])
|
|
68
|
-
|
|
69
|
-
const selectAll = useCallback(() => {
|
|
70
|
-
setValue([...options])
|
|
71
|
-
}, [options])
|
|
72
|
-
|
|
73
|
-
const removeSelection = useCallback(() => {
|
|
74
|
-
setValue([])
|
|
75
|
-
}, [])
|
|
76
|
-
|
|
77
|
-
return {
|
|
78
|
-
/**
|
|
79
|
-
* Selects all the options currently visible.
|
|
80
|
-
*/
|
|
81
|
-
selectAll,
|
|
82
|
-
/**
|
|
83
|
-
* Removes all options from the selection.
|
|
84
|
-
*/
|
|
85
|
-
removeSelection,
|
|
86
|
-
/**
|
|
87
|
-
* The current filter applied.
|
|
88
|
-
*/
|
|
89
|
-
filter,
|
|
90
|
-
/**
|
|
91
|
-
* Apply a new filter.
|
|
92
|
-
*/
|
|
93
|
-
setFilter,
|
|
94
|
-
/**
|
|
95
|
-
* The options that should be passed to the checkbox group.
|
|
96
|
-
*/
|
|
97
|
-
options,
|
|
98
|
-
/**
|
|
99
|
-
* The value that should be passed to the checkbox group.
|
|
100
|
-
*/
|
|
101
|
-
value,
|
|
102
|
-
/**
|
|
103
|
-
* Changes the current value, should be passed to the property `onChange` of the checkbox group.
|
|
104
|
-
*/
|
|
105
|
-
setValue,
|
|
106
|
-
/**
|
|
107
|
-
* A function to render a unique key for an option. Should be passed to the property `renderKey` of the checkbox group.
|
|
108
|
-
*/
|
|
109
|
-
renderKey,
|
|
110
|
-
/**
|
|
111
|
-
* A function that returns true if the option is filtered out, but checked.
|
|
112
|
-
* @param option the option to check.
|
|
113
|
-
* @returns true if the option was filtered out, but is checked; false otherwise.
|
|
114
|
-
*/
|
|
115
|
-
isUnfilteredButChecked,
|
|
116
|
-
/**
|
|
117
|
-
* True if all options showing are selected, false otherwise.
|
|
118
|
-
*/
|
|
119
|
-
isAllSelected: value.length === options.length,
|
|
120
|
-
}
|
|
121
|
-
}
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import { defaultRenderKey } from './options'
|
|
3
|
+
|
|
4
|
+
export interface CheckboxGroupHookParams<T, F = string> {
|
|
5
|
+
/**
|
|
6
|
+
* The initial value for the checkbox group.
|
|
7
|
+
*
|
|
8
|
+
* @default []
|
|
9
|
+
*/
|
|
10
|
+
initialValue?: T[],
|
|
11
|
+
/**
|
|
12
|
+
* A function to apply a filter to an option. Must return true if the option respects the filter and false otherwise.
|
|
13
|
+
* @param filter the current filter.
|
|
14
|
+
* @param option the current option.
|
|
15
|
+
* @returns true if the option should pass the filter, false if the option should be discarded.
|
|
16
|
+
*/
|
|
17
|
+
applyFilter?: (filter: F, option: T) => boolean,
|
|
18
|
+
/**
|
|
19
|
+
* The full set of options for the checkbox group.
|
|
20
|
+
*/
|
|
21
|
+
options: T[],
|
|
22
|
+
/**
|
|
23
|
+
* A function that produces a unique id for an option.
|
|
24
|
+
* @param option the current option.
|
|
25
|
+
* @returns a unique key.
|
|
26
|
+
*/
|
|
27
|
+
renderKey: (option: T) => string | number | undefined,
|
|
28
|
+
/**
|
|
29
|
+
* A function to call whenever the value changes.
|
|
30
|
+
* @param newValue the new value.
|
|
31
|
+
* @param previousValue the previous value.
|
|
32
|
+
*/
|
|
33
|
+
onChange?: (newValue: T[], previousValue: T[]) => void,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Use this hook to easily implement filtering and selection controls for a checkbox group.
|
|
38
|
+
* @param params the parameters to create the controls.
|
|
39
|
+
* @returns the checkbox controls.
|
|
40
|
+
*/
|
|
41
|
+
export function useCheckboxGroupControls<T, F = string>(params: CheckboxGroupHookParams<T, F>) {
|
|
42
|
+
const [value, setValue] = useState(params.initialValue ?? [])
|
|
43
|
+
const [filter, setFilter] = useState<F | undefined>()
|
|
44
|
+
const previousValue = useRef(value)
|
|
45
|
+
const renderKey = params.renderKey ?? defaultRenderKey
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
params.onChange?.(value, previousValue.current)
|
|
49
|
+
previousValue.current = value
|
|
50
|
+
}, [value])
|
|
51
|
+
|
|
52
|
+
const { options, isUnfilteredButChecked } = useMemo(() => {
|
|
53
|
+
if (!params.applyFilter || !filter) return { options: params.options, isUnfilteredButChecked: () => false }
|
|
54
|
+
const filtered: T[] = []
|
|
55
|
+
const unfilteredButChecked: T[] = []
|
|
56
|
+
const map = new Map<number | string | undefined, boolean>()
|
|
57
|
+
const valueKeys = value.map(o => renderKey(o))
|
|
58
|
+
for (const o of params.options) {
|
|
59
|
+
const key = renderKey(o)
|
|
60
|
+
if (params.applyFilter(filter, o)) filtered.push(o)
|
|
61
|
+
else if (valueKeys.includes(key)) {
|
|
62
|
+
unfilteredButChecked.push(o)
|
|
63
|
+
map.set(key, true)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { options: [...unfilteredButChecked, ...filtered], isUnfilteredButChecked: (o: T) => map.has(renderKey(o)) }
|
|
67
|
+
}, [params.options, filter])
|
|
68
|
+
|
|
69
|
+
const selectAll = useCallback(() => {
|
|
70
|
+
setValue([...options])
|
|
71
|
+
}, [options])
|
|
72
|
+
|
|
73
|
+
const removeSelection = useCallback(() => {
|
|
74
|
+
setValue([])
|
|
75
|
+
}, [])
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
/**
|
|
79
|
+
* Selects all the options currently visible.
|
|
80
|
+
*/
|
|
81
|
+
selectAll,
|
|
82
|
+
/**
|
|
83
|
+
* Removes all options from the selection.
|
|
84
|
+
*/
|
|
85
|
+
removeSelection,
|
|
86
|
+
/**
|
|
87
|
+
* The current filter applied.
|
|
88
|
+
*/
|
|
89
|
+
filter,
|
|
90
|
+
/**
|
|
91
|
+
* Apply a new filter.
|
|
92
|
+
*/
|
|
93
|
+
setFilter,
|
|
94
|
+
/**
|
|
95
|
+
* The options that should be passed to the checkbox group.
|
|
96
|
+
*/
|
|
97
|
+
options,
|
|
98
|
+
/**
|
|
99
|
+
* The value that should be passed to the checkbox group.
|
|
100
|
+
*/
|
|
101
|
+
value,
|
|
102
|
+
/**
|
|
103
|
+
* Changes the current value, should be passed to the property `onChange` of the checkbox group.
|
|
104
|
+
*/
|
|
105
|
+
setValue,
|
|
106
|
+
/**
|
|
107
|
+
* A function to render a unique key for an option. Should be passed to the property `renderKey` of the checkbox group.
|
|
108
|
+
*/
|
|
109
|
+
renderKey,
|
|
110
|
+
/**
|
|
111
|
+
* A function that returns true if the option is filtered out, but checked.
|
|
112
|
+
* @param option the option to check.
|
|
113
|
+
* @returns true if the option was filtered out, but is checked; false otherwise.
|
|
114
|
+
*/
|
|
115
|
+
isUnfilteredButChecked,
|
|
116
|
+
/**
|
|
117
|
+
* True if all options showing are selected, false otherwise.
|
|
118
|
+
*/
|
|
119
|
+
isAllSelected: value.length === options.length,
|
|
120
|
+
}
|
|
121
|
+
}
|
package/src/utils/css.ts
CHANGED
|
@@ -1,119 +1,119 @@
|
|
|
1
|
-
import { ColorKey, listToClass, theme } from '@stack-spot/portal-theme'
|
|
2
|
-
import { SpacingKey } from '@stack-spot/portal-theme/dist/definition'
|
|
3
|
-
import { isNil, omit, omitBy } from 'lodash'
|
|
4
|
-
import { TextAppearance, WithStyleShortcuts } from '../types'
|
|
5
|
-
|
|
6
|
-
export function colorNameToColorVariable(name: ColorKey): string {
|
|
7
|
-
return `var(--${name.replaceAll('.', '-')})`
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function applyColor(style: React.CSSProperties | undefined, color: ColorKey | undefined) {
|
|
11
|
-
return color ? { ...style, color: colorNameToColorVariable(color) } : style
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function textAppearanceToClass(appearance: TextAppearance): string {
|
|
15
|
-
return `text-${appearance}`
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function applyTextAppearance(className: string | undefined, appearance: TextAppearance | undefined) {
|
|
19
|
-
return listToClass([className, appearance ? textAppearanceToClass(appearance) : undefined])
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function applyCSSVariable(style: React.CSSProperties | undefined, name: string, value: any) {
|
|
23
|
-
if (isNil(value)) return style
|
|
24
|
-
return { ...style, [`--${name}`]: `${value}` } as React.CSSProperties
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function applyCSSVariables(style: React.CSSProperties | undefined, vars: ({ name: string, value: any } | '' | false | undefined)[]) {
|
|
28
|
-
const newStyle = { ...style }
|
|
29
|
-
for (const variable of vars) {
|
|
30
|
-
if (!variable) continue
|
|
31
|
-
(newStyle as any)[`--${variable.name}`] = variable.value
|
|
32
|
-
}
|
|
33
|
-
return newStyle as React.CSSProperties
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function spacingToStyle(spacing: SpacingKey | SpacingKey[] | string | undefined) {
|
|
37
|
-
if (typeof spacing === 'string') return spacing
|
|
38
|
-
if (typeof spacing === 'number') return theme.spacing[spacing]
|
|
39
|
-
return spacing?.map(s => theme.spacing[s]).join(' ')
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function getStyleFromProps({
|
|
43
|
-
bg,
|
|
44
|
-
fg,
|
|
45
|
-
border,
|
|
46
|
-
radius,
|
|
47
|
-
justifyContent,
|
|
48
|
-
alignItems,
|
|
49
|
-
flex,
|
|
50
|
-
gap,
|
|
51
|
-
m,
|
|
52
|
-
mt,
|
|
53
|
-
mb,
|
|
54
|
-
ml,
|
|
55
|
-
mr,
|
|
56
|
-
p,
|
|
57
|
-
pt,
|
|
58
|
-
pb,
|
|
59
|
-
pl,
|
|
60
|
-
pr,
|
|
61
|
-
w,
|
|
62
|
-
h,
|
|
63
|
-
}: WithStyleShortcuts): React.CSSProperties {
|
|
64
|
-
const [bgColor, bgLevel] = bg?.split('.') ?? []
|
|
65
|
-
const [fgColor, fgLevel] = fg?.split('.') ?? []
|
|
66
|
-
const [borderColor, borderLevel] = border?.split('.') ?? []
|
|
67
|
-
const borderColorVar = (theme.color as any)[borderColor]?.[borderLevel]
|
|
68
|
-
const newStyle: React.CSSProperties = {
|
|
69
|
-
backgroundColor: (theme.color as any)[bgColor]?.[bgLevel],
|
|
70
|
-
color: (theme.color as any)[fgColor]?.[fgLevel],
|
|
71
|
-
border: borderColorVar ? `1px solid ${borderColorVar}` : undefined,
|
|
72
|
-
borderRadius: radius ? theme.radius[radius] : undefined,
|
|
73
|
-
justifyContent,
|
|
74
|
-
alignItems,
|
|
75
|
-
flex,
|
|
76
|
-
gap,
|
|
77
|
-
margin: spacingToStyle(m),
|
|
78
|
-
marginTop: spacingToStyle(mt),
|
|
79
|
-
marginBottom: spacingToStyle(mb),
|
|
80
|
-
marginLeft: spacingToStyle(ml),
|
|
81
|
-
marginRight: spacingToStyle(mr),
|
|
82
|
-
padding: spacingToStyle(p),
|
|
83
|
-
paddingTop: spacingToStyle(pt),
|
|
84
|
-
paddingBottom: spacingToStyle(pb),
|
|
85
|
-
paddingLeft: spacingToStyle(pl),
|
|
86
|
-
paddingRight: spacingToStyle(pr),
|
|
87
|
-
width: w,
|
|
88
|
-
height: h,
|
|
89
|
-
}
|
|
90
|
-
return omitBy(newStyle, v => v === undefined)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function applyStyles({ style, ...props }: WithStyleShortcuts & Record<string, any>) {
|
|
94
|
-
const styleShortcutKeys = [
|
|
95
|
-
'bg', 'fg', 'border', 'radius', 'justifyContent', 'alignItems', 'flex', 'gap', 'm', 'mt', 'mb', 'ml', 'mr', 'p', 'pt', 'pb', 'pl',
|
|
96
|
-
'pr', 'w', 'h',
|
|
97
|
-
]
|
|
98
|
-
const newStyle = getStyleFromProps(props)
|
|
99
|
-
return { style: { ...newStyle, ...style }, ...omit(props, styleShortcutKeys) }
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// AI generated
|
|
103
|
-
export function styleObjectToCssString(styleObject: React.CSSProperties = {}) {
|
|
104
|
-
return Object.entries(styleObject)
|
|
105
|
-
.map(([property, value]) => {
|
|
106
|
-
// Convert camelCase to kebab-case
|
|
107
|
-
const cssProperty = property.replace(/([A-Z])/g, '-$1').toLowerCase()
|
|
108
|
-
|
|
109
|
-
// Append 'px' to numeric values for common dimension properties
|
|
110
|
-
const cssValue =
|
|
111
|
-
typeof value === 'number' &&
|
|
112
|
-
!['zIndex', 'opacity', 'fontWeight'].includes(property)
|
|
113
|
-
? `${value}px`
|
|
114
|
-
: value
|
|
115
|
-
|
|
116
|
-
return `${cssProperty}: ${cssValue}`
|
|
117
|
-
})
|
|
118
|
-
.join('; ')
|
|
119
|
-
}
|
|
1
|
+
import { ColorKey, listToClass, theme } from '@stack-spot/portal-theme'
|
|
2
|
+
import { SpacingKey } from '@stack-spot/portal-theme/dist/definition'
|
|
3
|
+
import { isNil, omit, omitBy } from 'lodash'
|
|
4
|
+
import { TextAppearance, WithStyleShortcuts } from '../types'
|
|
5
|
+
|
|
6
|
+
export function colorNameToColorVariable(name: ColorKey): string {
|
|
7
|
+
return `var(--${name.replaceAll('.', '-')})`
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function applyColor(style: React.CSSProperties | undefined, color: ColorKey | undefined) {
|
|
11
|
+
return color ? { ...style, color: colorNameToColorVariable(color) } : style
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function textAppearanceToClass(appearance: TextAppearance): string {
|
|
15
|
+
return `text-${appearance}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function applyTextAppearance(className: string | undefined, appearance: TextAppearance | undefined) {
|
|
19
|
+
return listToClass([className, appearance ? textAppearanceToClass(appearance) : undefined])
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function applyCSSVariable(style: React.CSSProperties | undefined, name: string, value: any) {
|
|
23
|
+
if (isNil(value)) return style
|
|
24
|
+
return { ...style, [`--${name}`]: `${value}` } as React.CSSProperties
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function applyCSSVariables(style: React.CSSProperties | undefined, vars: ({ name: string, value: any } | '' | false | undefined)[]) {
|
|
28
|
+
const newStyle = { ...style }
|
|
29
|
+
for (const variable of vars) {
|
|
30
|
+
if (!variable) continue
|
|
31
|
+
(newStyle as any)[`--${variable.name}`] = variable.value
|
|
32
|
+
}
|
|
33
|
+
return newStyle as React.CSSProperties
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function spacingToStyle(spacing: SpacingKey | SpacingKey[] | string | undefined) {
|
|
37
|
+
if (typeof spacing === 'string') return spacing
|
|
38
|
+
if (typeof spacing === 'number') return theme.spacing[spacing]
|
|
39
|
+
return spacing?.map(s => theme.spacing[s]).join(' ')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getStyleFromProps({
|
|
43
|
+
bg,
|
|
44
|
+
fg,
|
|
45
|
+
border,
|
|
46
|
+
radius,
|
|
47
|
+
justifyContent,
|
|
48
|
+
alignItems,
|
|
49
|
+
flex,
|
|
50
|
+
gap,
|
|
51
|
+
m,
|
|
52
|
+
mt,
|
|
53
|
+
mb,
|
|
54
|
+
ml,
|
|
55
|
+
mr,
|
|
56
|
+
p,
|
|
57
|
+
pt,
|
|
58
|
+
pb,
|
|
59
|
+
pl,
|
|
60
|
+
pr,
|
|
61
|
+
w,
|
|
62
|
+
h,
|
|
63
|
+
}: WithStyleShortcuts): React.CSSProperties {
|
|
64
|
+
const [bgColor, bgLevel] = bg?.split('.') ?? []
|
|
65
|
+
const [fgColor, fgLevel] = fg?.split('.') ?? []
|
|
66
|
+
const [borderColor, borderLevel] = border?.split('.') ?? []
|
|
67
|
+
const borderColorVar = (theme.color as any)[borderColor]?.[borderLevel]
|
|
68
|
+
const newStyle: React.CSSProperties = {
|
|
69
|
+
backgroundColor: (theme.color as any)[bgColor]?.[bgLevel],
|
|
70
|
+
color: (theme.color as any)[fgColor]?.[fgLevel],
|
|
71
|
+
border: borderColorVar ? `1px solid ${borderColorVar}` : undefined,
|
|
72
|
+
borderRadius: radius ? theme.radius[radius] : undefined,
|
|
73
|
+
justifyContent,
|
|
74
|
+
alignItems,
|
|
75
|
+
flex,
|
|
76
|
+
gap,
|
|
77
|
+
margin: spacingToStyle(m),
|
|
78
|
+
marginTop: spacingToStyle(mt),
|
|
79
|
+
marginBottom: spacingToStyle(mb),
|
|
80
|
+
marginLeft: spacingToStyle(ml),
|
|
81
|
+
marginRight: spacingToStyle(mr),
|
|
82
|
+
padding: spacingToStyle(p),
|
|
83
|
+
paddingTop: spacingToStyle(pt),
|
|
84
|
+
paddingBottom: spacingToStyle(pb),
|
|
85
|
+
paddingLeft: spacingToStyle(pl),
|
|
86
|
+
paddingRight: spacingToStyle(pr),
|
|
87
|
+
width: w,
|
|
88
|
+
height: h,
|
|
89
|
+
}
|
|
90
|
+
return omitBy(newStyle, v => v === undefined)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function applyStyles({ style, ...props }: WithStyleShortcuts & Record<string, any>) {
|
|
94
|
+
const styleShortcutKeys = [
|
|
95
|
+
'bg', 'fg', 'border', 'radius', 'justifyContent', 'alignItems', 'flex', 'gap', 'm', 'mt', 'mb', 'ml', 'mr', 'p', 'pt', 'pb', 'pl',
|
|
96
|
+
'pr', 'w', 'h',
|
|
97
|
+
]
|
|
98
|
+
const newStyle = getStyleFromProps(props)
|
|
99
|
+
return { style: { ...newStyle, ...style }, ...omit(props, styleShortcutKeys) }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// AI generated
|
|
103
|
+
export function styleObjectToCssString(styleObject: React.CSSProperties = {}) {
|
|
104
|
+
return Object.entries(styleObject)
|
|
105
|
+
.map(([property, value]) => {
|
|
106
|
+
// Convert camelCase to kebab-case
|
|
107
|
+
const cssProperty = property.replace(/([A-Z])/g, '-$1').toLowerCase()
|
|
108
|
+
|
|
109
|
+
// Append 'px' to numeric values for common dimension properties
|
|
110
|
+
const cssValue =
|
|
111
|
+
typeof value === 'number' &&
|
|
112
|
+
!['zIndex', 'opacity', 'fontWeight'].includes(property)
|
|
113
|
+
? `${value}px`
|
|
114
|
+
: value
|
|
115
|
+
|
|
116
|
+
return `${cssProperty}: ${cssValue}`
|
|
117
|
+
})
|
|
118
|
+
.join('; ')
|
|
119
|
+
}
|