@rettangoli/ui 1.9.1 → 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/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) {
|
|
@@ -123,9 +123,16 @@ class RettangoliInputNumberElement extends HTMLElement {
|
|
|
123
123
|
this._inputElement.focus();
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
_emitValueEvent = (eventName) => {
|
|
126
|
+
_emitValueEvent = (eventName, { commit = false } = {}) => {
|
|
127
127
|
const inputValue = this._inputElement.value;
|
|
128
128
|
if (inputValue.trim() === "") {
|
|
129
|
+
// Native number inputs expose incomplete values such as `1e` as an
|
|
130
|
+
// empty string with badInput=true. Keep the last valid form value until
|
|
131
|
+
// the editor becomes either a complete number or a genuine empty state.
|
|
132
|
+
if (this._inputElement.validity.badInput) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
129
136
|
this.dispatchEvent(new CustomEvent(eventName, {
|
|
130
137
|
detail: {
|
|
131
138
|
value: null,
|
|
@@ -135,16 +142,24 @@ class RettangoliInputNumberElement extends HTMLElement {
|
|
|
135
142
|
return;
|
|
136
143
|
}
|
|
137
144
|
|
|
138
|
-
|
|
145
|
+
const numericValue = this._inputElement.valueAsNumber;
|
|
139
146
|
|
|
140
147
|
// Only process if the value is a valid number (not NaN)
|
|
141
|
-
if (!isNaN(numericValue)) {
|
|
142
|
-
|
|
143
|
-
|
|
148
|
+
if (!Number.isNaN(numericValue)) {
|
|
149
|
+
const nextValue = commit
|
|
150
|
+
? this._clampValueToBounds(numericValue)
|
|
151
|
+
: numericValue;
|
|
152
|
+
|
|
153
|
+
// Preserve native editing states (for example a trailing decimal) while
|
|
154
|
+
// typing. Rewriting the value on every input event resets the caret and
|
|
155
|
+
// makes decimal entry and deletion unreliable.
|
|
156
|
+
if (commit) {
|
|
157
|
+
this._inputElement.value = nextValue.toString();
|
|
158
|
+
}
|
|
144
159
|
|
|
145
160
|
this.dispatchEvent(new CustomEvent(eventName, {
|
|
146
161
|
detail: {
|
|
147
|
-
value:
|
|
162
|
+
value: nextValue,
|
|
148
163
|
},
|
|
149
164
|
bubbles: true,
|
|
150
165
|
}));
|
|
@@ -156,7 +171,7 @@ class RettangoliInputNumberElement extends HTMLElement {
|
|
|
156
171
|
};
|
|
157
172
|
|
|
158
173
|
_onChange = () => {
|
|
159
|
-
this._emitValueEvent('value-change');
|
|
174
|
+
this._emitValueEvent('value-change', { commit: true });
|
|
160
175
|
};
|
|
161
176
|
|
|
162
177
|
attributeChangedCallback(name, oldValue, newValue) {
|