@reliability-design/web 1.4.3 → 1.4.4

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.
@@ -52,6 +52,8 @@ export class RelRangePicker extends LitElement {
52
52
  @state() private hoverDate: string | null = null;
53
53
 
54
54
  @query(".trigger-button") private triggerButton?: HTMLButtonElement;
55
+ @query(".popover") private popoverEl?: HTMLDivElement;
56
+ @query(".backdrop") private backdropEl?: HTMLDivElement;
55
57
 
56
58
  static styles = css`
57
59
  :host {
@@ -113,6 +115,17 @@ export class RelRangePicker extends LitElement {
113
115
 
114
116
  /* ── Popover shell ── */
115
117
  .popover {
118
+ /* Reset native [popover] UA styles first (the shell is promoted to
119
+ the top layer so transformed / overflow:hidden ancestors — e.g. a
120
+ centered rel-modal panel — can't hijack or clip it). While in the
121
+ top layer, JS overrides the anchored position below with inline
122
+ fixed coordinates; the CSS anchoring is the no-Popover-API
123
+ fallback. */
124
+ margin: 0;
125
+ padding: 0;
126
+ width: auto;
127
+ height: auto;
128
+ color: inherit;
116
129
  position: absolute;
117
130
  top: calc(100% + 4px);
118
131
  left: 0;
@@ -137,6 +150,13 @@ export class RelRangePicker extends LitElement {
137
150
  .backdrop {
138
151
  position: fixed;
139
152
  inset: 0;
153
+ /* Reset native [popover] UA styles — the backdrop is promoted to the
154
+ top layer right before the panel so the panel stacks above it. */
155
+ margin: 0;
156
+ padding: 0;
157
+ width: auto;
158
+ height: auto;
159
+ border: none;
140
160
  z-index: 2147483646;
141
161
  background: rgba(0,0,0,0.4);
142
162
  }
@@ -448,17 +468,111 @@ export class RelRangePicker extends LitElement {
448
468
  super.disconnectedCallback();
449
469
  document.removeEventListener("click", this.handleClickOutside);
450
470
  document.removeEventListener("keydown", this.handleGlobalKeydown);
471
+ window.removeEventListener("resize", this.onReposition);
472
+ window.removeEventListener("scroll", this.onReposition, true);
473
+ this.hideTopLayer();
451
474
  }
452
475
 
453
- updated() {
476
+ updated(changed: Map<string, unknown>) {
454
477
  this.style.width = this.width;
455
478
  this.style.minWidth = this.minWidth;
456
479
  this.style.maxWidth = this.maxWidth;
457
480
  this.style.height = this.height;
458
481
  this.style.minHeight = this.minHeight;
459
482
  this.style.maxHeight = this.maxHeight;
483
+
484
+ // `open` is a public reflected property (consumers may set it directly,
485
+ // not only via the trigger), so top-layer promotion syncs here rather
486
+ // than inside openPopover/cancelPopover.
487
+ if (changed.has("open")) {
488
+ if (this.open) {
489
+ this.showTopLayer();
490
+ window.addEventListener("resize", this.onReposition);
491
+ window.addEventListener("scroll", this.onReposition, true);
492
+ } else {
493
+ window.removeEventListener("resize", this.onReposition);
494
+ window.removeEventListener("scroll", this.onReposition, true);
495
+ this.hideTopLayer();
496
+ }
497
+ }
498
+ }
499
+
500
+ /* A transformed ancestor (e.g. rel-modal's centered panel with
501
+ * translate(-50%,-50%)) becomes the containing block for position:fixed
502
+ * descendants, and its overflow:hidden clips position:absolute ones —
503
+ * either way the picker shell breaks inside a modal. Promoting shell
504
+ * (and popup-mode backdrop) to the browser top layer sidesteps both —
505
+ * same fix as rel-popover / rel-autocomplete / rel-select. `manual`
506
+ * keeps outside-click and Escape handling under this component's own
507
+ * control. While in the top layer the CSS anchoring (absolute, below
508
+ * the trigger) is meaningless, so popover mode gets inline fixed
509
+ * coordinates from updatePopoverPosition(). */
510
+
511
+ private showTopLayer() {
512
+ // Backdrop first — later-shown popovers stack above earlier ones.
513
+ for (const el of [this.mode === "popup" ? this.backdropEl : undefined, this.popoverEl]) {
514
+ if (!el || typeof el.showPopover !== "function") continue;
515
+ try {
516
+ if (!el.matches(":popover-open")) el.showPopover();
517
+ } catch {
518
+ /* older engines / detached element — CSS fallback applies */
519
+ }
520
+ }
521
+ if (this.mode === "popover") {
522
+ requestAnimationFrame(() => this.updatePopoverPosition());
523
+ }
460
524
  }
461
525
 
526
+ private hideTopLayer() {
527
+ for (const el of [this.popoverEl, this.backdropEl]) {
528
+ if (!el || typeof el.hidePopover !== "function") continue;
529
+ try {
530
+ if (el.matches(":popover-open")) el.hidePopover();
531
+ } catch {
532
+ /* already hidden or unsupported — nothing to do */
533
+ }
534
+ }
535
+ if (this.popoverEl) {
536
+ this.popoverEl.style.position = "";
537
+ this.popoverEl.style.top = "";
538
+ this.popoverEl.style.left = "";
539
+ }
540
+ }
541
+
542
+ private onReposition = () => {
543
+ if (this.open && this.mode === "popover") this.updatePopoverPosition();
544
+ };
545
+
546
+ private updatePopoverPosition = () => {
547
+ const panel = this.popoverEl;
548
+ if (!panel || !this.open || this.mode !== "popover") return;
549
+
550
+ // Host rect anchors both trigger variants (default button and icon).
551
+ const t = this.getBoundingClientRect();
552
+ const c = panel.getBoundingClientRect();
553
+
554
+ let top = t.bottom + 4;
555
+ if (top + c.height > window.innerHeight - 8) {
556
+ top = Math.max(8, t.top - c.height - 4);
557
+ }
558
+ let left = t.left;
559
+ if (left + c.width > window.innerWidth - 8) {
560
+ left = Math.max(8, window.innerWidth - c.width - 8);
561
+ }
562
+
563
+ // Without top-layer support a transformed ancestor may be the containing
564
+ // block for position:fixed, shifting the shell away from the viewport
565
+ // coordinates computed above. Apply, re-measure, and cancel the offset.
566
+ panel.style.position = "fixed";
567
+ panel.style.top = `${top}px`;
568
+ panel.style.left = `${left}px`;
569
+ const applied = panel.getBoundingClientRect();
570
+ top -= applied.top - top;
571
+ left -= applied.left - left;
572
+ panel.style.top = `${top}px`;
573
+ panel.style.left = `${left}px`;
574
+ };
575
+
462
576
  // ── Open / close ──
463
577
 
464
578
  private handleClickOutside = (e: MouseEvent) => {
@@ -916,11 +1030,12 @@ export class RelRangePicker extends LitElement {
916
1030
  `}
