@ds-gn/ui 0.9.0 → 0.11.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.
Files changed (84) hide show
  1. package/README.md +4 -4
  2. package/custom-elements.json +5527 -2136
  3. package/dist/common/controllers/OverlayDismissController.d.ts +52 -0
  4. package/dist/common/controllers/OverlayPositionController.d.ts +168 -0
  5. package/dist/common/controllers/RovingFocusController.d.ts +99 -0
  6. package/dist/common/controllers/SectionEntranceController.d.ts +37 -0
  7. package/dist/common/helpers/field-control.d.ts +35 -0
  8. package/dist/common/helpers/listbox-navigation.d.ts +50 -0
  9. package/dist/common/props/size/size.props.d.ts +11 -0
  10. package/dist/common/styles/overlay-panel.styles.d.ts +55 -0
  11. package/dist/components/Button/src/Button.d.ts +20 -0
  12. package/dist/components/Button/src/Button.js +28 -0
  13. package/dist/components/Container/src/Container.d.ts +10 -0
  14. package/dist/components/Container/src/Container.js +148 -23
  15. package/dist/components/DropdownMenu/src/DropdownMenu.d.ts +150 -0
  16. package/dist/components/DropdownMenu/src/DropdownMenu.js +1550 -0
  17. package/dist/components/DropdownMenu/src/DropdownMenu.playground.d.ts +2 -0
  18. package/dist/components/DropdownMenu/src/DropdownMenu.props.d.ts +47 -0
  19. package/dist/components/DropdownMenu/src/DropdownMenu.props.js +54 -0
  20. package/dist/components/DropdownMenu/src/DropdownMenu.styles.d.ts +20 -0
  21. package/dist/components/Field/src/Field.d.ts +39 -0
  22. package/dist/components/Field/src/Field.js +203 -0
  23. package/dist/components/Field/src/Field.playground.d.ts +2 -0
  24. package/dist/components/Field/src/Field.styles.d.ts +6 -0
  25. package/dist/components/Icon/src/Icon.d.ts +5 -0
  26. package/dist/components/Icon/src/Icon.js +3 -0
  27. package/dist/components/Icon/src/icons/arrow-down.d.ts +1 -0
  28. package/dist/components/Icon/src/icons/arrow-down.js +6 -0
  29. package/dist/components/Icon/src/icons/arrow-up.d.ts +1 -0
  30. package/dist/components/Icon/src/icons/arrow-up.js +6 -0
  31. package/dist/components/Icon/src/icons/chevrons-up-down.d.ts +1 -0
  32. package/dist/components/Icon/src/icons/chevrons-up-down.js +6 -0
  33. package/dist/components/Icon/src/icons/cookie.d.ts +1 -0
  34. package/dist/components/Icon/src/icons/cookie.js +6 -0
  35. package/dist/components/Icon/src/icons/icon-names.d.ts +1 -1
  36. package/dist/components/Icon/src/icons/icon-names.js +4 -0
  37. package/dist/components/Icon/src/icons/index.d.ts +4 -0
  38. package/dist/components/Icon/src/icons/index.js +4 -0
  39. package/dist/components/Input/src/Input.d.ts +37 -0
  40. package/dist/components/Input/src/Input.js +300 -143
  41. package/dist/components/Input/src/Input.props.d.ts +2 -1
  42. package/dist/components/Input/src/Input.props.js +12 -6
  43. package/dist/components/Input/src/Input.styles.d.ts +12 -4
  44. package/dist/components/Modal/src/Modal.d.ts +1 -0
  45. package/dist/components/Modal/src/Modal.js +1 -0
  46. package/dist/components/Nav/src/Nav.js +2 -1
  47. package/dist/components/NavLink/src/NavLink.d.ts +14 -30
  48. package/dist/components/NavLink/src/NavLink.js +201 -65
  49. package/dist/components/Select/src/Select.d.ts +157 -0
  50. package/dist/components/Select/src/Select.js +1766 -0
  51. package/dist/components/Select/src/Select.playground.d.ts +2 -0
  52. package/dist/components/Select/src/Select.props.d.ts +19 -0
  53. package/dist/components/Select/src/Select.props.js +40 -0
  54. package/dist/components/Select/src/Select.styles.d.ts +30 -0
  55. package/dist/components/Table/src/Table.d.ts +93 -0
  56. package/dist/components/Table/src/Table.js +1105 -0
  57. package/dist/components/Table/src/Table.playground.d.ts +2 -0
  58. package/dist/components/Table/src/Table.props.d.ts +53 -0
  59. package/dist/components/Table/src/Table.props.js +18 -0
  60. package/dist/components/Table/src/Table.styles.d.ts +33 -0
  61. package/dist/components/Toaster/src/Toaster.d.ts +174 -0
  62. package/dist/components/Toaster/src/Toaster.js +1489 -0
  63. package/dist/components/Toaster/src/Toaster.playground.d.ts +2 -0
  64. package/dist/components/Toaster/src/Toaster.props.d.ts +14 -0
  65. package/dist/components/Toaster/src/Toaster.props.js +81 -0
  66. package/dist/components/Toaster/src/Toaster.styles.d.ts +52 -0
  67. package/dist/components/Toaster/src/toast.d.ts +115 -0
  68. package/dist/components/Toaster/src/toast.js +74 -0
  69. package/dist/playground/types.d.ts +27 -0
  70. package/dist/playground-manifest.d.ts +3 -2
  71. package/dist/playground-manifest.js +507 -7
  72. package/dist/react/DropdownMenu.d.ts +8 -0
  73. package/dist/react/DropdownMenu.js +13 -0
  74. package/dist/react/Field.d.ts +6 -0
  75. package/dist/react/Field.js +13 -0
  76. package/dist/react/Select.d.ts +8 -0
  77. package/dist/react/Select.js +13 -0
  78. package/dist/react/Table.d.ts +8 -0
  79. package/dist/react/Table.js +13 -0
  80. package/dist/react/Toaster.d.ts +8 -0
  81. package/dist/react/Toaster.js +13 -0
  82. package/dist/react/index.d.ts +6 -0
  83. package/dist/react/index.js +6 -0
  84. package/package.json +52 -2
