@brightspace-ui/core 3.29.0 → 3.30.1
Sign up to get free protection for your applications and to get access to all the features.
- package/components/list/list-item-generic-layout.js +48 -120
- package/helpers/README.md +3 -0
- package/helpers/focus.js +19 -0
- package/package.json +1 -1
@@ -1,6 +1,6 @@
|
|
1
1
|
import { css, html, LitElement } from 'lit';
|
2
|
-
import { findComposedAncestor,
|
3
|
-
import { getComposedActiveElement, getFirstFocusableDescendant, getLastFocusableDescendant, getNextFocusable, getPreviousFocusable
|
2
|
+
import { findComposedAncestor, isComposedAncestor } from '../../helpers/dom.js';
|
3
|
+
import { getComposedActiveElement, getFirstFocusableDescendant, getFocusableDescendants, getLastFocusableDescendant, getNextFocusable, getPreviousFocusable } from '../../helpers/focus.js';
|
4
4
|
import { isInteractiveDescendant } from '../../mixins/interactive/interactive-mixin.js';
|
5
5
|
import { RtlMixin } from '../../mixins/rtl/rtl-mixin.js';
|
6
6
|
|
@@ -220,7 +220,6 @@ class ListItemGenericLayout extends RtlMixin(LitElement) {
|
|
220
220
|
|
221
221
|
firstUpdated() {
|
222
222
|
this.addEventListener('keydown', this._onKeydown.bind(this));
|
223
|
-
this.addEventListener('focusin', this._setFocusInfo.bind(this));
|
224
223
|
}
|
225
224
|
|
226
225
|
render() {
|
@@ -247,19 +246,18 @@ class ListItemGenericLayout extends RtlMixin(LitElement) {
|
|
247
246
|
`;
|
248
247
|
}
|
249
248
|
|
250
|
-
_focusCellItem(
|
251
|
-
const cell = this.shadowRoot
|
249
|
+
_focusCellItem(focusInfo) {
|
250
|
+
const cell = this.shadowRoot?.querySelector(`[data-cell-num="${focusInfo.cellNum}"]`);
|
252
251
|
if (!cell) return;
|
253
252
|
|
254
|
-
|
255
|
-
|
253
|
+
let focusable;
|
254
|
+
const focusables = getFocusableDescendants(cell, { deep: true, predicate: elem => !isInteractiveDescendant(elem) });
|
256
255
|
|
257
|
-
if (
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
}
|
256
|
+
if (focusInfo.index <= focusables.length - 1) focusable = focusables[focusInfo.index];
|
257
|
+
else if (focusables.length > 0) focusable = focusables[focusables.length - 1];
|
258
|
+
|
259
|
+
if (focusable) focusable.focus();
|
260
|
+
return focusable;
|
263
261
|
}
|
264
262
|
|
265
263
|
_focusFirstCell() {
|
@@ -321,7 +319,7 @@ class ListItemGenericLayout extends RtlMixin(LitElement) {
|
|
321
319
|
return focusable;
|
322
320
|
}
|
323
321
|
|
324
|
-
_focusNextRow(previous = false, num = 1) {
|
322
|
+
_focusNextRow(focusInfo, previous = false, num = 1) {
|
325
323
|
|
326
324
|
const curListItem = findComposedAncestor(this, node => node.role === 'rowgroup');
|
327
325
|
let listItem = curListItem;
|
@@ -335,7 +333,7 @@ class ListItemGenericLayout extends RtlMixin(LitElement) {
|
|
335
333
|
|
336
334
|
if (!listItem) return;
|
337
335
|
const listItemRow = listItem.shadowRoot.querySelector('[role="gridrow"]');
|
338
|
-
const focusedCellItem = listItemRow._focusCellItem(
|
336
|
+
const focusedCellItem = listItemRow._focusCellItem(focusInfo);
|
339
337
|
|
340
338
|
if (!focusedCellItem) {
|
341
339
|
// could not focus on same cell in adjacent list-item so try general focus on item
|
@@ -351,51 +349,14 @@ class ListItemGenericLayout extends RtlMixin(LitElement) {
|
|
351
349
|
|
352
350
|
}
|
353
351
|
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
let focusable = null;
|
358
|
-
let siblingNum = 1;
|
359
|
-
while (!focusable || siblingNum < num) {
|
360
|
-
node = this._getNextSiblingInCell(node);
|
361
|
-
|
362
|
-
if (!node) break;
|
363
|
-
++siblingNum;
|
364
|
-
|
365
|
-
focusable = isFocusable(node, true) ? node : getFirstFocusableDescendant(node);
|
366
|
-
if (isInteractiveDescendant(focusable)) focusable = null;
|
367
|
-
}
|
368
|
-
|
369
|
-
if (focusable) focusable.focus();
|
370
|
-
return focusable;
|
352
|
+
_focusNextWithinRow(focusInfo, focusables) {
|
353
|
+
if (focusInfo.index === focusables.length - 1) this._focusNextCell(focusInfo.cellNum + 1);
|
354
|
+
else focusables[focusInfo.index + 1].focus();
|
371
355
|
}
|
372
356
|
|
373
|
-
|
374
|
-
if (
|
375
|
-
|
376
|
-
while (!focusable) {
|
377
|
-
node = this._getPrevSiblingInCell(node);
|
378
|
-
if (!node) break;
|
379
|
-
focusable = isFocusable(node, true) ? node : getLastFocusableDescendant(node);
|
380
|
-
}
|
381
|
-
if (focusable) focusable.focus();
|
382
|
-
return focusable;
|
383
|
-
}
|
384
|
-
|
385
|
-
_getFocusedItemPosition(node) {
|
386
|
-
let position = 1;
|
387
|
-
// walk the tree backwards until we hit the cell
|
388
|
-
do {
|
389
|
-
node = this._getPrevSiblingInCell(node);
|
390
|
-
if (node) {
|
391
|
-
const focusable = isFocusable(node, true) ? node : getLastFocusableDescendant(node);
|
392
|
-
if (focusable) {
|
393
|
-
++position;
|
394
|
-
node = focusable;
|
395
|
-
}
|
396
|
-
}
|
397
|
-
} while (node);
|
398
|
-
return position;
|
357
|
+
_focusPreviousWithinRow(focusInfo, focusables) {
|
358
|
+
if (focusInfo.index === 0) this._focusNextCell(focusInfo.cellNum - 1, false);
|
359
|
+
else focusables[focusInfo.index - 1].focus();
|
399
360
|
}
|
400
361
|
|
401
362
|
_getNextFlattenedListItem(listItem) {
|
@@ -430,15 +391,6 @@ class ListItemGenericLayout extends RtlMixin(LitElement) {
|
|
430
391
|
|
431
392
|
}
|
432
393
|
|
433
|
-
_getNextSiblingInCell(node) {
|
434
|
-
const cell = findComposedAncestor(node, (parent) => parent.classList && parent.classList.contains('d2l-cell'));
|
435
|
-
if (!cell || cell.name === node.slot) return null;
|
436
|
-
if (node.nextElementSibling) return node.nextElementSibling;
|
437
|
-
|
438
|
-
const sibling = getNextAncestorSibling(node);
|
439
|
-
return isComposedAncestor(cell, sibling) ? sibling : null;
|
440
|
-
}
|
441
|
-
|
442
394
|
_getPreviousFlattenedListItem(listItem) {
|
443
395
|
|
444
396
|
let previousElement = listItem.previousElementSibling;
|
@@ -476,72 +428,58 @@ class ListItemGenericLayout extends RtlMixin(LitElement) {
|
|
476
428
|
|
477
429
|
}
|
478
430
|
|
479
|
-
_getPrevSiblingInCell(node) {
|
480
|
-
const cell = findComposedAncestor(node, (parent) => parent.classList && parent.classList.contains('d2l-cell'));
|
481
|
-
if (!cell || cell.name === node.slot) return null;
|
482
|
-
if (node.previousElementSibling) return node.previousElementSibling;
|
483
|
-
|
484
|
-
const sibling = getPreviousAncestorSibling(node);
|
485
|
-
return isComposedAncestor(cell, sibling) ? sibling : null;
|
486
|
-
}
|
487
|
-
|
488
|
-
_getThisCell() {
|
489
|
-
return this.shadowRoot &&
|
490
|
-
this.shadowRoot.querySelector(`.d2l-cell[data-cell-num="${this._cellNum}"]`);
|
491
|
-
}
|
492
|
-
|
493
431
|
_isContainedInSameRootList(item, node) {
|
494
432
|
const rootList = item?.getRootList?.(item);
|
495
433
|
return isComposedAncestor(rootList, node);
|
496
434
|
}
|
497
435
|
|
498
|
-
_onKeydown(
|
436
|
+
_onKeydown(e) {
|
499
437
|
if (!this.gridActive) return;
|
500
|
-
let node = null;
|
501
438
|
let preventDefault = true;
|
502
|
-
|
439
|
+
|
440
|
+
const node = getComposedActiveElement();
|
441
|
+
const cell = findComposedAncestor(node, parent => parent.classList?.contains('d2l-cell'));
|
442
|
+
if (!cell) return;
|
443
|
+
|
444
|
+
const focusables = getFocusableDescendants(cell, { deep: true, predicate: elem => !isInteractiveDescendant(elem) });
|
445
|
+
const focusInfo = {
|
446
|
+
cellNum: parseInt(cell.getAttribute('data-cell-num')),
|
447
|
+
index: focusables.findIndex(elem => elem === node)
|
448
|
+
};
|
449
|
+
|
450
|
+
switch (e.keyCode) {
|
503
451
|
case keyCodes.RIGHT:
|
504
|
-
node = getComposedActiveElement();
|
505
452
|
if (this.dir === 'rtl') {
|
506
|
-
|
507
|
-
this._focusNextCell(this._cellNum - 1, false);
|
508
|
-
}
|
453
|
+
this._focusPreviousWithinRow(focusInfo, focusables);
|
509
454
|
} else {
|
510
|
-
|
511
|
-
this._focusNextCell(this._cellNum + 1);
|
512
|
-
}
|
455
|
+
this._focusNextWithinRow(focusInfo, focusables);
|
513
456
|
}
|
514
457
|
break;
|
515
458
|
case keyCodes.LEFT:
|
516
|
-
node = getComposedActiveElement();
|
517
459
|
if (this.dir === 'rtl') {
|
518
|
-
|
519
|
-
this._focusNextCell(this._cellNum + 1);
|
520
|
-
}
|
460
|
+
this._focusNextWithinRow(focusInfo, focusables);
|
521
461
|
} else {
|
522
|
-
|
523
|
-
this._focusNextCell(this._cellNum - 1, false);
|
524
|
-
}
|
462
|
+
this._focusPreviousWithinRow(focusInfo, focusables);
|
525
463
|
}
|
526
464
|
break;
|
527
465
|
case keyCodes.UP:
|
528
466
|
// move to above row, focus same item within the cell
|
529
|
-
this._focusNextRow(true);
|
467
|
+
this._focusNextRow(focusInfo, true);
|
530
468
|
break;
|
531
469
|
case keyCodes.DOWN:
|
532
470
|
// move to below row, focus same item within the cell
|
533
|
-
this._focusNextRow();
|
471
|
+
this._focusNextRow(focusInfo);
|
534
472
|
break;
|
535
473
|
case keyCodes.HOME:
|
536
474
|
if (this.dir === 'rtl') {
|
537
|
-
if (
|
475
|
+
if (e.ctrlKey) {
|
538
476
|
this._focusFirstRow();
|
539
477
|
} else {
|
540
478
|
// focus last cell
|
541
479
|
this._focusLastCell();
|
542
480
|
}
|
543
481
|
} else {
|
544
|
-
if (
|
482
|
+
if (e.ctrlKey) {
|
545
483
|
// focus first item of first row
|
546
484
|
this._focusFirstRow();
|
547
485
|
} else {
|
@@ -552,7 +490,7 @@ class ListItemGenericLayout extends RtlMixin(LitElement) {
|
|
552
490
|
break;
|
553
491
|
case keyCodes.END:
|
554
492
|
if (this.dir === 'rtl') {
|
555
|
-
if (
|
493
|
+
if (e.ctrlKey) {
|
556
494
|
// focus first item of last row
|
557
495
|
this._focusLastRow();
|
558
496
|
} else {
|
@@ -560,7 +498,7 @@ class ListItemGenericLayout extends RtlMixin(LitElement) {
|
|
560
498
|
this._focusFirstCell();
|
561
499
|
}
|
562
500
|
} else {
|
563
|
-
if (
|
501
|
+
if (e.ctrlKey) {
|
564
502
|
// focus last item of last row
|
565
503
|
this._focusLastRow();
|
566
504
|
} else {
|
@@ -571,31 +509,21 @@ class ListItemGenericLayout extends RtlMixin(LitElement) {
|
|
571
509
|
break;
|
572
510
|
case keyCodes.PAGEUP:
|
573
511
|
// focus five rows up
|
574
|
-
this._focusNextRow(true, 5);
|
512
|
+
this._focusNextRow(focusInfo, true, 5);
|
575
513
|
break;
|
576
514
|
case keyCodes.PAGEDOWN:
|
577
515
|
// focus five rows down
|
578
|
-
this._focusNextRow(false, 5);
|
516
|
+
this._focusNextRow(focusInfo, false, 5);
|
579
517
|
break;
|
580
518
|
default:
|
581
519
|
preventDefault = false;
|
582
520
|
}
|
521
|
+
|
583
522
|
if (preventDefault) {
|
584
|
-
|
585
|
-
|
523
|
+
e.preventDefault();
|
524
|
+
e.stopPropagation();
|
586
525
|
}
|
587
|
-
|
588
|
-
|
589
|
-
_setFocusInfo(e) {
|
590
|
-
e.stopPropagation();
|
591
|
-
|
592
|
-
if (!this.gridActive) return;
|
593
|
-
const slot = (e.path || e.composedPath()).find(node =>
|
594
|
-
node.nodeName === 'SLOT' && node.classList.contains('d2l-cell'));
|
595
|
-
if (!slot) return;
|
596
|
-
|
597
|
-
this._cellNum = parseInt(slot.getAttribute('data-cell-num'));
|
598
|
-
this._cellFocusedItem = this._getFocusedItemPosition(e.target);
|
526
|
+
return;
|
599
527
|
}
|
600
528
|
|
601
529
|
}
|
package/helpers/README.md
CHANGED
@@ -126,6 +126,9 @@ getComposedActiveElement()
|
|
126
126
|
// gets the first focusable descendant given a node, including those within the shadow DOM
|
127
127
|
getFirstFocusableDescendant(node, includeHidden, predicate, includeTabbablesOnly)
|
128
128
|
|
129
|
+
// gets the focusable elements within the specified element
|
130
|
+
getFocusableDescendants(node, { deep: false, disabled: false, hidden: false, predicate: elem => false, tabbablesOnly: true })
|
131
|
+
|
129
132
|
// gets the focus pseudo-class to used in selectors (focus-visible if supported, or focus)
|
130
133
|
// Usage:
|
131
134
|
// css`
|
package/helpers/focus.js
CHANGED
@@ -40,6 +40,25 @@ export function getFirstFocusableDescendant(node, includeHidden, predicate, incl
|
|
40
40
|
return null;
|
41
41
|
}
|
42
42
|
|
43
|
+
export function getFocusableDescendants(node, options) {
|
44
|
+
let focusables = [];
|
45
|
+
|
46
|
+
const composedChildren = getComposedChildren(node);
|
47
|
+
composedChildren.forEach(child => {
|
48
|
+
if (child.tagName === 'svg') return;
|
49
|
+
if (options?.predicate) {
|
50
|
+
if (!options.predicate(child)) return;
|
51
|
+
}
|
52
|
+
|
53
|
+
if (isFocusable(child, options?.hidden, options?.tabbablesOnly, options?.disabled)) focusables.push(child);
|
54
|
+
if (options?.deep) {
|
55
|
+
focusables = [...focusables, ...getFocusableDescendants(child, options)];
|
56
|
+
}
|
57
|
+
});
|
58
|
+
|
59
|
+
return focusables;
|
60
|
+
}
|
61
|
+
|
43
62
|
export function getFocusPseudoClass() {
|
44
63
|
return isFocusVisibleSupported() ? 'focus-visible' : 'focus';
|
45
64
|
}
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@brightspace-ui/core",
|
3
|
-
"version": "3.
|
3
|
+
"version": "3.30.1",
|
4
4
|
"description": "A collection of accessible, free, open-source web components for building Brightspace applications",
|
5
5
|
"type": "module",
|
6
6
|
"repository": "https://github.com/BrightspaceUI/core.git",
|