@coveo/quantic 3.38.2 → 3.39.1
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/force-app/main/default/labels/CustomLabels.labels-meta.xml +14 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerFollowUpInput/__tests__/quanticGeneratedAnswerFollowUpInput.test.js +180 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerFollowUpInput/quanticGeneratedAnswerFollowUpInput.css +17 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerFollowUpInput/quanticGeneratedAnswerFollowUpInput.html +29 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerFollowUpInput/quanticGeneratedAnswerFollowUpInput.js +79 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerFollowUpInput/quanticGeneratedAnswerFollowUpInput.js-meta.xml +5 -0
- package/force-app/main/default/lwc/quanticUtils/__tests__/accessibilityUtils.test.js +214 -0
- package/force-app/main/default/lwc/quanticUtils/__tests__/facetStoreUtils.test.js +86 -0
- package/force-app/main/default/lwc/quanticUtils/accessibilityUtils.js +225 -0
- package/force-app/main/default/lwc/quanticUtils/facetStoreUtils.js +65 -0
- package/force-app/main/default/lwc/quanticUtils/quanticUtils.js +2 -291
- package/force-app/main/default/staticresources/coveoheadless/case-assist/headless.js +4 -4
- package/force-app/main/default/staticresources/coveoheadless/headless.js +5 -5
- package/force-app/main/default/staticresources/coveoheadless/insight/headless.js +4 -4
- package/force-app/main/default/staticresources/coveoheadless/recommendation/headless.js +14 -14
- package/package.json +4 -4
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AriaLiveUtils
|
|
3
|
+
* @typedef {Object} AriaLiveUtils
|
|
4
|
+
* @property {Function} dispatchMessage
|
|
5
|
+
* @property {Function} registerRegion
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* AriaLiveRegion Create an AriaLiveRegion to be able to send events to dispatch messages for assistive technologies.
|
|
10
|
+
* @param {string} regionName
|
|
11
|
+
* @param {Object} elem
|
|
12
|
+
* @param {boolean} assertive
|
|
13
|
+
* @returns {AriaLiveUtils} Object with methods to dispatch messages and register the region.
|
|
14
|
+
*/
|
|
15
|
+
export function AriaLiveRegion(regionName, elem, assertive = false) {
|
|
16
|
+
function dispatchMessage(message) {
|
|
17
|
+
const ariaLiveMessageEvent = new CustomEvent('quantic__arialivemessage', {
|
|
18
|
+
bubbles: true,
|
|
19
|
+
composed: true,
|
|
20
|
+
detail: {
|
|
21
|
+
regionName,
|
|
22
|
+
assertive,
|
|
23
|
+
message,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
elem.dispatchEvent(ariaLiveMessageEvent);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function registerRegion() {
|
|
30
|
+
const registerRegionEvent = new CustomEvent('quantic__registerregion', {
|
|
31
|
+
bubbles: true,
|
|
32
|
+
composed: true,
|
|
33
|
+
detail: {
|
|
34
|
+
regionName,
|
|
35
|
+
assertive,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
elem.dispatchEvent(registerRegionEvent);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
registerRegion();
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
dispatchMessage,
|
|
45
|
+
registerRegion,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Checks whether an element is focusable.
|
|
51
|
+
* @param {HTMLElement | Element} element
|
|
52
|
+
* @returns {boolean}
|
|
53
|
+
*/
|
|
54
|
+
export function isFocusable(element) {
|
|
55
|
+
// Source: https://stackoverflow.com/a/30753870
|
|
56
|
+
if (element.getAttribute('tabindex') === '-1') {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
if (
|
|
60
|
+
element.hasAttribute('tabindex') ||
|
|
61
|
+
element.getAttribute('contentEditable') === 'true'
|
|
62
|
+
) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
switch (element.tagName) {
|
|
66
|
+
case 'A':
|
|
67
|
+
case 'AREA':
|
|
68
|
+
return element.hasAttribute('href');
|
|
69
|
+
case 'INPUT':
|
|
70
|
+
case 'SELECT':
|
|
71
|
+
case 'TEXTAREA':
|
|
72
|
+
case 'BUTTON':
|
|
73
|
+
return !element.hasAttribute('disabled');
|
|
74
|
+
case 'IFRAME':
|
|
75
|
+
return true;
|
|
76
|
+
default:
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Returns the last focusable element of for an HTML element.
|
|
83
|
+
* This function would NOT work with shadow root.
|
|
84
|
+
* @param {HTMLElement & {assignedElements?: () => Array<HTMLElement>} | null} element
|
|
85
|
+
* @returns {HTMLElement | null}
|
|
86
|
+
*/
|
|
87
|
+
export function getLastFocusableElement(element) {
|
|
88
|
+
if (!element || element.nodeType === Node.TEXT_NODE) return null;
|
|
89
|
+
|
|
90
|
+
if (isCustomElement(element)) {
|
|
91
|
+
if (element.dataset?.focusable?.toString() === 'true') {
|
|
92
|
+
return element;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (element.tagName === 'SLOT' && element.assignedElements().length) {
|
|
98
|
+
return getLastFocusableElementFromSlot(element);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** @type {Array} */
|
|
102
|
+
const childNodes = Array.from(element.childNodes);
|
|
103
|
+
const focusableElements = childNodes
|
|
104
|
+
.map((item) => getLastFocusableElement(item))
|
|
105
|
+
.filter((item) => !!item);
|
|
106
|
+
|
|
107
|
+
if (focusableElements.length) {
|
|
108
|
+
return focusableElements[focusableElements.length - 1];
|
|
109
|
+
} else if (isFocusable(element)) {
|
|
110
|
+
return element;
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Returns the First focusable element of for an HTML element.
|
|
117
|
+
* This function would NOT work with shadow root.
|
|
118
|
+
* @param {HTMLElement & {assignedElements?: () => Array<HTMLElement>} | null} element
|
|
119
|
+
* @returns {HTMLElement | null}
|
|
120
|
+
*/
|
|
121
|
+
export function getFirstFocusableElement(element) {
|
|
122
|
+
if (!element || element.nodeType === Node.TEXT_NODE) return null;
|
|
123
|
+
|
|
124
|
+
if (isCustomElement(element)) {
|
|
125
|
+
if (element.dataset?.focusable?.toString() === 'true') {
|
|
126
|
+
return element;
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (element.tagName === 'SLOT' && element.assignedElements().length) {
|
|
132
|
+
return getFirstFocusableElementFromSlot(element);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** @type {Array} */
|
|
136
|
+
const childNodes = Array.from(element.childNodes);
|
|
137
|
+
const focusableElements = childNodes
|
|
138
|
+
.map((item) => getFirstFocusableElement(item))
|
|
139
|
+
.filter((item) => !!item);
|
|
140
|
+
|
|
141
|
+
if (focusableElements.length) {
|
|
142
|
+
return focusableElements[0];
|
|
143
|
+
} else if (isFocusable(element)) {
|
|
144
|
+
return element;
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Checks whether an element is a custom element.
|
|
151
|
+
* @param {HTMLElement | null} element
|
|
152
|
+
* @returns {boolean}
|
|
153
|
+
*/
|
|
154
|
+
export function isCustomElement(element) {
|
|
155
|
+
if (element && element.tagName.includes('-')) {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Returns the last focusable element in an HTML slot.
|
|
163
|
+
* @param {HTMLElement & {assignedElements?: () => Array<HTMLElement> | null}} slotElement
|
|
164
|
+
* @returns {HTMLElement | null}
|
|
165
|
+
*/
|
|
166
|
+
function getLastFocusableElementFromSlot(slotElement) {
|
|
167
|
+
if (!slotElement && slotElement.assignedElements) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
const assignedElements = Array.from(slotElement.assignedElements());
|
|
171
|
+
const focusableElements = assignedElements
|
|
172
|
+
.map((item) => getLastFocusableElement(item))
|
|
173
|
+
.filter((item) => !!item);
|
|
174
|
+
|
|
175
|
+
if (focusableElements.length) {
|
|
176
|
+
return focusableElements[focusableElements.length - 1];
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Returns the first focusable element in an HTML slot.
|
|
183
|
+
* @param {HTMLElement & {assignedElements?: () => Array<HTMLElement> | null}} slotElement
|
|
184
|
+
* @return {HTMLElement | null}
|
|
185
|
+
*/
|
|
186
|
+
function getFirstFocusableElementFromSlot(slotElement) {
|
|
187
|
+
if (!slotElement && slotElement.assignedElements) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
const assignedElements = Array.from(slotElement.assignedElements());
|
|
191
|
+
const focusableElements = assignedElements
|
|
192
|
+
.map((item) => getFirstFocusableElement(item))
|
|
193
|
+
.filter((item) => !!item);
|
|
194
|
+
|
|
195
|
+
if (focusableElements.length) {
|
|
196
|
+
return focusableElements[0];
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Checks whether an element is indeed the targetElement or one of its parents.
|
|
203
|
+
* @param {HTMLElement} element
|
|
204
|
+
* @param {string} targetElement
|
|
205
|
+
* @returns {boolean}
|
|
206
|
+
*/
|
|
207
|
+
export function isParentOf(element, targetElement) {
|
|
208
|
+
if (!element || element.nodeType === Node.TEXT_NODE) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (isCustomElement(element)) {
|
|
213
|
+
if (element.tagName === targetElement) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
/** @type {Array} */
|
|
219
|
+
const childNodes = Array.from(element.childNodes);
|
|
220
|
+
if (childNodes.length === 0) return false;
|
|
221
|
+
return childNodes.reduce(
|
|
222
|
+
(acc, val) => acc || isParentOf(val, targetElement),
|
|
223
|
+
false
|
|
224
|
+
);
|
|
225
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** @typedef {import("coveo").SortCriterion} SortCriterion */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Utility class for managing a simple in-memory store.
|
|
5
|
+
* Supports registering and retrieving facet and sort option data.
|
|
6
|
+
*/
|
|
7
|
+
export class Store {
|
|
8
|
+
static facetTypes = {
|
|
9
|
+
FACETS: 'facets',
|
|
10
|
+
NUMERICFACETS: 'numericFacets',
|
|
11
|
+
DATEFACETS: 'dateFacets',
|
|
12
|
+
CATEGORYFACETS: 'categoryFacets',
|
|
13
|
+
};
|
|
14
|
+
static initialize() {
|
|
15
|
+
return {
|
|
16
|
+
state: {
|
|
17
|
+
facets: {},
|
|
18
|
+
numericFacets: {},
|
|
19
|
+
dateFacets: {},
|
|
20
|
+
categoryFacets: {},
|
|
21
|
+
sort: {},
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Registers a facet to the store if it does not already exist.
|
|
27
|
+
* @param {Record<String, unknown>} store
|
|
28
|
+
* @param {string} facetType
|
|
29
|
+
* @param {{ label?: string; facetId: any; format?: Function;}} data
|
|
30
|
+
*/
|
|
31
|
+
static registerFacetToStore(store, facetType, data) {
|
|
32
|
+
if (store?.state[facetType][data.facetId]) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
store.state[facetType][data.facetId] = data;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Registers sort option data to the store.
|
|
40
|
+
* @param {Record<String, any>} store
|
|
41
|
+
* @param {Array<{label: string; value: string; criterion: SortCriterion;}>} data
|
|
42
|
+
*/
|
|
43
|
+
static registerSortOptionDataToStore(store, data) {
|
|
44
|
+
store.state.sort = data;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Gets facet data from the store.
|
|
49
|
+
* @param {Record<String, unknown>} store
|
|
50
|
+
* @param {string} facetType
|
|
51
|
+
* @return {Object} The facet data.
|
|
52
|
+
*/
|
|
53
|
+
static getFromStore(store, facetType) {
|
|
54
|
+
return store.state[facetType];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Gets sort options from the store.
|
|
59
|
+
* @param {Record<String, Object>} store
|
|
60
|
+
* @return {Array} The sort options.
|
|
61
|
+
*/
|
|
62
|
+
static getSortOptionsFromStore(store) {
|
|
63
|
+
return store.state.sort;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import LOCALE from '@salesforce/i18n/locale';
|
|
2
2
|
|
|
3
3
|
/** @typedef {import("coveo").Result} Result */
|
|
4
|
-
/** @typedef {import("coveo").SortCriterion} SortCriterion */
|
|
5
4
|
|
|
6
5
|
export * from './recentQueriesUtils';
|
|
7
6
|
export * from './markdownUtils';
|
|
8
7
|
export * from './facetDependenciesUtils';
|
|
9
8
|
export * from './citationAnchoringUtils';
|
|
10
9
|
export * from './timeAndDateUtils';
|
|
10
|
+
export * from './accessibilityUtils';
|
|
11
|
+
export * from './facetStoreUtils';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Utility class for debouncing function calls.
|
|
@@ -283,296 +284,6 @@ export function unwrapLockerProxiedObject(value) {
|
|
|
283
284
|
return unwrappedValue;
|
|
284
285
|
}
|
|
285
286
|
|
|
286
|
-
/**
|
|
287
|
-
* Utility class for managing a simple in-memory store.
|
|
288
|
-
* Supports registering and retrieving facet and sort option data.
|
|
289
|
-
*/
|
|
290
|
-
export class Store {
|
|
291
|
-
static facetTypes = {
|
|
292
|
-
FACETS: 'facets',
|
|
293
|
-
NUMERICFACETS: 'numericFacets',
|
|
294
|
-
DATEFACETS: 'dateFacets',
|
|
295
|
-
CATEGORYFACETS: 'categoryFacets',
|
|
296
|
-
};
|
|
297
|
-
static initialize() {
|
|
298
|
-
return {
|
|
299
|
-
state: {
|
|
300
|
-
facets: {},
|
|
301
|
-
numericFacets: {},
|
|
302
|
-
dateFacets: {},
|
|
303
|
-
categoryFacets: {},
|
|
304
|
-
sort: {},
|
|
305
|
-
},
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
/**
|
|
309
|
-
* Registers a facet to the store if it does not already exist.
|
|
310
|
-
* @param {Record<String, unknown>} store
|
|
311
|
-
* @param {string} facetType
|
|
312
|
-
* @param {{ label?: string; facetId: any; format?: Function;}} data
|
|
313
|
-
*/
|
|
314
|
-
static registerFacetToStore(store, facetType, data) {
|
|
315
|
-
if (store?.state[facetType][data.facetId]) {
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
store.state[facetType][data.facetId] = data;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* Registers sort option data to the store.
|
|
323
|
-
* @param {Record<String, any>} store
|
|
324
|
-
* @param {Array<{label: string; value: string; criterion: SortCriterion;}>} data
|
|
325
|
-
*/
|
|
326
|
-
static registerSortOptionDataToStore(store, data) {
|
|
327
|
-
store.state.sort = data;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* Gets facet data from the store.
|
|
332
|
-
* @param {Record<String, unknown>} store
|
|
333
|
-
* @param {string} facetType
|
|
334
|
-
* @return {Object} The facet data.
|
|
335
|
-
*/
|
|
336
|
-
static getFromStore(store, facetType) {
|
|
337
|
-
return store.state[facetType];
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* Gets sort options from the store.
|
|
342
|
-
* @param {Record<String, Object>} store
|
|
343
|
-
* @return {Array} The sort options.
|
|
344
|
-
*/
|
|
345
|
-
static getSortOptionsFromStore(store) {
|
|
346
|
-
return store.state.sort;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* AriaLiveUtils
|
|
352
|
-
* @typedef {Object} AriaLiveUtils
|
|
353
|
-
* @property {Function} dispatchMessage
|
|
354
|
-
* @property {Function} registerRegion
|
|
355
|
-
*/
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* AriaLiveRegion Create an AriaLiveRegion to be able to send events to dispatch messages for assistive technologies.
|
|
359
|
-
* @param {string} regionName
|
|
360
|
-
* @param {Object} elem
|
|
361
|
-
* @param {boolean} assertive
|
|
362
|
-
* @returns {AriaLiveUtils} Object with methods to dispatch messages and register the region.
|
|
363
|
-
*/
|
|
364
|
-
export function AriaLiveRegion(regionName, elem, assertive = false) {
|
|
365
|
-
function dispatchMessage(message) {
|
|
366
|
-
const ariaLiveMessageEvent = new CustomEvent('quantic__arialivemessage', {
|
|
367
|
-
bubbles: true,
|
|
368
|
-
composed: true,
|
|
369
|
-
detail: {
|
|
370
|
-
regionName,
|
|
371
|
-
assertive,
|
|
372
|
-
message,
|
|
373
|
-
},
|
|
374
|
-
});
|
|
375
|
-
elem.dispatchEvent(ariaLiveMessageEvent);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function registerRegion() {
|
|
379
|
-
const registerRegionEvent = new CustomEvent('quantic__registerregion', {
|
|
380
|
-
bubbles: true,
|
|
381
|
-
composed: true,
|
|
382
|
-
detail: {
|
|
383
|
-
regionName,
|
|
384
|
-
assertive,
|
|
385
|
-
},
|
|
386
|
-
});
|
|
387
|
-
elem.dispatchEvent(registerRegionEvent);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
registerRegion();
|
|
391
|
-
|
|
392
|
-
return {
|
|
393
|
-
dispatchMessage,
|
|
394
|
-
registerRegion,
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
/**
|
|
399
|
-
* Checks whether an element is focusable.
|
|
400
|
-
* @param {HTMLElement | Element} element
|
|
401
|
-
* @returns {boolean}
|
|
402
|
-
*/
|
|
403
|
-
export function isFocusable(element) {
|
|
404
|
-
// Source: https://stackoverflow.com/a/30753870
|
|
405
|
-
if (element.getAttribute('tabindex') === '-1') {
|
|
406
|
-
return false;
|
|
407
|
-
}
|
|
408
|
-
if (
|
|
409
|
-
element.hasAttribute('tabindex') ||
|
|
410
|
-
element.getAttribute('contentEditable') === 'true'
|
|
411
|
-
) {
|
|
412
|
-
return true;
|
|
413
|
-
}
|
|
414
|
-
switch (element.tagName) {
|
|
415
|
-
case 'A':
|
|
416
|
-
case 'AREA':
|
|
417
|
-
return element.hasAttribute('href');
|
|
418
|
-
case 'INPUT':
|
|
419
|
-
case 'SELECT':
|
|
420
|
-
case 'TEXTAREA':
|
|
421
|
-
case 'BUTTON':
|
|
422
|
-
return !element.hasAttribute('disabled');
|
|
423
|
-
case 'IFRAME':
|
|
424
|
-
return true;
|
|
425
|
-
default:
|
|
426
|
-
return false;
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
/**
|
|
431
|
-
* Returns the last focusable element of for an HTML element.
|
|
432
|
-
* This function would NOT work with shadow root.
|
|
433
|
-
* @param {HTMLElement & {assignedElements?: () => Array<HTMLElement>} | null} element
|
|
434
|
-
* @returns {HTMLElement | null}
|
|
435
|
-
*/
|
|
436
|
-
export function getLastFocusableElement(element) {
|
|
437
|
-
if (!element || element.nodeType === Node.TEXT_NODE) return null;
|
|
438
|
-
|
|
439
|
-
if (isCustomElement(element)) {
|
|
440
|
-
if (element.dataset?.focusable?.toString() === 'true') {
|
|
441
|
-
return element;
|
|
442
|
-
}
|
|
443
|
-
return null;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
if (element.tagName === 'SLOT' && element.assignedElements().length) {
|
|
447
|
-
return getLastFocusableElementFromSlot(element);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/** @type {Array} */
|
|
451
|
-
const childNodes = Array.from(element.childNodes);
|
|
452
|
-
const focusableElements = childNodes
|
|
453
|
-
.map((item) => getLastFocusableElement(item))
|
|
454
|
-
.filter((item) => !!item);
|
|
455
|
-
|
|
456
|
-
if (focusableElements.length) {
|
|
457
|
-
return focusableElements[focusableElements.length - 1];
|
|
458
|
-
} else if (isFocusable(element)) {
|
|
459
|
-
return element;
|
|
460
|
-
}
|
|
461
|
-
return null;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* Returns the First focusable element of for an HTML element.
|
|
466
|
-
* This function would NOT work with shadow root.
|
|
467
|
-
* @param {HTMLElement & {assignedElements?: () => Array<HTMLElement>} | null} element
|
|
468
|
-
* @returns {HTMLElement | null}
|
|
469
|
-
*/
|
|
470
|
-
export function getFirstFocusableElement(element) {
|
|
471
|
-
if (!element || element.nodeType === Node.TEXT_NODE) return null;
|
|
472
|
-
|
|
473
|
-
if (isCustomElement(element)) {
|
|
474
|
-
if (element.dataset?.focusable?.toString() === 'true') {
|
|
475
|
-
return element;
|
|
476
|
-
}
|
|
477
|
-
return null;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
if (element.tagName === 'SLOT' && element.assignedElements().length) {
|
|
481
|
-
return getFirstFocusableElementFromSlot(element);
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
/** @type {Array} */
|
|
485
|
-
const childNodes = Array.from(element.childNodes);
|
|
486
|
-
const focusableElements = childNodes
|
|
487
|
-
.map((item) => getFirstFocusableElement(item))
|
|
488
|
-
.filter((item) => !!item);
|
|
489
|
-
|
|
490
|
-
if (focusableElements.length) {
|
|
491
|
-
return focusableElements[0];
|
|
492
|
-
} else if (isFocusable(element)) {
|
|
493
|
-
return element;
|
|
494
|
-
}
|
|
495
|
-
return null;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
/**
|
|
499
|
-
* Checks whether an element is a custom element.
|
|
500
|
-
* @param {HTMLElement | null} element
|
|
501
|
-
* @returns {boolean}
|
|
502
|
-
*/
|
|
503
|
-
export function isCustomElement(element) {
|
|
504
|
-
if (element && element.tagName.includes('-')) {
|
|
505
|
-
return true;
|
|
506
|
-
}
|
|
507
|
-
return false;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
/**
|
|
511
|
-
* Returns the last focusable element in an HTML slot.
|
|
512
|
-
* @param {HTMLElement & {assignedElements?: () => Array<HTMLElement> | null}} slotElement
|
|
513
|
-
* @returns {HTMLElement | null}
|
|
514
|
-
*/
|
|
515
|
-
function getLastFocusableElementFromSlot(slotElement) {
|
|
516
|
-
if (!slotElement && slotElement.assignedElements) {
|
|
517
|
-
return null;
|
|
518
|
-
}
|
|
519
|
-
const assignedElements = Array.from(slotElement.assignedElements());
|
|
520
|
-
const focusableElements = assignedElements
|
|
521
|
-
.map((item) => getLastFocusableElement(item))
|
|
522
|
-
.filter((item) => !!item);
|
|
523
|
-
|
|
524
|
-
if (focusableElements.length) {
|
|
525
|
-
return focusableElements[focusableElements.length - 1];
|
|
526
|
-
}
|
|
527
|
-
return null;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
/**
|
|
531
|
-
* Returns the first focusable element in an HTML slot.
|
|
532
|
-
* @param {HTMLElement & {assignedElements?: () => Array<HTMLElement> | null}} slotElement
|
|
533
|
-
* @return {HTMLElement | null}
|
|
534
|
-
*/
|
|
535
|
-
function getFirstFocusableElementFromSlot(slotElement) {
|
|
536
|
-
if (!slotElement && slotElement.assignedElements) {
|
|
537
|
-
return null;
|
|
538
|
-
}
|
|
539
|
-
const assignedElements = Array.from(slotElement.assignedElements());
|
|
540
|
-
const focusableElements = assignedElements
|
|
541
|
-
.map((item) => getFirstFocusableElement(item))
|
|
542
|
-
.filter((item) => !!item);
|
|
543
|
-
|
|
544
|
-
if (focusableElements.length) {
|
|
545
|
-
return focusableElements[0];
|
|
546
|
-
}
|
|
547
|
-
return null;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
/**
|
|
551
|
-
* Checks whether an element is indeed the targetElement or one of its parents.
|
|
552
|
-
* @param {HTMLElement} element
|
|
553
|
-
* @param {string} targetElement
|
|
554
|
-
* @returns {boolean}
|
|
555
|
-
*/
|
|
556
|
-
export function isParentOf(element, targetElement) {
|
|
557
|
-
if (!element || element.nodeType === Node.TEXT_NODE) {
|
|
558
|
-
return false;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
if (isCustomElement(element)) {
|
|
562
|
-
if (element.tagName === targetElement) {
|
|
563
|
-
return true;
|
|
564
|
-
}
|
|
565
|
-
return false;
|
|
566
|
-
}
|
|
567
|
-
/** @type {Array} */
|
|
568
|
-
const childNodes = Array.from(element.childNodes);
|
|
569
|
-
if (childNodes.length === 0) return false;
|
|
570
|
-
return childNodes.reduce(
|
|
571
|
-
(acc, val) => acc || isParentOf(val, targetElement),
|
|
572
|
-
false
|
|
573
|
-
);
|
|
574
|
-
}
|
|
575
|
-
|
|
576
287
|
/**
|
|
577
288
|
* Copies text to clipboard using the Clipboard API.
|
|
578
289
|
* https://developer.mozilla.org/en-US/docs/Web/API/Clipboard
|