@@ -1,3 +1,4 @@
1
+ import { type SizeType } from '../../../common/props/size/size.props.js';
1
2
  export declare const inputTypeValues: {
2
3
  readonly TEXT: "text";
3
4
  readonly EMAIL: "email";
@@ -9,5 +10,5 @@ export declare const inputSizeValues: {
9
10
  readonly MD: "md";
10
11
  readonly LG: "lg";
11
12
  };
12
- export type InputSizeType = (typeof inputSizeValues)[keyof typeof inputSizeValues];
13
+ export type InputSizeType = SizeType;
13
14
  export declare const inputSizeProps: import("lit").PropertyDeclaration<unknown, unknown>;
@@ -21,18 +21,24 @@ var enumCreator = (acceptableValues, propName, type, other = {}) => {
21
21
  };
22
22
  };
23
23
 
24
+ // common/props/size/size.props.ts
25
+ var sizeValues = {
26
+ SM: "sm",
27
+ MD: "md",
28
+ LG: "lg"
29
+ };
30
+ var sizeProps = enumCreator(Object.values(sizeValues), "size", String, {
31
+ reflect: true
32
+ });
33
+
24
34
  // components/Input/src/Input.props.ts
25
35
  var inputTypeValues = {
26
36
  TEXT: "text",
27
37
  EMAIL: "email"
28
38
  };
29
39
  var inputTypeProps = enumCreator(Object.values(inputTypeValues), "type", String, { reflect: true });
