@stack-spot/portal-components 2.27.0 → 2.27.2
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 +635 -621
- package/dist/components/AnimatedHeight.d.ts +1 -1
- package/dist/components/AnimatedHeight.js +26 -26
- package/dist/components/AsyncContent.d.ts +1 -1
- package/dist/components/AsyncContent.js +1 -1
- package/dist/components/BannerWarning.d.ts +1 -1
- package/dist/components/BannerWarning.js +1 -1
- package/dist/components/Breadcrumb/index.d.ts +2 -2
- package/dist/components/Breadcrumb/index.js +1 -1
- package/dist/components/Breadcrumb/styled.js +31 -31
- package/dist/components/ButtonLoading.d.ts +1 -1
- package/dist/components/ButtonLoading.js +1 -1
- package/dist/components/ChatBot.d.ts +1 -1
- package/dist/components/ChatBot.js +1 -1
- package/dist/components/ContentValidateFilter.d.ts +1 -1
- package/dist/components/ContentValidateFilter.js +1 -1
- package/dist/components/FadingOverflow.d.ts +1 -1
- package/dist/components/FadingOverflow.js +69 -69
- package/dist/components/FileTreeView/More.d.ts +1 -1
- package/dist/components/FileTreeView/More.js +1 -1
- package/dist/components/FileTreeView/index.d.ts +1 -1
- package/dist/components/FileTreeView/index.js +1 -1
- package/dist/components/InfiniteScroll.d.ts +1 -1
- package/dist/components/InfiniteScroll.js +1 -1
- package/dist/components/InfoMaintenanceBanner.d.ts +1 -1
- package/dist/components/InfoMaintenanceBanner.js +2 -2
- package/dist/components/LazyMarkdown/BlockquoteMd.d.ts +1 -1
- package/dist/components/LazyMarkdown/BlockquoteMd.js +1 -1
- package/dist/components/LazyMarkdown/CodeViewer.d.ts +1 -1
- package/dist/components/LazyMarkdown/CodeViewer.js +76 -76
- package/dist/components/LazyMarkdown/Markdown.d.ts +1 -1
- package/dist/components/LazyMarkdown/Markdown.js +1 -1
- package/dist/components/LazyMarkdown/MarkdownButton.d.ts +1 -1
- package/dist/components/LazyMarkdown/MarkdownButton.js +1 -1
- package/dist/components/LazyMarkdown/Video.d.ts +1 -1
- package/dist/components/LazyMarkdown/Video.js +1 -1
- package/dist/components/LazyMarkdown/index.d.ts +1 -1
- package/dist/components/LazyMarkdown/index.js +1 -1
- package/dist/components/Placeholder.d.ts +7 -3
- package/dist/components/Placeholder.d.ts.map +1 -1
- package/dist/components/Placeholder.js +3 -3
- package/dist/components/Placeholder.js.map +1 -1
- package/dist/components/ScrollView.js +16 -16
- package/dist/components/Select/BadgeItem.d.ts +1 -1
- package/dist/components/Select/BadgeItem.js +1 -1
- package/dist/components/Select/ClearInput.d.ts +1 -1
- package/dist/components/Select/ClearInput.js +1 -1
- package/dist/components/Select/CloseItem.d.ts +1 -1
- package/dist/components/Select/CloseItem.js +1 -1
- package/dist/components/Select/CreatableSelect.js +1 -1
- package/dist/components/Select/CustomMenu.d.ts +1 -1
- package/dist/components/Select/CustomMenu.js +1 -1
- package/dist/components/Select/LabelItem.d.ts +1 -1
- package/dist/components/Select/LabelItem.js +1 -1
- package/dist/components/Select/MultiValue.d.ts +1 -1
- package/dist/components/Select/MultiValue.js +1 -1
- package/dist/components/Select/SelectInfiniteScroll.d.ts +1 -1
- package/dist/components/Select/SelectInfiniteScroll.js +1 -1
- package/dist/components/Select/SelectSearch.d.ts +1 -1
- package/dist/components/Select/SelectSearch.js +1 -1
- package/dist/components/SelectionList.d.ts +1 -1
- package/dist/components/SelectionList.js +61 -61
- package/dist/components/StatusCircle.d.ts +1 -1
- package/dist/components/StatusCircle.js +6 -6
- package/dist/components/Stepper/Navigation.js +4 -4
- package/dist/components/Stepper/Step.js +3 -3
- package/dist/components/Stepper/Stepper.js +6 -6
- package/dist/components/Stepper/headers.js +22 -22
- package/dist/components/Table/HeaderItem.js +1 -1
- package/dist/components/Table/SettingsVerticalMenu.d.ts +1 -1
- package/dist/components/Table/SettingsVerticalMenu.js +1 -1
- package/dist/components/Table/StyledLinkTable.d.ts +1 -1
- package/dist/components/Table/StyledLinkTable.js +5 -5
- package/dist/components/Table/TableData.d.ts +1 -1
- package/dist/components/Table/TableData.js +25 -25
- package/dist/components/TimelineSection.d.ts +1 -1
- package/dist/components/TimelineSection.js +14 -14
- package/dist/components/error/ErrorFeedback.d.ts +1 -1
- package/dist/components/error/ErrorFeedback.js +35 -35
- package/dist/components/error/NotFound.d.ts +1 -1
- package/dist/components/error/NotFound.js +1 -1
- package/dist/components/error/UnderMaintenance.d.ts +1 -1
- package/dist/components/error/UnderMaintenance.js +1 -1
- package/dist/components/form/Form/Form.d.ts +1 -1
- package/dist/components/form/Form/Form.js +1 -1
- package/dist/components/form/Form/FormGroup.d.ts +2 -2
- package/dist/components/form/Form/FormGroup.js +1 -1
- package/dist/components/form/SearchInput.d.ts +1 -1
- package/dist/components/form/SearchInput.js +1 -1
- package/dist/components/form/Select/CustomSelect.d.ts +1 -1
- package/dist/components/form/Select/CustomSelect.js +1 -1
- package/dist/components/form/Select/DetailedSelect.d.ts +1 -1
- package/dist/components/form/Select/DetailedSelect.js +1 -1
- package/dist/components/form/Select/Select.d.ts +1 -1
- package/dist/components/form/Select/Select.js +1 -1
- package/dist/components/form/Select/styled.js +161 -161
- package/dist/components/form/Select/utils.js +1 -1
- package/dist/components/notification/NotificationComponent.d.ts +1 -1
- package/dist/components/notification/NotificationComponent.js +54 -54
- package/dist/components/notification/NotificationItem.d.ts +1 -1
- package/dist/components/notification/NotificationItem.d.ts.map +1 -1
- package/dist/components/notification/NotificationItem.js +11 -5
- package/dist/components/notification/NotificationItem.js.map +1 -1
- package/dist/components/notification/NotificationList.d.ts +1 -1
- package/dist/components/notification/NotificationList.d.ts.map +1 -1
- package/dist/components/notification/NotificationList.js +44 -44
- package/dist/components/notification/NotificationList.js.map +1 -1
- package/dist/components/notification/NotificationPlaceholder.d.ts +1 -1
- package/dist/components/notification/NotificationPlaceholder.d.ts.map +1 -1
- package/dist/components/notification/NotificationPlaceholder.js +2 -2
- package/dist/components/notification/NotificationPlaceholder.js.map +1 -1
- package/dist/containers/NotificationsPage.d.ts +1 -1
- package/dist/containers/NotificationsPage.d.ts.map +1 -1
- package/dist/containers/NotificationsPage.js +24 -11
- package/dist/containers/NotificationsPage.js.map +1 -1
- package/dist/context/anchor.d.ts +1 -1
- package/dist/context/anchor.js +1 -1
- package/dist/context/loading.d.ts +1 -1
- package/dist/context/loading.js +1 -1
- package/dist/context/notification/context.d.ts +1 -1
- package/dist/context/notification/context.js +1 -1
- package/dist/context/notification/types.d.ts +1 -0
- package/dist/context/notification/types.d.ts.map +1 -1
- package/dist/hooks/date.js +1 -1
- package/dist/hooks/service-now.js +28 -28
- package/dist/svg/AI.d.ts +1 -1
- package/dist/svg/AI.js +1 -1
- package/dist/svg/CS.d.ts +1 -1
- package/dist/svg/CS.js +1 -1
- package/dist/svg/EDP.d.ts +1 -1
- package/dist/svg/EDP.js +1 -1
- package/dist/svg/Forbidden.d.ts +1 -1
- package/dist/svg/Forbidden.js +1 -1
- package/dist/svg/GenericPlaceholder.d.ts +4 -2
- package/dist/svg/GenericPlaceholder.d.ts.map +1 -1
- package/dist/svg/GenericPlaceholder.js +2 -2
- package/dist/svg/GenericPlaceholder.js.map +1 -1
- package/dist/svg/HUB.d.ts +1 -1
- package/dist/svg/HUB.js +1 -1
- package/dist/svg/Logo.d.ts +1 -1
- package/dist/svg/Logo.js +1 -1
- package/dist/svg/MiniLogo.d.ts +1 -1
- package/dist/svg/MiniLogo.js +1 -1
- package/dist/svg/NotFound.d.ts +1 -1
- package/dist/svg/NotFound.js +1 -1
- package/dist/svg/ServerError.d.ts +1 -1
- package/dist/svg/ServerError.js +1 -1
- package/dist/svg/Unauthenticated.d.ts +1 -1
- package/dist/svg/Unauthenticated.js +1 -1
- package/package.json +6 -6
- package/readme.md +66 -66
- package/src/components/AnimatedHeight.tsx +174 -174
- package/src/components/AsyncContent.tsx +78 -78
- package/src/components/BannerWarning.tsx +91 -91
- package/src/components/Breadcrumb/index.tsx +76 -76
- package/src/components/Breadcrumb/styled.ts +37 -37
- package/src/components/ButtonLoading.tsx +29 -29
- package/src/components/ChatBot.tsx +82 -82
- package/src/components/ContentValidateFilter.tsx +15 -15
- package/src/components/FadingOverflow.tsx +265 -265
- package/src/components/FileTreeView/More.tsx +114 -114
- package/src/components/FileTreeView/index.tsx +186 -186
- package/src/components/InfiniteScroll.tsx +24 -24
- package/src/components/InfoMaintenanceBanner.tsx +29 -29
- package/src/components/LazyMarkdown/BlockquoteMd.tsx +107 -107
- package/src/components/LazyMarkdown/CodeViewer.tsx +161 -161
- package/src/components/LazyMarkdown/Markdown.tsx +122 -122
- package/src/components/LazyMarkdown/MarkdownButton.tsx +24 -24
- package/src/components/LazyMarkdown/Video.tsx +13 -13
- package/src/components/LazyMarkdown/index.tsx +21 -21
- package/src/components/Placeholder.tsx +123 -118
- package/src/components/ScrollView.tsx +57 -57
- package/src/components/Select/BadgeItem.tsx +58 -58
- package/src/components/Select/ClearInput.tsx +24 -24
- package/src/components/Select/CloseItem.tsx +38 -38
- package/src/components/Select/CreatableSelect.tsx +155 -155
- package/src/components/Select/CustomMenu.tsx +16 -16
- package/src/components/Select/LabelItem.tsx +8 -8
- package/src/components/Select/MultiValue.tsx +49 -49
- package/src/components/Select/SelectInfiniteScroll.tsx +82 -82
- package/src/components/Select/SelectSearch.tsx +195 -195
- package/src/components/Select/index.tsx +7 -7
- package/src/components/Select/types.ts +8 -8
- package/src/components/SelectionList.tsx +427 -427
- package/src/components/StatusCircle.tsx +67 -67
- package/src/components/Stepper/Navigation.tsx +97 -97
- package/src/components/Stepper/Step.tsx +30 -30
- package/src/components/Stepper/Stepper.tsx +113 -113
- package/src/components/Stepper/headers.tsx +64 -64
- package/src/components/Stepper/index.ts +3 -3
- package/src/components/Table/HeaderItem.tsx +52 -52
- package/src/components/Table/SettingsVerticalMenu.tsx +50 -50
- package/src/components/Table/StyledLinkTable.tsx +22 -22
- package/src/components/Table/TableData.tsx +251 -251
- package/src/components/Table/index.tsx +2 -2
- package/src/components/TimelineSection.tsx +66 -66
- package/src/components/error/ErrorFeedback.tsx +217 -217
- package/src/components/error/NotFound.tsx +24 -24
- package/src/components/error/UnderMaintenance.tsx +30 -30
- package/src/components/error/index.ts +4 -4
- package/src/components/form/Form/Form.tsx +101 -101
- package/src/components/form/Form/FormGroup.tsx +221 -221
- package/src/components/form/Form/index.ts +2 -2
- package/src/components/form/SearchInput.tsx +69 -69
- package/src/components/form/Select/CustomSelect.tsx +232 -232
- package/src/components/form/Select/DetailedSelect.tsx +85 -85
- package/src/components/form/Select/Select.tsx +67 -67
- package/src/components/form/Select/index.ts +4 -4
- package/src/components/form/Select/styled.ts +165 -165
- package/src/components/form/Select/types.ts +112 -112
- package/src/components/form/Select/utils.tsx +28 -28
- package/src/components/notification/NotificationComponent.tsx +340 -340
- package/src/components/notification/NotificationItem.tsx +345 -336
- package/src/components/notification/NotificationList.tsx +179 -178
- package/src/components/notification/NotificationPlaceholder.tsx +44 -43
- package/src/components/notification/types.ts +72 -72
- package/src/containers/NotificationsPage.tsx +119 -98
- package/src/context/anchor.tsx +37 -37
- package/src/context/loading.tsx +36 -36
- package/src/context/notification/LazyNotificationList.ts +103 -103
- package/src/context/notification/NotificationController.ts +104 -104
- package/src/context/notification/context.tsx +23 -23
- package/src/context/notification/hooks.ts +98 -98
- package/src/context/notification/types.ts +66 -65
- package/src/hooks/date.ts +31 -31
- package/src/hooks/keyboard.tsx +128 -128
- package/src/hooks/manual-render.tsx +10 -10
- package/src/hooks/service-now.tsx +233 -233
- package/src/hooks/text.tsx +30 -30
- package/src/hooks/title.tsx +28 -28
- package/src/hooks/use-effect-once.tsx +43 -43
- package/src/index.ts +19 -19
- package/src/notifications.ts +11 -11
- package/src/svg/AI.tsx +41 -41
- package/src/svg/CS.tsx +48 -48
- package/src/svg/EDP.tsx +31 -31
- package/src/svg/Forbidden.tsx +22 -22
- package/src/svg/GenericPlaceholder.tsx +20 -20
- package/src/svg/HUB.tsx +48 -48
- package/src/svg/Logo.tsx +16 -16
- package/src/svg/MiniLogo.tsx +12 -12
- package/src/svg/NotFound.tsx +16 -16
- package/src/svg/ServerError.tsx +33 -33
- package/src/svg/Unauthenticated.tsx +16 -16
- package/src/svg/index.ts +11 -11
- package/src/utils/accessibility.ts +135 -135
- package/src/utils/cookie.ts +73 -73
- package/src/utils/promise.ts +5 -5
- package/src/utils/read-file.ts +16 -16
- package/tsconfig.json +10 -10
|
@@ -1,135 +1,135 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Important for accessibility.
|
|
3
|
-
*
|
|
4
|
-
* Makes it so we focus the next focusable element in the DOM hierarchy, disregarding the element passed as parameter and its children.
|
|
5
|
-
*
|
|
6
|
-
* If there's no next focusable element, the first focusable element of the page will be focused. If the page doesn't contain any focusable
|
|
7
|
-
* element, nothing happens.
|
|
8
|
-
*
|
|
9
|
-
* @param current the reference element to focus the next. If not provided, will be the currently active element.
|
|
10
|
-
*/
|
|
11
|
-
export function focusNextIgnoringChildren(current?: HTMLElement | null) {
|
|
12
|
-
current = current ?? document.activeElement as HTMLElement
|
|
13
|
-
while (current && !current.nextElementSibling) {
|
|
14
|
-
current = current?.parentElement
|
|
15
|
-
}
|
|
16
|
-
current = current?.nextElementSibling as HTMLElement
|
|
17
|
-
while (current && current.tabIndex < 0) {
|
|
18
|
-
current = (current.children.length ? current.firstChild : current.nextElementSibling) as HTMLElement
|
|
19
|
-
}
|
|
20
|
-
if (current) current?.focus?.()
|
|
21
|
-
else focusFirstChild(document)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export type TagPriority = 'a' | 'button' | 'input' | 'textarea' | 'select' | 'other'
|
|
25
|
-
export type TagPriorityElement = TagPriority | TagPriority[]
|
|
26
|
-
|
|
27
|
-
interface FocusOptions {
|
|
28
|
-
/**
|
|
29
|
-
* Instead of focusing the first element overall, focus the first according to this list of priorities.
|
|
30
|
-
*
|
|
31
|
-
* 'other' means elements that are normally not focusable, but have positive tabIndex values.
|
|
32
|
-
*/
|
|
33
|
-
priority?: TagPriorityElement[],
|
|
34
|
-
/**
|
|
35
|
-
* Ignores any element that matches this query selector.
|
|
36
|
-
*/
|
|
37
|
-
ignore?: string,
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const selectors: Record<TagPriority, string> = {
|
|
41
|
-
a: 'a[href]:not(:disabled)',
|
|
42
|
-
button: 'button:not(:disabled)',
|
|
43
|
-
input: 'input:not(:disabled):not([type="hidden"])',
|
|
44
|
-
select: 'textarea:not(:disabled)',
|
|
45
|
-
textarea: 'select:not(:disabled)',
|
|
46
|
-
other: '[tabindex]:not([tabindex="-1"])',
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Focus the first focusable child of the element provided. If the element has no focusable child, nothing happens.
|
|
51
|
-
*
|
|
52
|
-
* 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
|
|
53
|
-
* list.
|
|
54
|
-
*
|
|
55
|
-
* An ignore query selector can also be passed in the options parameter. If the first focusable element matches the query selector, the
|
|
56
|
-
* next element is focused instead.
|
|
57
|
-
*
|
|
58
|
-
* @example
|
|
59
|
-
* Suppose the children of element are: h1, button, p, input, select.
|
|
60
|
-
* 1. We don't pass a priority list. The focused element will be the button.
|
|
61
|
-
* 2. Our priority list is ['button']. The focused element will be the button.
|
|
62
|
-
* 3. Our priority list is ['input', 'button']. The focused element will be the input.
|
|
63
|
-
* 4. Our priority list is ['select', 'input']. The focused element will be the select.
|
|
64
|
-
* 5. Our priority list is [['select', 'input'], 'button']. The focused element will be the input.
|
|
65
|
-
*
|
|
66
|
-
* @param element the element to search a child to focus.
|
|
67
|
-
* @param options optional.
|
|
68
|
-
*/
|
|
69
|
-
export function focusFirstChild(element: HTMLElement | Document | null | undefined, { priority = [], ignore }: FocusOptions = {}) {
|
|
70
|
-
const allFocusableTags: TagPriority[] = ['a', 'button', 'input', 'other', 'select', 'textarea']
|
|
71
|
-
const focusableList: (NodeListOf<HTMLElement> | undefined)[] = [
|
|
72
|
-
element?.querySelectorAll(allFocusableTags.map(t => selectors[t]).join(', ')),
|
|
73
|
-
]
|
|
74
|
-
for (const p of priority) {
|
|
75
|
-
const tags = Array.isArray(p) ? p : [p]
|
|
76
|
-
const querySelectors = tags.map(t => selectors[t])
|
|
77
|
-
focusableList.unshift(element?.querySelectorAll(querySelectors.join(', ')))
|
|
78
|
-
}
|
|
79
|
-
for (const focusable of focusableList ?? []) {
|
|
80
|
-
for (const f of focusable ?? []) {
|
|
81
|
-
if (!ignore || !f.matches(ignore)) {
|
|
82
|
-
const styles = window.getComputedStyle(f)
|
|
83
|
-
if (styles.display != 'none' && styles.visibility != 'hidden') return f.focus()
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Checks if an element can receive focus.
|
|
91
|
-
*
|
|
92
|
-
* Elements can receive focus only if:
|
|
93
|
-
* - they exist;
|
|
94
|
-
* - they're visible;
|
|
95
|
-
* - they're not disabled;
|
|
96
|
-
* - they are a focusable tag name or have a positive tab index;
|
|
97
|
-
* - they don't have a negative tab index.
|
|
98
|
-
* @param element the element to check.
|
|
99
|
-
* @returns true if the element is focusable, false otherwise.
|
|
100
|
-
*/
|
|
101
|
-
export function isFocusable(element?: Element | null) {
|
|
102
|
-
if (!element) return false
|
|
103
|
-
// is disabled: return false
|
|
104
|
-
if (element.ariaDisabled || element.getAttribute('disabled') !== null) return false
|
|
105
|
-
// is invisible: return false
|
|
106
|
-
if (!element.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true })) return false
|
|
107
|
-
// has tab index: return false if negative, true otherwise
|
|
108
|
-
const tabIndexStr = element.getAttribute('tabindex')
|
|
109
|
-
const tabIndex = tabIndexStr ? parseInt(tabIndexStr) : undefined
|
|
110
|
-
if (tabIndex !== undefined) return tabIndex >= 0
|
|
111
|
-
// check the tag name
|
|
112
|
-
return ['a', 'button', 'input', 'iframe', 'select', 'textarea'].includes(element.tagName.toLowerCase() ?? '')
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Focus the element passed as parameter if it can receive focus. Otherwise, decides on another element to focus based on the HTML tree
|
|
117
|
-
* and accessibility attributes.
|
|
118
|
-
*
|
|
119
|
-
* Decision making:
|
|
120
|
-
* - If the element is focusable, focus it.
|
|
121
|
-
* - If the element is not focusable, check if it has an id and if another element controls it (aria-controls).
|
|
122
|
-
* - If the element is controlled by another, try to focus the controller, based in this same algorithm.
|
|
123
|
-
* - Otherwise, try to focus its parent.
|
|
124
|
-
*
|
|
125
|
-
* If no focusable element is found. Nothing happens.
|
|
126
|
-
* @param element the element to focus.
|
|
127
|
-
*/
|
|
128
|
-
export function focusAccessibleElement(element?: Element | null) {
|
|
129
|
-
let elementToFocus = element as HTMLElement | null | undefined
|
|
130
|
-
while (elementToFocus && !isFocusable(elementToFocus)) {
|
|
131
|
-
const controlledBy = elementToFocus.id ? document.querySelector(`[aria-controls="${elementToFocus.id}"]`) : undefined
|
|
132
|
-
elementToFocus = (controlledBy ?? elementToFocus.parentElement) as HTMLElement | null | undefined
|
|
133
|
-
}
|
|
134
|
-
elementToFocus?.focus?.()
|
|
135
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Important for accessibility.
|
|
3
|
+
*
|
|
4
|
+
* Makes it so we focus the next focusable element in the DOM hierarchy, disregarding the element passed as parameter and its children.
|
|
5
|
+
*
|
|
6
|
+
* If there's no next focusable element, the first focusable element of the page will be focused. If the page doesn't contain any focusable
|
|
7
|
+
* element, nothing happens.
|
|
8
|
+
*
|
|
9
|
+
* @param current the reference element to focus the next. If not provided, will be the currently active element.
|
|
10
|
+
*/
|
|
11
|
+
export function focusNextIgnoringChildren(current?: HTMLElement | null) {
|
|
12
|
+
current = current ?? document.activeElement as HTMLElement
|
|
13
|
+
while (current && !current.nextElementSibling) {
|
|
14
|
+
current = current?.parentElement
|
|
15
|
+
}
|
|
16
|
+
current = current?.nextElementSibling as HTMLElement
|
|
17
|
+
while (current && current.tabIndex < 0) {
|
|
18
|
+
current = (current.children.length ? current.firstChild : current.nextElementSibling) as HTMLElement
|
|
19
|
+
}
|
|
20
|
+
if (current) current?.focus?.()
|
|
21
|
+
else focusFirstChild(document)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type TagPriority = 'a' | 'button' | 'input' | 'textarea' | 'select' | 'other'
|
|
25
|
+
export type TagPriorityElement = TagPriority | TagPriority[]
|
|
26
|
+
|
|
27
|
+
interface FocusOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Instead of focusing the first element overall, focus the first according to this list of priorities.
|
|
30
|
+
*
|
|
31
|
+
* 'other' means elements that are normally not focusable, but have positive tabIndex values.
|
|
32
|
+
*/
|
|
33
|
+
priority?: TagPriorityElement[],
|
|
34
|
+
/**
|
|
35
|
+
* Ignores any element that matches this query selector.
|
|
36
|
+
*/
|
|
37
|
+
ignore?: string,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const selectors: Record<TagPriority, string> = {
|
|
41
|
+
a: 'a[href]:not(:disabled)',
|
|
42
|
+
button: 'button:not(:disabled)',
|
|
43
|
+
input: 'input:not(:disabled):not([type="hidden"])',
|
|
44
|
+
select: 'textarea:not(:disabled)',
|
|
45
|
+
textarea: 'select:not(:disabled)',
|
|
46
|
+
other: '[tabindex]:not([tabindex="-1"])',
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Focus the first focusable child of the element provided. If the element has no focusable child, nothing happens.
|
|
51
|
+
*
|
|
52
|
+
* 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
|
|
53
|
+
* list.
|
|
54
|
+
*
|
|
55
|
+
* An ignore query selector can also be passed in the options parameter. If the first focusable element matches the query selector, the
|
|
56
|
+
* next element is focused instead.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* Suppose the children of element are: h1, button, p, input, select.
|
|
60
|
+
* 1. We don't pass a priority list. The focused element will be the button.
|
|
61
|
+
* 2. Our priority list is ['button']. The focused element will be the button.
|
|
62
|
+
* 3. Our priority list is ['input', 'button']. The focused element will be the input.
|
|
63
|
+
* 4. Our priority list is ['select', 'input']. The focused element will be the select.
|
|
64
|
+
* 5. Our priority list is [['select', 'input'], 'button']. The focused element will be the input.
|
|
65
|
+
*
|
|
66
|
+
* @param element the element to search a child to focus.
|
|
67
|
+
* @param options optional.
|
|
68
|
+
*/
|
|
69
|
+
export function focusFirstChild(element: HTMLElement | Document | null | undefined, { priority = [], ignore }: FocusOptions = {}) {
|
|
70
|
+
const allFocusableTags: TagPriority[] = ['a', 'button', 'input', 'other', 'select', 'textarea']
|
|
71
|
+
const focusableList: (NodeListOf<HTMLElement> | undefined)[] = [
|
|
72
|
+
element?.querySelectorAll(allFocusableTags.map(t => selectors[t]).join(', ')),
|
|
73
|
+
]
|
|
74
|
+
for (const p of priority) {
|
|
75
|
+
const tags = Array.isArray(p) ? p : [p]
|
|
76
|
+
const querySelectors = tags.map(t => selectors[t])
|
|
77
|
+
focusableList.unshift(element?.querySelectorAll(querySelectors.join(', ')))
|
|
78
|
+
}
|
|
79
|
+
for (const focusable of focusableList ?? []) {
|
|
80
|
+
for (const f of focusable ?? []) {
|
|
81
|
+
if (!ignore || !f.matches(ignore)) {
|
|
82
|
+
const styles = window.getComputedStyle(f)
|
|
83
|
+
if (styles.display != 'none' && styles.visibility != 'hidden') return f.focus()
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Checks if an element can receive focus.
|
|
91
|
+
*
|
|
92
|
+
* Elements can receive focus only if:
|
|
93
|
+
* - they exist;
|
|
94
|
+
* - they're visible;
|
|
95
|
+
* - they're not disabled;
|
|
96
|
+
* - they are a focusable tag name or have a positive tab index;
|
|
97
|
+
* - they don't have a negative tab index.
|
|
98
|
+
* @param element the element to check.
|
|
99
|
+
* @returns true if the element is focusable, false otherwise.
|
|
100
|
+
*/
|
|
101
|
+
export function isFocusable(element?: Element | null) {
|
|
102
|
+
if (!element) return false
|
|
103
|
+
// is disabled: return false
|
|
104
|
+
if (element.ariaDisabled || element.getAttribute('disabled') !== null) return false
|
|
105
|
+
// is invisible: return false
|
|
106
|
+
if (!element.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true })) return false
|
|
107
|
+
// has tab index: return false if negative, true otherwise
|
|
108
|
+
const tabIndexStr = element.getAttribute('tabindex')
|
|
109
|
+
const tabIndex = tabIndexStr ? parseInt(tabIndexStr) : undefined
|
|
110
|
+
if (tabIndex !== undefined) return tabIndex >= 0
|
|
111
|
+
// check the tag name
|
|
112
|
+
return ['a', 'button', 'input', 'iframe', 'select', 'textarea'].includes(element.tagName.toLowerCase() ?? '')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Focus the element passed as parameter if it can receive focus. Otherwise, decides on another element to focus based on the HTML tree
|
|
117
|
+
* and accessibility attributes.
|
|
118
|
+
*
|
|
119
|
+
* Decision making:
|
|
120
|
+
* - If the element is focusable, focus it.
|
|
121
|
+
* - If the element is not focusable, check if it has an id and if another element controls it (aria-controls).
|
|
122
|
+
* - If the element is controlled by another, try to focus the controller, based in this same algorithm.
|
|
123
|
+
* - Otherwise, try to focus its parent.
|
|
124
|
+
*
|
|
125
|
+
* If no focusable element is found. Nothing happens.
|
|
126
|
+
* @param element the element to focus.
|
|
127
|
+
*/
|
|
128
|
+
export function focusAccessibleElement(element?: Element | null) {
|
|
129
|
+
let elementToFocus = element as HTMLElement | null | undefined
|
|
130
|
+
while (elementToFocus && !isFocusable(elementToFocus)) {
|
|
131
|
+
const controlledBy = elementToFocus.id ? document.querySelector(`[aria-controls="${elementToFocus.id}"]`) : undefined
|
|
132
|
+
elementToFocus = (controlledBy ?? elementToFocus.parentElement) as HTMLElement | null | undefined
|
|
133
|
+
}
|
|
134
|
+
elementToFocus?.focus?.()
|
|
135
|
+
}
|
package/src/utils/cookie.ts
CHANGED
|
@@ -1,73 +1,73 @@
|
|
|
1
|
-
import { isEmpty } from 'lodash'
|
|
2
|
-
|
|
3
|
-
const DEFAULT_DOMAIN_REGEX = new RegExp(/(\.*(prd|stg|dev)*.stackspot.com)|localhost/)
|
|
4
|
-
let cookieDomain = ''
|
|
5
|
-
let cookieAttributes: Record<string, string> = {}
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Setup the cookie utilities to use a another domain. By default, it's based on *.stackspot.com.
|
|
9
|
-
* @param domainRegex the regex to identify the domain. Example: {@link DEFAULT_DOMAIN_REGEX}.
|
|
10
|
-
*/
|
|
11
|
-
export function setupCookies(domainRegex: RegExp) {
|
|
12
|
-
const portalUrl = new URL(location.href)
|
|
13
|
-
cookieDomain = domainRegex.exec(portalUrl.host)?.[0] ?? ''
|
|
14
|
-
cookieAttributes = { domain: cookieDomain, SameSite: 'Strict' }
|
|
15
|
-
// Add Secure attribute if using HTTPS
|
|
16
|
-
if (portalUrl.protocol === 'https:') {
|
|
17
|
-
cookieAttributes['Secure'] = ''
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Retrieves the current domain used for cookies. To change the domain, use `setupCookies(domainRegex)`.
|
|
23
|
-
* @returns the current cookie domain.
|
|
24
|
-
*/
|
|
25
|
-
export function getCookieDomain() {
|
|
26
|
-
if (isEmpty(cookieAttributes)) setupCookies(DEFAULT_DOMAIN_REGEX)
|
|
27
|
-
return cookieDomain
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Get all cookies as an object where the keys are the cookie names and the values are the respective cookie values.
|
|
32
|
-
* @returns a map of cookie name to cookie value.
|
|
33
|
-
*/
|
|
34
|
-
export function getCookies(): Record<string, string | undefined> {
|
|
35
|
-
if (isEmpty(cookieAttributes)) setupCookies(DEFAULT_DOMAIN_REGEX)
|
|
36
|
-
return document.cookie.split('; ').reduce((result, current) => {
|
|
37
|
-
const [name, ...value] = current.split('=')
|
|
38
|
-
result[name] = value.join('=')
|
|
39
|
-
return result
|
|
40
|
-
}, {} as Record<string, string>)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Sets the value of a cookie.
|
|
45
|
-
* @param key the cookie name (identifier).
|
|
46
|
-
* @param value the cookie value.
|
|
47
|
-
* @param customAttributes Accepted values: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#attributes.
|
|
48
|
-
*/
|
|
49
|
-
export function setCookie(key: string, value: string, customAttributes: Record<string, string> = {}) {
|
|
50
|
-
if (isEmpty(cookieAttributes)) setupCookies(DEFAULT_DOMAIN_REGEX)
|
|
51
|
-
document.cookie = `${key}=${value};${objectToCookieString({ ...cookieAttributes, ...customAttributes })}`
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Removes a cookie.
|
|
56
|
-
* @param key the cookie name (identifier).
|
|
57
|
-
*/
|
|
58
|
-
export function removeCookie(key: string) {
|
|
59
|
-
if (isEmpty(cookieAttributes)) setupCookies(DEFAULT_DOMAIN_REGEX)
|
|
60
|
-
document.cookie = `${key}=;${objectToCookieString({ ...cookieAttributes, expires: 'Thu, 01 Jan 1970 00:00:00 GMT' })}`
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Retrieves the value of a cookie given its key/name.
|
|
65
|
-
* @param key the cookie name (identifier).
|
|
66
|
-
* @returns the cookie value or undefined, if the cookie doesn't exist.
|
|
67
|
-
*/
|
|
68
|
-
export function getCookie(key: string) {
|
|
69
|
-
return getCookies()[key]
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const objectToCookieString = (object: Record<string, string>): string =>
|
|
73
|
-
Object.entries(object).reduce((prev, [k, v]) => `${prev} ${k}${v !== '' ? `=${v}` : ''};`, '').trim()
|
|
1
|
+
import { isEmpty } from 'lodash'
|
|
2
|
+
|
|
3
|
+
const DEFAULT_DOMAIN_REGEX = new RegExp(/(\.*(prd|stg|dev)*.stackspot.com)|localhost/)
|
|
4
|
+
let cookieDomain = ''
|
|
5
|
+
let cookieAttributes: Record<string, string> = {}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Setup the cookie utilities to use a another domain. By default, it's based on *.stackspot.com.
|
|
9
|
+
* @param domainRegex the regex to identify the domain. Example: {@link DEFAULT_DOMAIN_REGEX}.
|
|
10
|
+
*/
|
|
11
|
+
export function setupCookies(domainRegex: RegExp) {
|
|
12
|
+
const portalUrl = new URL(location.href)
|
|
13
|
+
cookieDomain = domainRegex.exec(portalUrl.host)?.[0] ?? ''
|
|
14
|
+
cookieAttributes = { domain: cookieDomain, SameSite: 'Strict' }
|
|
15
|
+
// Add Secure attribute if using HTTPS
|
|
16
|
+
if (portalUrl.protocol === 'https:') {
|
|
17
|
+
cookieAttributes['Secure'] = ''
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Retrieves the current domain used for cookies. To change the domain, use `setupCookies(domainRegex)`.
|
|
23
|
+
* @returns the current cookie domain.
|
|
24
|
+
*/
|
|
25
|
+
export function getCookieDomain() {
|
|
26
|
+
if (isEmpty(cookieAttributes)) setupCookies(DEFAULT_DOMAIN_REGEX)
|
|
27
|
+
return cookieDomain
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get all cookies as an object where the keys are the cookie names and the values are the respective cookie values.
|
|
32
|
+
* @returns a map of cookie name to cookie value.
|
|
33
|
+
*/
|
|
34
|
+
export function getCookies(): Record<string, string | undefined> {
|
|
35
|
+
if (isEmpty(cookieAttributes)) setupCookies(DEFAULT_DOMAIN_REGEX)
|
|
36
|
+
return document.cookie.split('; ').reduce((result, current) => {
|
|
37
|
+
const [name, ...value] = current.split('=')
|
|
38
|
+
result[name] = value.join('=')
|
|
39
|
+
return result
|
|
40
|
+
}, {} as Record<string, string>)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Sets the value of a cookie.
|
|
45
|
+
* @param key the cookie name (identifier).
|
|
46
|
+
* @param value the cookie value.
|
|
47
|
+
* @param customAttributes Accepted values: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#attributes.
|
|
48
|
+
*/
|
|
49
|
+
export function setCookie(key: string, value: string, customAttributes: Record<string, string> = {}) {
|
|
50
|
+
if (isEmpty(cookieAttributes)) setupCookies(DEFAULT_DOMAIN_REGEX)
|
|
51
|
+
document.cookie = `${key}=${value};${objectToCookieString({ ...cookieAttributes, ...customAttributes })}`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Removes a cookie.
|
|
56
|
+
* @param key the cookie name (identifier).
|
|
57
|
+
*/
|
|
58
|
+
export function removeCookie(key: string) {
|
|
59
|
+
if (isEmpty(cookieAttributes)) setupCookies(DEFAULT_DOMAIN_REGEX)
|
|
60
|
+
document.cookie = `${key}=;${objectToCookieString({ ...cookieAttributes, expires: 'Thu, 01 Jan 1970 00:00:00 GMT' })}`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Retrieves the value of a cookie given its key/name.
|
|
65
|
+
* @param key the cookie name (identifier).
|
|
66
|
+
* @returns the cookie value or undefined, if the cookie doesn't exist.
|
|
67
|
+
*/
|
|
68
|
+
export function getCookie(key: string) {
|
|
69
|
+
return getCookies()[key]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const objectToCookieString = (object: Record<string, string>): string =>
|
|
73
|
+
Object.entries(object).reduce((prev, [k, v]) => `${prev} ${k}${v !== '' ? `=${v}` : ''};`, '').trim()
|
package/src/utils/promise.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export function delay(ms: number) {
|
|
2
|
-
return new Promise((resolve) => {
|
|
3
|
-
setTimeout(resolve, ms)
|
|
4
|
-
})
|
|
5
|
-
}
|
|
1
|
+
export function delay(ms: number) {
|
|
2
|
+
return new Promise((resolve) => {
|
|
3
|
+
setTimeout(resolve, ms)
|
|
4
|
+
})
|
|
5
|
+
}
|
package/src/utils/read-file.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
export async function readFile(file: File, isJson = false): Promise<string | undefined> {
|
|
2
|
-
return new Promise((resolve, reject) => {
|
|
3
|
-
const reader = new FileReader()
|
|
4
|
-
|
|
5
|
-
reader.onload = (e) => {
|
|
6
|
-
const content = e.target?.result as string
|
|
7
|
-
resolve(isJson ? JSON.parse(content) : content)
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
reader.onerror = (e) => {
|
|
11
|
-
reject(e)
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
reader.readAsText(file)
|
|
15
|
-
})
|
|
16
|
-
}
|
|
1
|
+
export async function readFile(file: File, isJson = false): Promise<string | undefined> {
|
|
2
|
+
return new Promise((resolve, reject) => {
|
|
3
|
+
const reader = new FileReader()
|
|
4
|
+
|
|
5
|
+
reader.onload = (e) => {
|
|
6
|
+
const content = e.target?.result as string
|
|
7
|
+
resolve(isJson ? JSON.parse(content) : content)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
reader.onerror = (e) => {
|
|
11
|
+
reject(e)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
reader.readAsText(file)
|
|
15
|
+
})
|
|
16
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "../../tsconfig",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"rootDir": "src",
|
|
5
|
-
"module": "ESNext",
|
|
6
|
-
"outDir": "dist"
|
|
7
|
-
},
|
|
8
|
-
"include": [
|
|
9
|
-
"src",
|
|
10
|
-
]
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "src",
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"outDir": "dist"
|
|
7
|
+
},
|
|
8
|
+
"include": [
|
|
9
|
+
"src",
|
|
10
|
+
]
|
|
11
11
|
}
|