917
1031
 
918
1032
  ${isPopup && this.open
919
- ? html`<div part="overlay" class="backdrop" @click=${this.cancelPopover}></div>`
1033
+ ? html`<div part="overlay" class="backdrop" popover="manual" @click=${this.cancelPopover}></div>`
920
1034
  : nothing}
921
1035
 
922
1036
  <div
923
1037
  part="panel" class="popover ${this.open ? "open" : ""} ${isPopup ? "popup" : ""}"
1038
+ popover="manual"
924
1039
  role="dialog"
925
1040
  aria-label="Select date range"
926
1041
  >
@@ -119,3 +119,4 @@ None exposed.
119
119
  - Typed text that doesn't exactly match (case-insensitive) any option reverts to the last committed `selected` value on blur, Escape, or Tab
120
120
  - The ghost-text suggestion shows the remainder of the first option whose name starts with the typed text (case-insensitive); it has no effect on filtering, which matches substrings anywhere in an option
121
121
  - Dropdown panel is positioned via `position: fixed` and flips above the trigger if there isn't enough viewport space below
122
+ - The open listbox is promoted to the browser **top layer** (native Popover API, `popover="manual"`), so it renders correctly inside transformed / `overflow: hidden` containers — a centered `rel-modal`, a bottom `rel-drawer`. Engines without the Popover API fall back to `position: fixed` with a measure-and-cancel correction for transformed ancestors