30
- var inputSizeValues = {
31
- SM: "sm",
32
- MD: "md",
33
- LG: "lg"
34
- };
35
- var inputSizeProps = enumCreator(Object.values(inputSizeValues), "size", String, { reflect: true });
40
+ var inputSizeValues = sizeValues;
41
+ var inputSizeProps = sizeProps;
36
42
  export {
37
43
  inputSizeProps,
38
44
  inputSizeValues,
@@ -1,6 +1,14 @@
1
1
  import { type InputSizeType } from './Input.props.js';
2
- export declare const classNames: (size: InputSizeType, hasError: boolean, disabled: boolean) => string;
2
+ /**
3
+ * Visual state of the control. Exactly one applies, so the border and
4
+ * background utilities never compete: disabled wins over read-only, which wins
5
+ * over invalid.
6
+ */
7
+ export type InputState = 'default' | 'invalid' | 'readonly' | 'disabled';
8
+ export declare const classNames: (size: InputSizeType, state: InputState) => string;
3
9
  export declare const wrapperClasses: () => string;
4
- export declare const labelClasses: () => string;
5
- export declare const errorWrapClasses: (hasError: boolean) => string;
6
- export declare const errorMessageClasses: (hasError: boolean) => string;
10
+ export declare const labelClasses: (size: InputSizeType, disabled: boolean) => string;
11
+ export declare const requiredClasses: () => string;
12
+ export declare const hintClasses: (size: InputSizeType) => string;
13
+ export declare const errorClasses: (size: InputSizeType) => string;
14
+ export declare const errorIconClasses: () => string;
@@ -22,6 +22,7 @@ import { type ModalCloseReasonType, type ModalMotionType, type ModalPlacementTyp
22
22
  * @slot description - Supporting text referenced by `aria-describedby`.
23
23
  * @slot footer - Actions, typically buttons.
24
24
  * @slot close - A close control; clicking anything slotted here requests a close.
25
+ * @slot toaster - Not for authors: an open page's `dsgn-toaster` moves its region here while the modal is open, because a modal dialog makes everything outside it inert.
25
26
  *
26
27
  * @csspart dialog - Native dialog element.
27
28
  * @csspart close-button - Built-in close button.
@@ -1163,6 +1163,7 @@ class Modal extends Base {
1163
1163
  <slot name="footer" @slotchange=${this._handleSlotChange}></slot>
1164
1164
  </footer>
1165
1165
  </div>
1166
+ <slot name="toaster"></slot>
1166
1167
  </dialog>
1167
1168
  `;
1168
1169
  }
@@ -144,7 +144,8 @@ var styles2 = css2`/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.co
144
144
  --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
145
145
  --default-transition-duration: 150ms;
146
146
  --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
147
- --radius-input: var(--dsgn-src-radius-input, 0.5rem);
147
+ --radius-control: var(--dsgn-src-radius-control, 0.375rem);
148
+ --radius-input: var(--dsgn-src-radius-input, var(--radius-control));
148
149
  --color-accent: inherit;
149
150
  --color-outline: inherit;
150
151
  --color-page: inherit;
@@ -91,13 +91,21 @@ export declare class NavLink extends Base {
91
91
  */
92
92
  private breakpointController;
93
93
  /**
94
- * The shift is measured against the viewport, so a change in the room around
95
- * the trigger invalidates it: an open panel would otherwise keep the offset
96
- * the old width earned it and hang off whichever edge the new width put it
97
- * over. Only subscribed while a panel is open. See the controller for the
98
- * layout changes this does and does not see.
94
+ * Panels are positioned against their own trigger and the viewport, not
95
+ * against the nav, so every column count uses the same rule. The shift is
96
+ * measured against the viewport, so a change in the room around the trigger
97
+ * invalidates it: an open panel would otherwise keep the offset the old width
98
+ * earned it and hang off whichever edge the new width put it over. The
99
+ * controller only tracks layout while a panel is open. The default 16px
100
+ * gutter matches the `2rem` the styles subtract from `100vw` for the panel's
101
+ * max width, so a panel squeezed to its widest still lands on the gutter.
102
+ */
103
+ private overlayPosition;
104
+ /**
105
+ * Escape closes any open panel and returns focus to the trigger; a press
106
+ * outside closes a pinned one (the listener only exists while pinned).
99
107
  */
100
- private layoutChangeController;
108
+ private overlayDismiss;
101
109
  connectedCallback(): void;
102
110
  /**
103
111
  * Resolve the `rel` attribute. For `target="_blank"` links, always include
@@ -105,7 +113,6 @@ export declare class NavLink extends Base {
105
113
  * new-tab nav link never exposes `window.opener`.
106
114
  */
107
115
  private get computedRel();
108
- disconnectedCallback(): void;
109
116
  updated(changedProperties: Map<string | number | symbol, unknown>): void;
110
117
  private updateDropdownState;
111
118
  private handleDropdownSlotChange;
@@ -128,29 +135,6 @@ export declare class NavLink extends Base {
128
135
  */
129
136
  private setExpanded;
130
137
  private toggleDropdown;
131
- private handleDocumentPointerDown;
132
- private clearDropdownAlignment;
133
- /**
134
- * Width the dropdown panel renders at. A closed panel is `display: none`, so
135
- * lay it out just long enough to read it — the browser cannot paint between
136
- * the two writes below, so nothing flashes.
137
- */
138
- private measureDropdownWidth;
139
- /**
140
- * Horizontal shift for the panel, measured from the left-aligned position
141
- * under its trigger. Hanging right of the trigger is the default; if that
142
- * would spill past the viewport, the panel flips to hang left instead, with
143
- * its right edge on the trigger's. Only when *that* would leave the viewport
144
- * too — a panel wider than the room on either side of its trigger — does it
145
- * give up the anchor and sit on the gutter, where its fluid width has already
146
- * shrunk it to fit.
147
- */
148
- private dropdownShift;
149
- /**
150
- * Panels are positioned against their own trigger and the viewport, not
151
- * against the nav, so every column count uses the same rule.
152
- */
153
- private updateDropdownAlignment;
154
138
  /**
155
139
  * Hover-to-open applies only where the desktop trigger is rendered (>= sm)
156
140
  * and only on a device that really hovers.
@@ -91,6 +91,60 @@ class Base extends LitElement {
91
91
  }
92
92
  defineElement("dsgn-base", Base);
93
93
 
94
+ // common/controllers/OverlayDismissController.ts
95
+ class OverlayDismissController {
96
+ constructor(host, options) {
97
+ this.listening = false;
98
+ this.wantsOutside = false;
99
+ this.handleDocumentPointerDown = (event) => {
100
+ if (!event.composedPath().includes(this.host)) {
101
+ this.options.onDismiss("outside");
102
+ }
103
+ };
104
+ this.host = host;
105
+ this.options = options;
106
+ host.addController(this);
107
+ }
108
+ start() {
109
+ this.wantsOutside = true;
110
+ this.listen();
111
+ }
112
+ stop() {
113
+ this.wantsOutside = false;
114
+ this.unlisten();
115
+ }
116
+ handleKeydown(event) {
117
+ if (event.key !== "Escape" || !this.options.isOpen()) {
118
+ return false;
119
+ }
120
+ this.options.onDismiss("escape");
121
+ this.options.returnFocusTo?.()?.focus();
122
+ return true;
123
+ }
124
+ hostConnected() {
125
+ if (this.wantsOutside) {
126
+ this.listen();
127
+ }
128
+ }
129
+ hostDisconnected() {
130
+ this.unlisten();
131
+ }
132
+ listen() {
133
+ if (this.listening || typeof document === "undefined") {
134
+ return;
135
+ }
136
+ this.listening = true;
137
+ document.addEventListener("pointerdown", this.handleDocumentPointerDown);
138
+ }
139
+ unlisten() {
140
+ if (!this.listening) {
141
+ return;
142
+ }
143
+ this.listening = false;
144
+ document.removeEventListener("pointerdown", this.handleDocumentPointerDown);
145
+ }
146
+ }
147
+
94
148
  // common/helpers/layout-change.ts
95
149
  var listenersByTarget = new Map;
96
150
  var observer;
@@ -152,21 +206,118 @@ var observeLayoutChanges = (target, listener) => {
152
206
  };
153
207
  };
154
208
 
155
- // common/controllers/LayoutChangeController.ts
156
- class LayoutChangeController {
157
- constructor(host, onLayoutChange) {
209
+ // common/controllers/OverlayPositionController.ts
210
+ var overlayViewportGutter = 16;
211
+ var computeViewportPosition = (anchor, { placement, shift }, viewportHeight, offset = 0) => {
212
+ const below = placement.startsWith("bottom");
213
+ return {
214
+ left: anchor.left + shift,
215
+ top: below ? anchor.bottom + offset : null,
216
+ bottom: below ? null : viewportHeight - anchor.top + offset,
217
+ anchorWidth: anchor.width
218
+ };
219
+ };
220
+ var computeOverlayShift = (anchor, overlayWidth, viewportWidth, gutter = overlayViewportGutter, align = "start") => {
221
+ const viewportRight = viewportWidth - gutter;
222
+ const startFits = anchor.left + overlayWidth <= viewportRight;
223
+ const endAligned = anchor.width - overlayWidth;
224
+ const endFits = anchor.left + endAligned >= gutter;
225
+ if (!overlayWidth) {
226
+ return 0;
227
+ }
228
+ if (align === "end") {
229
+ if (endFits)
230
+ return endAligned;
231
+ return startFits ? 0 : viewportRight - overlayWidth - anchor.left;
232
+ }
233
+ if (startFits)
234
+ return 0;
235
+ return endFits ? endAligned : gutter - anchor.left;
236
+ };
237
+ var computeOverlayPlacement = (anchor, overlayHeight, viewportHeight, {
238
+ gutter = overlayViewportGutter,
239
+ offset = 0,
240
+ align = "start"
241
+ } = {}) => {
242
+ const below = viewportHeight - gutter - anchor.bottom - offset;
243
+ const above = anchor.top - gutter - offset;
244
+ const flips = !!overlayHeight && overlayHeight > below && (overlayHeight <= above || above > below);
245
+ return `${flips ? "top" : "bottom"}-${align}`;
246
+ };
247
+ var sizeOf = (overlay) => {
248
+ if (overlay.offsetWidth) {
249
+ return { width: overlay.offsetWidth, height: overlay.offsetHeight };
250
+ }
251
+ const { width, height } = overlay.getBoundingClientRect();
252
+ return { width, height };
253
+ };
254
+ var measureOverlay = (overlay) => {
255
+ const open = sizeOf(overlay);
256
+ if (open.width) {
257
+ return open;
258
+ }
259
+ const inline = overlay.style.cssText;
260
+ overlay.style.display = "block";
261
+ overlay.style.visibility = "hidden";
262
+ const size = sizeOf(overlay);
263
+ overlay.style.cssText = inline;
264
+ return size;
265
+ };
266
+
267
+ class OverlayPositionController {
268
+ constructor(host, options) {
158
269
  this.wantsUpdates = false;
270
+ this.update = () => {
271
+ const anchor = this.options.anchor?.() ?? this.host;
272
+ const overlay = this.options.overlay();
273
+ if (!anchor || !overlay) {
274
+ return;
275
+ }
276
+ const anchorRect = anchor.getBoundingClientRect();
277
+ this.options.beforeMeasure?.(anchorRect);
278
+ const { width, height } = measureOverlay(overlay);
279
+ const gutter = this.options.gutter ?? overlayViewportGutter;
280
+ const offset = this.options.offset ?? 0;
281
+ const align = this.options.align?.() ?? "start";
282
+ const shift = computeOverlayShift(anchorRect, width, window.innerWidth, gutter, align);
283
+ const placement = this.options.flip ? computeOverlayPlacement(anchorRect, height, window.innerHeight, {
284
+ gutter,
285
+ offset,
286
+ align
287
+ }) : `bottom-${align}`;
288
+ this._position = {
289
+ shift,
290
+ placement,
291
+ viewport: computeViewportPosition(anchorRect, { placement, shift }, window.innerHeight, offset)
292
+ };
293
+ this.options.apply(this._position);
294
+ };
295
+ this.scheduleUpdate = () => {
296
+ if (this.frame !== undefined) {
297
+ return;
298
+ }
299
+ this.frame = requestAnimationFrame(() => {
300
+ this.frame = undefined;
301
+ this.update();
302
+ });
303
+ };
159
304
  this.host = host;
160
- this.onLayoutChange = onLayoutChange;
305
+ this.options = options;
161
306
  host.addController(this);
162
307
  }
163
- observe() {
308
+ get position() {
309
+ return this._position;
310
+ }
311
+ start() {
164
312
  this.wantsUpdates = true;
165
313
  this.subscribe();
314
+ this.update();
166
315
  }
167
- unobserve() {
316
+ stop() {
168
317
  this.wantsUpdates = false;
169
- this.clear();
318
+ this.unsubscribeAll();
319
+ this._position = undefined;
320
+ this.options.clear?.();
170
321
  }
171
322
  hostConnected() {
172
323
  if (this.wantsUpdates) {
@@ -174,17 +325,33 @@ class LayoutChangeController {
174
325
  }
175
326
  }
176
327
  hostDisconnected() {
177
- this.clear();
328
+ this.unsubscribeAll();
178
329
  }
179
330
  subscribe() {
180
331
  if (this.unsubscribe) {
181
332
  return;
182
333
  }
183
- this.unsubscribe = observeLayoutChanges(this.host, this.onLayoutChange);
334
+ const stopLayout = observeLayoutChanges(this.host, this.update);
335
+ if (!this.options.trackViewport || typeof window === "undefined") {
336
+ this.unsubscribe = stopLayout;
337
+ return;
338
+ }
339
+ const listenerOptions = { capture: true, passive: true };
340
+ window.addEventListener("scroll", this.scheduleUpdate, listenerOptions);
341
+ window.addEventListener("resize", this.scheduleUpdate, listenerOptions);
342
+ this.unsubscribe = () => {
343
+ stopLayout();
344
+ window.removeEventListener("scroll", this.scheduleUpdate, true);
345
+ window.removeEventListener("resize", this.scheduleUpdate, true);
346
+ };
184
347
  }
185
- clear() {
348
+ unsubscribeAll() {
186
349
  this.unsubscribe?.();
187
350
  this.unsubscribe = undefined;
351
+ if (this.frame !== undefined) {
352
+ cancelAnimationFrame(this.frame);
353
+ this.frame = undefined;
354
+ }
188
355
  }
189
356
  }
190
357
 
@@ -1554,7 +1721,6 @@ var overviewClasses = () => clsx_default("col-span-full", "inline-flex", "w-full
1554
1721
  var navLinkDropdownId = 0;
1555
1722
  var dropdownShiftProperty = "--dsgn-nav-link-dropdown-shift";
1556
1723
  var supportsHover = () => typeof window === "undefined" || typeof window.matchMedia !== "function" ? true : window.matchMedia("(hover: hover)").matches;
1557
- var dropdownViewportGutter = 16;
1558
1724
 
1559
1725
  class NavLink extends Base {
1560
1726
  constructor() {
@@ -1575,7 +1741,22 @@ class NavLink extends Base {
1575
1741
  this.keyboardDismissed = false;
1576
1742
  this.dropdownId = `dsgn-nav-link-dropdown-${++navLinkDropdownId}`;
1577
1743
  this.breakpointController = new BreakpointController(this);
1578
- this.layoutChangeController = new LayoutChangeController(this, () => this.updateDropdownAlignment());
1744
+ this.overlayPosition = new OverlayPositionController(this, {
1745
+ overlay: () => this.shadowRoot?.querySelector(`#${this.dropdownId}`),
1746
+ apply: ({ shift }) => this.style.setProperty(dropdownShiftProperty, `${shift}px`),
1747
+ clear: () => this.style.removeProperty(dropdownShiftProperty)
1748
+ });
1749
+ this.overlayDismiss = new OverlayDismissController(this, {
1750
+ isOpen: () => this.expanded && this.hasDropdown,
1751
+ onDismiss: (reason) => {
1752
+ this.expanded = false;
1753
+ this.pinned = false;
1754
+ if (reason === "escape") {
1755
+ this.keyboardDismissed = true;
1756
+ }
1757
+ },
1758
+ returnFocusTo: () => this.shadowRoot?.querySelector("button, a")
1759
+ });
1579
1760
  this.handleDropdownSlotChange = () => {
1580
1761
  this.updateDropdownState();
1581
1762
  };
@@ -1585,17 +1766,11 @@ class NavLink extends Base {
1585
1766
  this.toggleDropdown = () => {
1586
1767
  this.pinned = !this.pinned;
1587
1768
  if (this.pinned) {
1588
- this.updateDropdownAlignment();
1769
+ this.overlayPosition.update();
1589
1770
  }
1590
1771
  this.keyboardDismissed = false;
1591
1772
  this.setExpanded(this.pinned);
1592
1773
  };
1593
- this.handleDocumentPointerDown = (event) => {
1594
- if (!event.composedPath().includes(this)) {
1595
- this.pinned = false;
1596
- this.expanded = false;
1597
- }
1598
- };
1599
1774
  this.handlePointerEnter = () => {
1600
1775
  if (!this.hoverOpensDropdown) {
1601
1776
  return;
@@ -1603,7 +1778,7 @@ class NavLink extends Base {
1603
1778
  this.keyboardDismissed = false;
1604
1779
  this.updateDropdownState();
1605
1780
  if (this.hasDropdown) {
1606
- this.updateDropdownAlignment();
1781
+ this.overlayPosition.update();
1607
1782
  this.setExpanded(true);
1608
1783
  }
1609
1784
  };
@@ -1619,7 +1794,7 @@ class NavLink extends Base {
1619
1794
  }
1620
1795
  this.updateDropdownState();
1621
1796
  if (this.hasDropdown) {
1622
- this.updateDropdownAlignment();
1797
+ this.overlayPosition.update();
1623
1798
  this.setExpanded(true);
1624
1799
  }
1625
1800
  };
@@ -1635,12 +1810,7 @@ class NavLink extends Base {
1635
1810
  });
1636
1811
  };
1637
1812
  this.handleKeydown = (event) => {
1638
- if (event.key === "Escape" && this.expanded && this.hasDropdown) {
1639
- this.expanded = false;
1640
- this.pinned = false;
1641
- this.keyboardDismissed = true;
1642
- this.shadowRoot?.querySelector("button, a")?.focus();
1643
- }
1813
+ this.overlayDismiss.handleKeydown(event);
1644
1814
  };
1645
1815
  }
1646
1816
  static styles = [Base.styles, NavLink_default];
@@ -1663,26 +1833,21 @@ class NavLink extends Base {
1663
1833
  tokens.add("noreferrer");
1664
1834
  return Array.from(tokens).join(" ");
1665
1835
  }
1666
- disconnectedCallback() {
1667
- document.removeEventListener("pointerdown", this.handleDocumentPointerDown);
1668
- super.disconnectedCallback();
1669
- }
1670
1836
  updated(changedProperties) {
1671
1837
  super.updated(changedProperties);
1672
1838
  this.updateTheme(changedProperties, this.surface);
1673
1839
  if (changedProperties.has("pinned")) {
1674
1840
  if (this.pinned) {
1675
- document.addEventListener("pointerdown", this.handleDocumentPointerDown);
1841
+ this.overlayDismiss.start();
1676
1842
  } else {
1677
- document.removeEventListener("pointerdown", this.handleDocumentPointerDown);
1843
+ this.overlayDismiss.stop();
1678
1844
  }
1679
1845
  }
1680
1846
  if (changedProperties.has("expanded")) {
1681
1847
  if (this.expanded) {
1682
- this.layoutChangeController.observe();
1848
+ this.overlayPosition.start();
1683
1849
  } else {
1684
- this.layoutChangeController.unobserve();
1685
- this.clearDropdownAlignment();
1850
+ this.overlayPosition.stop();
1686
1851
  }
1687
1852
  }
1688
1853
  }
@@ -1729,35 +1894,6 @@ class NavLink extends Base {
1729
1894
  this.expanded = open;
1730
1895
  this.emit("dsgn-nav-link-toggle", { open });
1731
1896
  }
1732
- clearDropdownAlignment() {
1733
- this.style.removeProperty(dropdownShiftProperty);
1734
- }
1735
- measureDropdownWidth() {
1736
- const panel = this.shadowRoot?.querySelector(`#${this.dropdownId}`);
1737
- if (!panel) {
1738
- return 0;
1739
- }
1740
- const openWidth = panel.getBoundingClientRect().width;
1741
- if (openWidth) {
1742
- return openWidth;
1743
- }
1744
- panel.style.cssText = "display:block;visibility:hidden";
1745
- const width = panel.getBoundingClientRect().width;
1746
- panel.style.cssText = "";
1747
- return width;
1748
- }
1749
- dropdownShift(triggerRect) {
1750
- const panelWidth = this.measureDropdownWidth();
1751
- const viewportRight = window.innerWidth - dropdownViewportGutter;
1752
- if (!panelWidth || triggerRect.left + panelWidth <= viewportRight) {
1753
- return 0;
1754
- }
1755
- const flipped = triggerRect.width - panelWidth;
1756
- return triggerRect.left + flipped >= dropdownViewportGutter ? flipped : dropdownViewportGutter - triggerRect.left;
1757
- }
1758
- updateDropdownAlignment() {
1759
- this.style.setProperty(dropdownShiftProperty, `${this.dropdownShift(this.getBoundingClientRect())}px`);
1760
- }
1761
1897
  get hoverOpensDropdown() {
1762
1898
  return this.breakpointController.isAtLeast("sm") && supportsHover();
1763
1899
  }
@@ -0,0 +1,157 @@
1
+ import '../../../types/global.d.ts';
2
+ import { type CSSResultGroup } from 'lit';
3
+ import { Base } from '../../../common/Base/Base.js';
4
+ import { type SurfaceType } from '../../../common/props/surface/surface.props.js';
5
+ import { type SelectOption, type SelectSizeType } from './Select.props.js';
6
+ /**
7
+ * Form-associated single-value select, following the WAI-ARIA APG select-only
8
+ * combobox: a `<button role="combobox">` trigger that keeps DOM focus while
9
+ * the open `role="listbox"` is navigated through `aria-activedescendant`.
10
+ *
11
+ * Renders its own label, hint and error inside the shadow root, next to the
12
+ * trigger they describe (error read before hint). Inside a `<dsgn-field>`,
13
+ * Field supplies `label`, `hint`, `error`, `required`, `invalid` and `size`;
14
+ * see `common/helpers/field-control.ts`.
15
+ *
16
+ * The listbox is positioned against the trigger and the viewport: below by
17
+ * default, flipped above when there isn't room, and shifted to keep a 16px
18
+ * viewport gutter. It opens in the top layer (`popover="manual"`), so an
19
+ * ancestor's `overflow` or stacking context cannot clip or cover it; where the
20
+ * Popover API is missing it falls back to absolute positioning inside the
21
+ * component. Dismissal stays with the component (not popover light-dismiss),
22
+ * because focus never leaves the trigger.
23
+ *
24
+ * @fires dsgn-change - Fired when the user picks a different option; `detail` is `{ value, name }`. Not fired when the same option is picked again, or when `value` is set programmatically.
25
+ *
26
+ * @csspart label - The visible `<label>`.
27
+ * @csspart required-indicator - The `*` appended to the label when `required`. Hidden from assistive technology; `aria-required` on the trigger is what is announced.
28
+ * @csspart trigger - The combobox button.
29
+ * @csspart listbox - The floating option list.
30
+ * @csspart option - Each option in the list.
31
+ * @csspart hint - Persistent hint text below the trigger.
32
+ * @csspart error - Error message, icon and text, below the hint.
33
+ */
34
+ export declare class Select extends Base {
35
+ #private;
36
+ static styles: CSSResultGroup;
37
+ static formAssociated: boolean;
38
+ /**
39
+ * `focus()` on the host (and a click on a `<label>` pointing at it) moves
40
+ * focus to the trigger.
41
+ */
42
+ static shadowRootOptions: ShadowRootInit;
43
+ constructor();
44
+ surface: SurfaceType;
45
+ size: SelectSizeType;
46
+ /**
47
+ * The choices, in order. Set as a property, or as a JSON attribute in
48
+ * plain HTML.
49
+ */
50
+ options: SelectOption[];
51
+ /**
52
+ * The selected option's `value`, or `''` for none. Not reflected, matching
53
+ * `dsgn-input`: the value at first render is the default `form.reset()`
54
+ * returns to.
55
+ */
56
+ value: string;
57
+ /**
58
+ * Field name under which the value is submitted. Reflected because the
59
+ * browser builds a form's entry list from the `name` content attribute.
60
+ */
61
+ name: string;
62
+ /** Shown in the trigger while no option is selected. */
63
+ placeholder: string;
64
+ label: string;
65
+ /**
66
+ * Accessible name for a select with no visible `label`. Ignored when `label`
67
+ * is set, so the visible text and the announced name cannot disagree.
68
+ */
69
+ ariaLabel: string;
70
+ /** Supporting text below the trigger. Stays visible alongside an error. */
71
+ hint: string;
72
+ /** Error message. A non-empty value also marks the select invalid. */
73
+ error: string;
74
+ /** Marks the select invalid without a message. */
75
+ invalid: boolean;
76
+ disabled: boolean;
77
+ required: boolean;
78
+ /**
79
+ * Disabled state inherited from an ancestor `<fieldset>`, kept apart from
80
+ * the authored `disabled` so re-enabling the fieldset is never stuck (the
81
+ * LL-130 fix; see `dsgn-input`).
82
+ */
83
+ private _formDisabled;
84
+ /** Whether the listbox is open (drives `aria-expanded`). */
85
+ private _open;
86
+ /** Whether the listbox is displayed — stays true through the exit animation. */
87
+ private _visible;
88
+ /** Index of the highlighted option, or -1. */
89
+ private _activeIndex;
90
+ private _placement;
91
+ private _trigger;
92
+ private _listbox;
93
+ /** The value `form.reset()` restores, captured at first render. */
94
+ private _defaultValue;
95
+ private readonly _typeahead;
96
+ private readonly _motion;
97
+ /** Invalidates a pending exit when the listbox reopens. */
98
+ private _exitRun;
99
+ /** Fixed at construction: the listbox's rendering mode never changes. */
100
+ private readonly _topLayer;
101
+ /** Whether this component has the listbox showing in the top layer. */
102
+ private _inTopLayer;
103
+ private _listboxScrollTop;
104
+ private readonly _position;
105
+ private readonly _dismiss;
106
+ private get _isDisabled();
107
+ private get _isInvalid();
108
+ private get _selectedIndex();
109
+ private get _state();
110
+ /** The selected option if it is enabled, otherwise the first enabled one. */
111
+ private get _openingIndex();
112
+ private _openAt;
113
+ private _close;
114
+ private _pick;
115
+ /** Space types into an active typeahead search instead of opening/picking. */
116
+ private _isTypeahead;
117
+ private _search;
118
+ /** Returns whether the key was handled (and its default should be blocked). */
119
+ private _handleClosedKey;
120
+ private _handleOpenKey;
121
+ private _handleKeydown;
122
+ /** Firefox activates a button on Space keyup; the keydown already acted. */
123
+ private _handleKeyup;
124
+ private _handleTriggerClick;
125
+ private _handleOptionClick;
126
+ private _handleOptionEnter;
127
+ /** Keeps focus on the trigger when the listbox is pressed. */
128
+ private _keepFocus;
129
+ private _handleBlur;
130
+ private _syncValidity;
131
+ formResetCallback(): void;
132
+ formDisabledCallback(disabled: boolean): void;
133
+ formStateRestoreCallback(restored: string | File | FormData | null): void;
134
+ firstUpdated(): void;
135
+ disconnectedCallback(): void;
136
+ willUpdate(changedProperties: Map<string | number | symbol, unknown>): void;
137
+ updated(changedProperties: Map<string | number | symbol, unknown>): void;
138
+ private _showListbox;
139
+ private _hideListbox;
140
+ private _handleOpenChange;
141
+ private _scrollActiveIntoView;
142
+ private _renderLabel;
143
+ private _renderHint;
144
+ private _renderError;
145
+ private _renderOption;
146
+ /** Name sources shared by the trigger and the listbox. */
147
+ private get _labelledBy();
148
+ private get _fallbackName();
149
+ private _renderTrigger;
150
+ private _renderListbox;
151
+ render(): import("lit").TemplateResult<1>;
152
+ }
153
+ declare global {
154
+ interface HTMLElementTagNameMap {
155
+ 'dsgn-select': Select;
156
+ }
157
+ }