@rettangoli/ui 1.9.2 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/rettangoli-iife-layout.min.js +71 -56
- package/dist/rettangoli-iife-ui.min.js +76 -61
- package/package.json +1 -1
- package/src/primitives/dialog.js +350 -0
package/package.json
CHANGED
package/src/primitives/dialog.js
CHANGED
|
@@ -12,6 +12,13 @@ const CLOSE_BUTTON_SIZE_PX = 32;
|
|
|
12
12
|
const CLOSE_BUTTON_OFFSET_PX = 8;
|
|
13
13
|
const ACTIVE_LAYOUT_ATTR = "data-rtgl-active-layout";
|
|
14
14
|
const FIXED_LAYOUT_SELECTOR = `:host([${ACTIVE_LAYOUT_ATTR}="fixed"])`;
|
|
15
|
+
const NATIVE_TEMPORAL_INPUT_TYPES = new Set([
|
|
16
|
+
"date",
|
|
17
|
+
"datetime-local",
|
|
18
|
+
"month",
|
|
19
|
+
"time",
|
|
20
|
+
"week",
|
|
21
|
+
]);
|
|
15
22
|
|
|
16
23
|
const mediaQueryCondition = (mediaQuery) => mediaQuery.replace(/^@media\s+/, "");
|
|
17
24
|
const fixedLayoutStyle = (selector) => css`
|
|
@@ -209,6 +216,21 @@ class RettangoliDialogElement extends HTMLElement {
|
|
|
209
216
|
max-width: 100vw;
|
|
210
217
|
padding: 0;
|
|
211
218
|
}
|
|
219
|
+
|
|
220
|
+
:host([bare]) dialog::backdrop {
|
|
221
|
+
background-color: transparent;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
:host([bare]) slot[name="content"] {
|
|
225
|
+
animation: none;
|
|
226
|
+
background-color: transparent !important;
|
|
227
|
+
border: none;
|
|
228
|
+
border-radius: 0;
|
|
229
|
+
margin-left: 0;
|
|
230
|
+
margin-right: 0;
|
|
231
|
+
max-width: 100vw;
|
|
232
|
+
padding: 0;
|
|
233
|
+
}
|
|
212
234
|
`);
|
|
213
235
|
}
|
|
214
236
|
}
|
|
@@ -254,6 +276,9 @@ class RettangoliDialogElement extends HTMLElement {
|
|
|
254
276
|
this._onDialogScroll = () => {
|
|
255
277
|
this._updateCloseButtonPosition();
|
|
256
278
|
};
|
|
279
|
+
this._onDialogKeyDown = (event) => {
|
|
280
|
+
this._containTabFocus(event);
|
|
281
|
+
};
|
|
257
282
|
this._onCloseButtonClick = (e) => {
|
|
258
283
|
e.preventDefault();
|
|
259
284
|
e.stopPropagation();
|
|
@@ -292,6 +317,7 @@ class RettangoliDialogElement extends HTMLElement {
|
|
|
292
317
|
e.preventDefault();
|
|
293
318
|
this._attemptClose();
|
|
294
319
|
});
|
|
320
|
+
this._dialogElement.addEventListener('keydown', this._onDialogKeyDown);
|
|
295
321
|
}
|
|
296
322
|
|
|
297
323
|
_attemptClose() {
|
|
@@ -382,6 +408,330 @@ class RettangoliDialogElement extends HTMLElement {
|
|
|
382
408
|
return closeButtonElement;
|
|
383
409
|
}
|
|
384
410
|
|
|
411
|
+
_isTabbableElement(element) {
|
|
412
|
+
if (!this._isTabbableElementIgnoringRadioGroup(element)) {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return !(
|
|
417
|
+
element instanceof HTMLInputElement &&
|
|
418
|
+
element.type === "radio" &&
|
|
419
|
+
!this._isTabbableRadio(element)
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
_getComposedParent(element) {
|
|
424
|
+
if (element.assignedSlot) {
|
|
425
|
+
return element.assignedSlot;
|
|
426
|
+
}
|
|
427
|
+
if (element.parentElement) {
|
|
428
|
+
return element.parentElement;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const root = element.getRootNode?.();
|
|
432
|
+
return root instanceof ShadowRoot ? root.host : null;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
_hasInertComposedAncestor(element) {
|
|
436
|
+
let currentElement = element;
|
|
437
|
+
while (currentElement) {
|
|
438
|
+
if (currentElement instanceof HTMLElement && currentElement.inert) {
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
currentElement = this._getComposedParent(currentElement);
|
|
442
|
+
}
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
_isEditingHost(element) {
|
|
447
|
+
if (!(element instanceof HTMLElement) || !element.isContentEditable) {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const parent = this._getComposedParent(element);
|
|
452
|
+
return !(parent instanceof HTMLElement && parent.isContentEditable);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
_getSequentialTabIndex(element) {
|
|
456
|
+
if (typeof element.tabIndex === "number" && element.tabIndex >= 0) {
|
|
457
|
+
return element.tabIndex;
|
|
458
|
+
}
|
|
459
|
+
if (!element.hasAttribute("tabindex") && this._isEditingHost(element)) {
|
|
460
|
+
return 0;
|
|
461
|
+
}
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
_isHiddenByClosedDetails(element) {
|
|
466
|
+
let currentElement = element;
|
|
467
|
+
let parent = this._getComposedParent(currentElement);
|
|
468
|
+
|
|
469
|
+
while (parent) {
|
|
470
|
+
if (parent instanceof HTMLDetailsElement && !parent.open) {
|
|
471
|
+
const summary = [...parent.children]
|
|
472
|
+
.find((child) => child instanceof HTMLElement && child.localName === "summary");
|
|
473
|
+
if (currentElement !== summary) {
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
currentElement = parent;
|
|
478
|
+
parent = this._getComposedParent(currentElement);
|
|
479
|
+
}
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
_isInsideDialog(element) {
|
|
484
|
+
let currentElement = element;
|
|
485
|
+
while (currentElement) {
|
|
486
|
+
if (currentElement === this._dialogElement) {
|
|
487
|
+
return true;
|
|
488
|
+
}
|
|
489
|
+
currentElement = this._getComposedParent(currentElement);
|
|
490
|
+
}
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
_isTabbableRadio(radioElement) {
|
|
495
|
+
if (!radioElement.name) {
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const root = radioElement.getRootNode();
|
|
500
|
+
const radioGroup = [...root.querySelectorAll("input[type='radio']")]
|
|
501
|
+
.filter((candidate) => (
|
|
502
|
+
candidate.name === radioElement.name &&
|
|
503
|
+
candidate.form === radioElement.form &&
|
|
504
|
+
this._isInsideDialog(candidate) &&
|
|
505
|
+
this._isTabbableElementIgnoringRadioGroup(candidate)
|
|
506
|
+
));
|
|
507
|
+
const checkedRadio = radioGroup.find((candidate) => candidate.checked);
|
|
508
|
+
return radioElement === (checkedRadio ?? radioGroup[0]);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
_isTabbableElementIgnoringRadioGroup(element) {
|
|
512
|
+
if (
|
|
513
|
+
!(element instanceof Element) ||
|
|
514
|
+
this._getSequentialTabIndex(element) === null ||
|
|
515
|
+
element.matches(":disabled") ||
|
|
516
|
+
(element instanceof HTMLInputElement && element.type === "hidden") ||
|
|
517
|
+
this._hasInertComposedAncestor(element) ||
|
|
518
|
+
this._isHiddenByClosedDetails(element)
|
|
519
|
+
) {
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const style = getComputedStyle(element);
|
|
524
|
+
const isCssVisible = (
|
|
525
|
+
style.display !== "none" &&
|
|
526
|
+
style.visibility !== "hidden" &&
|
|
527
|
+
style.visibility !== "collapse"
|
|
528
|
+
);
|
|
529
|
+
if (element instanceof HTMLAreaElement) {
|
|
530
|
+
return isCssVisible && this._hasVisibleAssociatedImage(element);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return (
|
|
534
|
+
element.getClientRects().length > 0 &&
|
|
535
|
+
isCssVisible
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
_hasVisibleAssociatedImage(areaElement) {
|
|
540
|
+
const mapElement = areaElement.closest("map[name]");
|
|
541
|
+
if (!(mapElement instanceof HTMLMapElement) || !mapElement.name) {
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const root = mapElement.getRootNode();
|
|
546
|
+
if (!(root instanceof Document || root instanceof ShadowRoot)) {
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return [...root.querySelectorAll("img[usemap]")].some((imageElement) => {
|
|
551
|
+
const useMap = imageElement.getAttribute("usemap")?.trim();
|
|
552
|
+
if (
|
|
553
|
+
useMap !== `#${mapElement.name}` ||
|
|
554
|
+
this._hasInertComposedAncestor(imageElement) ||
|
|
555
|
+
this._isHiddenByClosedDetails(imageElement)
|
|
556
|
+
) {
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const style = getComputedStyle(imageElement);
|
|
561
|
+
return (
|
|
562
|
+
imageElement.getClientRects().length > 0 &&
|
|
563
|
+
style.display !== "none" &&
|
|
564
|
+
style.visibility !== "hidden" &&
|
|
565
|
+
style.visibility !== "collapse"
|
|
566
|
+
);
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
_appendTabScope(scopeParent, children, items) {
|
|
571
|
+
const hasTabIndex = scopeParent.hasAttribute("tabindex");
|
|
572
|
+
if (hasTabIndex && scopeParent.tabIndex < 0) {
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const childItems = [];
|
|
577
|
+
this._collectTabOrderItems(children, childItems);
|
|
578
|
+
const elements = [];
|
|
579
|
+
if (this._isTabbableElement(scopeParent)) {
|
|
580
|
+
elements.push(scopeParent);
|
|
581
|
+
}
|
|
582
|
+
elements.push(...this._sortTabOrderItems(childItems));
|
|
583
|
+
|
|
584
|
+
if (elements.length > 0) {
|
|
585
|
+
items.push({
|
|
586
|
+
tabIndex: hasTabIndex ? scopeParent.tabIndex : 0,
|
|
587
|
+
elements,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
_collectTabOrderItems(nodes, items) {
|
|
593
|
+
for (const node of nodes) {
|
|
594
|
+
if (!(node instanceof Element)) {
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (node instanceof HTMLSlotElement) {
|
|
599
|
+
const assignedElements = node.assignedElements();
|
|
600
|
+
const children = assignedElements.length > 0
|
|
601
|
+
? assignedElements
|
|
602
|
+
: [...node.children];
|
|
603
|
+
this._appendTabScope(node, children, items);
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (node.shadowRoot) {
|
|
608
|
+
this._appendTabScope(node, [...node.shadowRoot.children], items);
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (this._isTabbableElement(node)) {
|
|
613
|
+
items.push({
|
|
614
|
+
tabIndex: this._getSequentialTabIndex(node),
|
|
615
|
+
elements: [node],
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
this._collectTabOrderItems(node.children, items);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
_sortTabOrderItems(items) {
|
|
623
|
+
const positiveItems = items
|
|
624
|
+
.map((item, documentOrder) => ({ ...item, documentOrder }))
|
|
625
|
+
.filter((item) => item.tabIndex > 0)
|
|
626
|
+
.sort((left, right) => (
|
|
627
|
+
left.tabIndex - right.tabIndex ||
|
|
628
|
+
left.documentOrder - right.documentOrder
|
|
629
|
+
));
|
|
630
|
+
const regularItems = items.filter((item) => item.tabIndex === 0);
|
|
631
|
+
return [...positiveItems, ...regularItems]
|
|
632
|
+
.flatMap((item) => item.elements);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
_getTabbableElements() {
|
|
636
|
+
const items = [];
|
|
637
|
+
this._collectTabOrderItems(this._dialogElement.children, items);
|
|
638
|
+
return this._sortTabOrderItems(items);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
_getDeepActiveElement() {
|
|
642
|
+
let activeElement = document.activeElement;
|
|
643
|
+
while (activeElement?.shadowRoot?.activeElement) {
|
|
644
|
+
activeElement = activeElement.shadowRoot.activeElement;
|
|
645
|
+
}
|
|
646
|
+
return activeElement;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
_hasNativeInternalTabSequence(element) {
|
|
650
|
+
return (
|
|
651
|
+
element instanceof HTMLInputElement &&
|
|
652
|
+
NATIVE_TEMPORAL_INPUT_TYPES.has(element.type)
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
_deferNativeTabContainment(movingBeforeFirst) {
|
|
657
|
+
setTimeout(() => {
|
|
658
|
+
if (!this._dialogElement.matches(":modal")) {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const activeElement = this._getDeepActiveElement();
|
|
663
|
+
if (activeElement && this._isInsideDialog(activeElement)) {
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const tabbableElements = this._getTabbableElements();
|
|
668
|
+
const nextElement = movingBeforeFirst
|
|
669
|
+
? tabbableElements[tabbableElements.length - 1]
|
|
670
|
+
: tabbableElements[0];
|
|
671
|
+
if (nextElement) {
|
|
672
|
+
nextElement.focus({ preventScroll: true });
|
|
673
|
+
} else {
|
|
674
|
+
this._dialogElement.focus({ preventScroll: true });
|
|
675
|
+
}
|
|
676
|
+
}, 0);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
_containTabFocus(event) {
|
|
680
|
+
if (
|
|
681
|
+
event.key !== "Tab" ||
|
|
682
|
+
event.defaultPrevented ||
|
|
683
|
+
event.altKey ||
|
|
684
|
+
event.ctrlKey ||
|
|
685
|
+
event.metaKey ||
|
|
686
|
+
!this._isTabEventOwnedByDialog(event)
|
|
687
|
+
) {
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const tabbableElements = this._getTabbableElements();
|
|
692
|
+
if (tabbableElements.length === 0) {
|
|
693
|
+
event.preventDefault();
|
|
694
|
+
this._dialogElement.focus({ preventScroll: true });
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const activeElement = this._getDeepActiveElement();
|
|
699
|
+
const firstElement = tabbableElements[0];
|
|
700
|
+
const lastElement = tabbableElements[tabbableElements.length - 1];
|
|
701
|
+
const movingBeforeFirst = event.shiftKey && (
|
|
702
|
+
activeElement === firstElement || activeElement === this._dialogElement
|
|
703
|
+
);
|
|
704
|
+
const movingAfterLast = !event.shiftKey && activeElement === lastElement;
|
|
705
|
+
|
|
706
|
+
if (!movingBeforeFirst && !movingAfterLast) {
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (this._hasNativeInternalTabSequence(activeElement)) {
|
|
711
|
+
this._deferNativeTabContainment(movingBeforeFirst);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
event.preventDefault();
|
|
716
|
+
const nextElement = movingBeforeFirst ? lastElement : firstElement;
|
|
717
|
+
nextElement.focus({ preventScroll: true });
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
_isTabEventOwnedByDialog(event) {
|
|
721
|
+
for (const node of event.composedPath()) {
|
|
722
|
+
if (!(node instanceof HTMLDialogElement)) {
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
if (node === this._dialogElement) {
|
|
726
|
+
return true;
|
|
727
|
+
}
|
|
728
|
+
if (node.matches(":modal")) {
|
|
729
|
+
return false;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
|
|
385
735
|
_clearManagedLongTokenWrapping() {
|
|
386
736
|
for (const textElement of this._managedLongTokenTextElements) {
|
|
387
737
|
if (textElement?.isConnected) {
|