@fusion.dev/helix-core 0.143.0 → 0.145.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 (107) hide show
  1. package/custom-elements.json +10266 -4161
  2. package/dist/controllers/aria-forward-controller/aria-forward-controller.d.ts +10 -0
  3. package/dist/controllers/aria-forward-controller/aria-forward-controller.js +6 -0
  4. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +89 -0
  5. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +102 -1
  6. package/dist/controllers/chart-pointer-controller/reading-step.d.ts +57 -0
  7. package/dist/controllers/chart-pointer-controller/reading-step.js +45 -0
  8. package/dist/controllers/copy-controller/copy-controller.d.ts +11 -6
  9. package/dist/controllers/copy-controller/copy-controller.js +11 -9
  10. package/dist/controllers/countdown-controller/countdown-controller.d.ts +55 -0
  11. package/dist/controllers/countdown-controller/countdown-controller.js +57 -0
  12. package/dist/controllers/delay-controller/delay-controller.d.ts +50 -0
  13. package/dist/controllers/delay-controller/delay-controller.js +54 -0
  14. package/dist/controllers/focus-scope-controller/focus-scope-controller.d.ts +56 -0
  15. package/dist/controllers/focus-scope-controller/focus-scope-controller.js +71 -0
  16. package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.d.ts +11 -2
  17. package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.js +4 -1
  18. package/dist/controllers/open-phase-controller/open-phase-controller.d.ts +49 -0
  19. package/dist/controllers/open-phase-controller/open-phase-controller.js +41 -0
  20. package/dist/controllers/source-controller/source-controller.d.ts +16 -0
  21. package/dist/controllers/source-controller/source-controller.js +44 -1
  22. package/dist/controllers/status-controller/status-controller.d.ts +47 -0
  23. package/dist/controllers/status-controller/status-controller.js +34 -0
  24. package/dist/controllers/swipe-controller/swipe-controller.d.ts +61 -0
  25. package/dist/controllers/swipe-controller/swipe-controller.js +106 -0
  26. package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +68 -35
  27. package/dist/controllers/virtualize-controller/virtualize-controller.js +77 -10
  28. package/dist/events/events.d.ts +221 -0
  29. package/dist/events/events.js +150 -0
  30. package/dist/helix-element/helix-element.js +14 -1
  31. package/dist/helix-element/react-bridge.d.ts +9 -4
  32. package/dist/helix-element/react-bridge.js +14 -1
  33. package/dist/index.d.ts +31 -9
  34. package/dist/index.js +29 -7
  35. package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +7 -2
  36. package/dist/mixins/mixin-chart-source/mixin-chart-source.js +5 -1
  37. package/dist/mixins/mixin-controller-checked/mixin-controller-checked.d.ts +10 -0
  38. package/dist/mixins/mixin-controller-checked/mixin-controller-checked.js +9 -0
  39. package/dist/mixins/mixin-dismissible/mixin-dismissible.d.ts +2 -0
  40. package/dist/mixins/mixin-dismissible/mixin-dismissible.js +7 -3
  41. package/dist/mixins/mixin-field/mixin-field.d.ts +2 -0
  42. package/dist/mixins/mixin-field/mixin-field.js +8 -0
  43. package/dist/mixins/mixin-picker-editing/mixin-picker-editing.d.ts +16 -6
  44. package/dist/mixins/mixin-picker-editing/mixin-picker-editing.js +19 -1
  45. package/dist/mixins/mixin-picker-field/mixin-picker-field.d.ts +5 -4
  46. package/dist/mixins/mixin-picker-field/mixin-picker-field.js +12 -1
  47. package/dist/mixins/mixin-segmented-field/mixin-segmented-field.d.ts +67 -0
  48. package/dist/mixins/mixin-segmented-field/mixin-segmented-field.js +243 -0
  49. package/dist/utils/arrow/arrow.d.ts +15 -0
  50. package/dist/utils/arrow/arrow.js +2 -0
  51. package/dist/utils/audit-name/audit-name.d.ts +28 -0
  52. package/dist/utils/audit-name/audit-name.js +95 -0
  53. package/dist/utils/calendar/calendar.d.ts +117 -0
  54. package/dist/utils/calendar/calendar.js +96 -0
  55. package/dist/utils/chord/chord.d.ts +43 -0
  56. package/dist/utils/chord/chord.js +53 -0
  57. package/dist/utils/clock/clock.d.ts +87 -0
  58. package/dist/utils/clock/clock.js +68 -0
  59. package/dist/utils/color-name/color-name.d.ts +28 -0
  60. package/dist/utils/color-name/color-name.js +150 -0
  61. package/dist/utils/count-label/count-label.d.ts +13 -0
  62. package/dist/utils/count-label/count-label.js +5 -0
  63. package/dist/utils/css-color/css-color.d.ts +12 -0
  64. package/dist/utils/css-color/css-color.js +2 -2
  65. package/dist/utils/date-segments/date-segments.d.ts +122 -0
  66. package/dist/utils/date-segments/date-segments.js +206 -0
  67. package/dist/utils/draw-dismiss/draw-dismiss.d.ts +30 -0
  68. package/dist/utils/draw-dismiss/draw-dismiss.js +16 -0
  69. package/dist/utils/draw-icon/draw-icon.d.ts +7 -6
  70. package/dist/utils/draw-tooltip/draw-tooltip.d.ts +15 -21
  71. package/dist/utils/draw-tooltip/draw-tooltip.js +11 -5
  72. package/dist/utils/draw-tooltip/spoken-reading.d.ts +10 -0
  73. package/dist/utils/draw-tooltip/spoken-reading.js +10 -0
  74. package/dist/utils/follow-link/follow-link.d.ts +46 -0
  75. package/dist/utils/follow-link/follow-link.js +37 -0
  76. package/dist/utils/heading-in/heading-in.d.ts +19 -0
  77. package/dist/utils/heading-in/heading-in.js +14 -0
  78. package/dist/utils/number-format/number-format.d.ts +23 -0
  79. package/dist/utils/number-format/number-format.js +55 -0
  80. package/dist/utils/platform/platform.d.ts +23 -0
  81. package/dist/utils/platform/platform.js +11 -0
  82. package/dist/utils/range-add/range-add.d.ts +19 -0
  83. package/dist/utils/range-add/range-add.js +5 -0
  84. package/dist/utils/read-color/read-color.d.ts +14 -0
  85. package/dist/utils/read-color/read-color.js +13 -0
  86. package/dist/utils/read-series/read-series.d.ts +6 -0
  87. package/dist/utils/read-series/read-series.js +1 -0
  88. package/dist/utils/reflect-elements/reflect-elements.d.ts +37 -0
  89. package/dist/utils/reflect-elements/reflect-elements.js +40 -0
  90. package/dist/utils/relative-time/relative-time.d.ts +13 -1
  91. package/dist/utils/relative-time/relative-time.js +7 -4
  92. package/dist/utils/report-runtime/report-runtime.d.ts +5 -1
  93. package/dist/utils/roving-stop/roving-stop.d.ts +20 -0
  94. package/dist/utils/roving-stop/roving-stop.js +3 -0
  95. package/dist/utils/swipe/swipe.d.ts +63 -0
  96. package/dist/utils/swipe/swipe.js +48 -0
  97. package/dist/utils/tab-stops/tab-stops.d.ts +16 -0
  98. package/dist/utils/tab-stops/tab-stops.js +22 -0
  99. package/dist/utils/value-as/value-as.d.ts +20 -0
  100. package/dist/utils/value-as/value-as.js +16 -0
  101. package/dist/utils/write-time/write-time.d.ts +15 -5
  102. package/dist/utils/write-time/write-time.js +7 -7
  103. package/dist/utils/written-list/written-list.d.ts +18 -0
  104. package/dist/utils/written-list/written-list.js +8 -0
  105. package/dist/version.d.ts +1 -1
  106. package/dist/version.js +1 -1
  107. package/package.json +4 -4
@@ -13,6 +13,8 @@ export declare class DismissibleElement {
13
13
  dismiss(): boolean;
14
14
  /** The event, built but not dispatched, for a caller that owns the timing. */
15
15
  protected dismissEvent(): HelixDismissEvent;
16
+ /** Where focus lands when a neighbor that held it is removed: the dismiss control. */
17
+ protected get returnFocusTo(): HTMLElement | null;
16
18
  }
17
19
  /**
18
20
  * Grafts `dismissible`, its label, and the event that says so.
@@ -21,6 +21,9 @@ export const MixinDismissible = (Base) => {
21
21
  .join('');
22
22
  return new HelixDismissEvent({ label: own.trim() });
23
23
  }
24
+ get returnFocusTo() {
25
+ return this.shadowRoot?.querySelector('[part~="dismiss"]') ?? null;
26
+ }
24
27
  dismiss() {
25
28
  const neighbors = deepContains(this, deepActiveElement()) ? this.neighbors() : [];
26
29
  const event = this.dismissEvent();
@@ -53,11 +56,12 @@ export const MixinDismissible = (Base) => {
53
56
  return;
54
57
  }
55
58
  for (const neighbor of neighbors) {
56
- const control = neighbor.isConnected &&
57
- neighbor.shadowRoot?.querySelector('[part~="dismiss"]');
59
+ const control = neighbor.isConnected && neighbor.returnFocusTo;
58
60
  if (control) {
59
61
  control.focus();
60
- return;
62
+ if (deepContains(neighbor, deepActiveElement())) {
63
+ return;
64
+ }
61
65
  }
62
66
  }
63
67
  };
@@ -20,6 +20,8 @@ export declare class MixinFieldInterface {
20
20
  protected labelId: string;
21
21
  /** Whether a label is drawn at all, in either shape. */
22
22
  protected get labeled(): boolean;
23
+ /** The element in the shadow root a reader lands on and hears the name of. */
24
+ protected get namedControl(): HTMLElement | null;
23
25
  /**
24
26
  * Help text shown under the control. A validation message replaces it.
25
27
  *
@@ -6,6 +6,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
6
6
  };
7
7
  import { html, nothing } from 'lit';
8
8
  import { property, state } from 'lit/decorators.js';
9
+ import { auditName } from '../../utils/audit-name/audit-name.js';
9
10
  import { hostAria } from '../../utils/host-aria/host-aria.js';
10
11
  const HINT_ID = 'helix-hint';
11
12
  const FEEDBACK_ID = 'helix-feedback';
@@ -36,6 +37,11 @@ export const MixinField = (Base) => {
36
37
  this.touched = false;
37
38
  this.shownFeedback = '';
38
39
  }
40
+ get namedControl() {
41
+ return (this.renderRoot?.querySelector(`#${this.controlId}`) ??
42
+ this.renderRoot?.querySelector('input, button, select, textarea') ??
43
+ null);
44
+ }
39
45
  get labeled() {
40
46
  return (this.label !== '' ||
41
47
  Boolean(this.getAttribute('aria-label')) ||
@@ -160,6 +166,8 @@ export const MixinField = (Base) => {
160
166
  this.addEventListener('invalid', touch);
161
167
  this.addEventListener('change', touch);
162
168
  this.renderRoot.addEventListener('slotchange', this.noteSlotted);
169
+ auditName(this, () => this.namedControl, 'Give it a `label`, an `aria-label`, words in its label, or a `<label for>` that names it.', () => this.labeled ||
170
+ [...this.children].some((child) => child.slot === 'label' && (child.textContent ?? '').trim() !== ''));
163
171
  }
164
172
  }
165
173
  __decorate([
@@ -19,7 +19,10 @@ export declare class MixinPickerEditingInterface {
19
19
  protected editing: boolean;
20
20
  /** The caret has left, so the field renders again. */
21
21
  protected readonly handleBlur: () => void;
22
- /** Focusing the field opens the picker, and only from outside the element. */
22
+ /**
23
+ * A press that focuses the field opens the picker, and only from outside the
24
+ * element. Focus that arrives by Tab or from a script opens nothing.
25
+ */
23
26
  protected readonly handleFocus: (event: FocusEvent) => void;
24
27
  /**
25
28
  * Whether the value is locked. A read-only control still submits its value.
@@ -37,8 +40,8 @@ export declare class MixinPickerEditingInterface {
37
40
  requestOpen(next: boolean): boolean;
38
41
  }
39
42
  /**
40
- * Grafts the typing half of a picker field: it opens on arrival, holds the
41
- * value while the caret is in it, and refuses while read-only.
43
+ * Grafts the typing half of a picker field: a press into it opens the picker,
44
+ * it holds the value while the caret is in it, and it refuses while read-only.
42
45
  *
43
46
  * **Over `MixinPickerField` and not part of it, because Color Input takes the
44
47
  * one and not the other.** Date Input and Time Input show a rendering while
@@ -46,9 +49,16 @@ export declare class MixinPickerEditingInterface {
46
49
  * while somebody is, because `Intl` formats and does not parse and a document
47
50
  * being edited must not be a rendering of something else. That split needs to
48
51
  * know when the caret is in the field, which is `editing`; and a reader who
49
- * reaches such a field is choosing, so arriving opens the picker. A color has
50
- * neither: `#ff0000` is what is typed and what is shown, and a reader who has
51
- * the hex pastes it and never opens anything. `readOnly` comes along here for
52
+ * presses into such a field is choosing, so the press opens the picker. A color
53
+ * has neither: `#ff0000` is what is typed and what is shown, and a reader who
54
+ * has the hex pastes it and never opens anything.
55
+ *
56
+ * **Tab into the field opens nothing.** It did, and a reader moving through a
57
+ * form with a screen reader heard a dialog appear at every date and every
58
+ * time they passed. React Aria's DatePicker opens on its button alone and the
59
+ * APG date picker combobox opens on its button or Down; here the trigger and
60
+ * ArrowDown open it from the keyboard, and a press into the text opens it for
61
+ * a pointer, which is where the reader has already shown what they came for. `readOnly` comes along here for
52
62
  * the two that take this mixin; Color Input declares its own, the same
53
63
  * property refusing the same two requests.
54
64
  */
@@ -14,10 +14,16 @@ export const MixinPickerEditing = (Base) => {
14
14
  this.handleBlur = () => {
15
15
  this.editing = false;
16
16
  };
17
+ this.pressing = false;
18
+ this.handlePress = (event) => {
19
+ this.pressing = event.type === 'pointerdown' && event.composedPath()[0] === this.control;
20
+ };
17
21
  this.handleFocus = (event) => {
18
22
  this.editing = !this.returning;
23
+ const pressed = this.pressing;
24
+ this.pressing = false;
19
25
  const from = event.relatedTarget;
20
- if (this.open || this.isDisabled || this.returning) {
26
+ if (!pressed || this.open || this.isDisabled || this.returning) {
21
27
  return;
22
28
  }
23
29
  if (from instanceof Node && this.renderRoot?.contains?.(from)) {
@@ -38,6 +44,18 @@ export const MixinPickerEditing = (Base) => {
38
44
  }
39
45
  return super.requestOpen(next);
40
46
  }
47
+ connectedCallback() {
48
+ super.connectedCallback();
49
+ this.addEventListener('pointerdown', this.handlePress);
50
+ this.addEventListener('pointercancel', this.handlePress);
51
+ this.addEventListener('click', this.handlePress);
52
+ }
53
+ disconnectedCallback() {
54
+ super.disconnectedCallback();
55
+ this.removeEventListener('pointerdown', this.handlePress);
56
+ this.removeEventListener('pointercancel', this.handlePress);
57
+ this.removeEventListener('click', this.handlePress);
58
+ }
41
59
  }
42
60
  __decorate([
43
61
  property({ attribute: 'readonly', reflect: true, type: Boolean })
@@ -125,10 +125,11 @@ export declare class MixinPickerFieldInterface {
125
125
  * same way, and for the same reason: a mixin reads a part out of whatever host
126
126
  * applies it.
127
127
  *
128
- * **Opening on focus, `readOnly` and the idle rendering are not here.** They
129
- * are `MixinPickerEditing`'s, because Color Input takes neither of the first
130
- * and last: a hex is pasted and read at a glance, so its field neither renders
131
- * nor opens on arrival. Color Input declares its own `readOnly`.
128
+ * **Opening on a press into the text, `readOnly` and the idle rendering are
129
+ * not here.** They are `MixinPickerEditing`'s, because Color Input takes
130
+ * neither of the first and last: a hex is pasted and read at a glance, so its
131
+ * field neither renders nor opens when pressed. Color Input declares its own
132
+ * `readOnly`.
132
133
  *
133
134
  * **Three things the two typed families share stay with them.** `renderTrigger`
134
135
  * differs only by its icon and would still be the wrong thing to lift: a part a
@@ -7,6 +7,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { property } from 'lit/decorators.js';
8
8
  import { PopoverController } from '../../controllers/popover-controller/popover-controller.js';
9
9
  import { reportValues } from '../../utils/report-values/report-values.js';
10
+ import { tabStops } from '../../utils/tab-stops/tab-stops.js';
10
11
  export const MixinPickerField = (Base) => {
11
12
  class PickerFieldElement extends Base {
12
13
  constructor() {
@@ -51,13 +52,23 @@ export const MixinPickerField = (Base) => {
51
52
  };
52
53
  this.handleSurfaceKeydown = (event) => {
53
54
  const surface = this.partNamed('surface');
54
- if (event.key !== 'Escape' ||
55
+ if ((event.key !== 'Escape' && event.key !== 'Tab') ||
55
56
  !this.open ||
56
57
  event.defaultPrevented ||
57
58
  !surface ||
58
59
  !event.composedPath().includes(surface)) {
59
60
  return;
60
61
  }
62
+ if (event.key === 'Tab') {
63
+ const stops = tabStops(surface);
64
+ const at = event.composedPath()[0];
65
+ const edge = event.shiftKey ? stops[0] : stops.at(-1);
66
+ if (edge !== undefined && at === edge) {
67
+ event.preventDefault();
68
+ (event.shiftKey ? stops.at(-1) : stops[0])?.focus();
69
+ }
70
+ return;
71
+ }
61
72
  event.stopPropagation();
62
73
  event.preventDefault();
63
74
  this.requestOpen(false);
@@ -0,0 +1,67 @@
1
+ import { type TemplateResult } from 'lit';
2
+ import type { HelixFormElement } from '../../helix-form-element/helix-form-element.js';
3
+ import { type SegmentedKind, type SegmentOptions } from '../../utils/date-segments/date-segments.js';
4
+ import type { MixinControllerOpenInterface } from '../mixin-controller-open/mixin-controller-open.js';
5
+ import type { MixinControllerValueInterface } from '../mixin-controller-value/mixin-controller-value.js';
6
+ import type { MixinFieldInterface } from '../mixin-field/mixin-field.js';
7
+ import type { MixinPickerFieldInterface } from '../mixin-picker-field/mixin-picker-field.js';
8
+ type Constructor<T> = new (...args: any[]) => T;
9
+ /** What this is applied over: a picker field, which `MixinPickerField` makes. */
10
+ type SegmentedBase = HelixFormElement & MixinControllerOpenInterface & MixinControllerValueInterface & MixinFieldInterface & MixinPickerFieldInterface;
11
+ /**
12
+ * The public surface `MixinSegmentedField` adds.
13
+ *
14
+ * Declared separately because TypeScript cannot emit declarations for an
15
+ * anonymous class that has private members (TS4094).
16
+ */
17
+ export declare class MixinSegmentedFieldInterface {
18
+ /** What a screen reader hears for a segment with nothing in it. */
19
+ emptySegmentLabel: string;
20
+ /**
21
+ * Whether the field is a row of segments, one for each part of the date or
22
+ * time, in place of a line of text.
23
+ */
24
+ segmented: boolean;
25
+ /** Whether the segments are drawn: `segmented`, where the family can hold them. */
26
+ protected get segmentsShown(): boolean;
27
+ /** The row of segments, the group a description is carried to, or `null` while none is drawn. */
28
+ protected get segmentGroup(): HTMLElement | null;
29
+ /** Whether this family can show segments in its current shape. */
30
+ protected get segmentable(): boolean;
31
+ /** A date or a time. */
32
+ protected get segmentedKind(): SegmentedKind;
33
+ /** The locale the segments are laid out and named in. */
34
+ protected get segmentLocale(): string;
35
+ /** The clock and the seconds a time is laid out with. */
36
+ protected get segmentOptions(): SegmentOptions;
37
+ /** The row of segments, in the locale's order, with what it writes between them. */
38
+ protected renderSegments(): TemplateResult;
39
+ }
40
+ /**
41
+ * Grafts the segmented field React Aria's DateField and TimeField are, and Web
42
+ * Awesome's time input has, onto a picker field: one `spinbutton` for each part
43
+ * of the date or the time, in the order the locale writes them.
44
+ *
45
+ * **Typing into a text field asks the reader to know the format.** Date Input
46
+ * and Time Input read ISO, `2026-08-04` and `14:30`, because `Intl` formats and
47
+ * does not parse, and a reader in Germany who types `4.8.2026` is refused. A
48
+ * segment needs no format: the month is a month, set with digits or the arrow
49
+ * keys, and the layout puts it where the locale does, so the field reads
50
+ * `04.08.2026` in German and `08/04/2026` in English with nothing to parse.
51
+ *
52
+ * **What a segment does with a key**, which is React Aria's: digits fill it and
53
+ * move on once it cannot take another, Up and Down step it and wrap, Home and
54
+ * End go to its ends, Backspace and Delete empty it, Left and Right move between
55
+ * segments along the line of text, and Alt+Down opens the picker. Each segment
56
+ * is a tab stop, as each is in React Aria.
57
+ *
58
+ * **The value is whole or empty.** A date with no year is no date, so the value
59
+ * stays empty while any segment is, and the field reports `badInput` as a
60
+ * native date input does with a half-typed date. The arithmetic is
61
+ * `date-segments`, so this draws and listens and nothing else.
62
+ *
63
+ * **Opt-in, with `segmented`**, because the text field is what every example and
64
+ * every form already holds, and a row of segments draws differently.
65
+ */
66
+ export declare const MixinSegmentedField: <T extends Constructor<SegmentedBase>>(Base: T) => Constructor<MixinSegmentedFieldInterface> & T;
67
+ export {};
@@ -0,0 +1,243 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { html, nothing } from 'lit';
8
+ import { property, state } from 'lit/decorators.js';
9
+ import { isPartial, isTwelveHourLayout, segmentKey, segmentLayout, segmentName, segmentRange, segmentsOf, segmentsValue, segmentText, segmentValueText, shownValue, } from '../../utils/date-segments/date-segments.js';
10
+ import { inlineStep } from '../../utils/inline-step/inline-step.js';
11
+ export const MixinSegmentedField = (Base) => {
12
+ class SegmentedFieldElement extends Base {
13
+ constructor() {
14
+ super(...arguments);
15
+ this.emptySegmentLabel = 'Empty';
16
+ this.segmented = false;
17
+ this.parts = {};
18
+ this.typed = '';
19
+ this.own = false;
20
+ this.entered = undefined;
21
+ this.handleSegmentKeydown = (event) => {
22
+ const element = event.currentTarget;
23
+ const kind = element.dataset.segment;
24
+ const twelve = isTwelveHourLayout(this.layout);
25
+ if (event.key === 'ArrowDown' && event.altKey) {
26
+ event.preventDefault();
27
+ this.handleTriggerClick();
28
+ return;
29
+ }
30
+ const along = inlineStep(this, event.key);
31
+ if (along !== 0) {
32
+ event.preventDefault();
33
+ this.typed = '';
34
+ this.moveFocus(element, along);
35
+ return;
36
+ }
37
+ if (event.key === 'Tab') {
38
+ this.typed = '';
39
+ if (this.open) {
40
+ this.requestOpen(false);
41
+ }
42
+ return;
43
+ }
44
+ if (event.metaKey || event.ctrlKey || event.altKey) {
45
+ this.typed = '';
46
+ return;
47
+ }
48
+ if (this.readOnly || this.isDisabled) {
49
+ return;
50
+ }
51
+ const edit = segmentKey(event.key, {
52
+ kind,
53
+ locale: this.segmentLocale,
54
+ segments: this.parts,
55
+ twelve,
56
+ typed: this.typed,
57
+ });
58
+ if (edit) {
59
+ event.preventDefault();
60
+ this.typed = edit.typed;
61
+ this.take(edit.segments);
62
+ if (edit.advance) {
63
+ this.moveFocus(element, 1);
64
+ }
65
+ }
66
+ };
67
+ this.handleSegmentsFocusIn = (event) => {
68
+ this.typed = '';
69
+ if (!this.segmentElements.includes(event.relatedTarget)) {
70
+ this.entered = this.value;
71
+ }
72
+ };
73
+ this.handleSegmentsFocusOut = (event) => {
74
+ if (this.segmentElements.includes(event.relatedTarget)) {
75
+ return;
76
+ }
77
+ if (this.entered !== undefined && this.entered !== this.value) {
78
+ this.announceChange();
79
+ }
80
+ this.entered = undefined;
81
+ };
82
+ this.handleLabelPress = (event) => {
83
+ if (!this.segmentsShown || this.isDisabled) {
84
+ return;
85
+ }
86
+ const label = this.renderRoot?.querySelector?.('[part~="label"]');
87
+ if (label && event.composedPath().includes(label)) {
88
+ this.anchor?.focus();
89
+ }
90
+ };
91
+ }
92
+ get segmentable() {
93
+ return true;
94
+ }
95
+ get segmentsShown() {
96
+ return this.segmented && this.segmentable;
97
+ }
98
+ get segmentedKind() {
99
+ return 'date';
100
+ }
101
+ get segmentLocale() {
102
+ return '';
103
+ }
104
+ get segmentOptions() {
105
+ return {};
106
+ }
107
+ get layout() {
108
+ return segmentLayout(this.segmentedKind, this.segmentLocale, this.segmentOptions);
109
+ }
110
+ get anchor() {
111
+ if (this.segmentsShown) {
112
+ return this.renderRoot?.querySelector('[part~="segment"]') ?? null;
113
+ }
114
+ return super.anchor;
115
+ }
116
+ get segmentGroup() {
117
+ return this.renderRoot?.querySelector('[part="segments"]') ?? null;
118
+ }
119
+ get segmentElements() {
120
+ return [...(this.renderRoot?.querySelectorAll('[part~="segment"]') ?? [])];
121
+ }
122
+ take(next) {
123
+ this.parts = next;
124
+ const value = segmentsValue(this.segmentedKind, next, this.layout);
125
+ if (value === this.value) {
126
+ this.sync();
127
+ return;
128
+ }
129
+ this.own = true;
130
+ if (!this.requestValue(value)) {
131
+ this.own = false;
132
+ this.parts = segmentsOf(this.segmentedKind, this.value);
133
+ }
134
+ }
135
+ moveFocus(from, by) {
136
+ const all = this.segmentElements;
137
+ all[all.indexOf(from) + by]?.focus();
138
+ }
139
+ willUpdate(changed) {
140
+ super.willUpdate(changed);
141
+ const own = this.own;
142
+ this.own = false;
143
+ if (!own &&
144
+ this.segmentsShown &&
145
+ segmentsValue(this.segmentedKind, this.parts, this.layout) !== this.value) {
146
+ this.parts = segmentsOf(this.segmentedKind, this.value);
147
+ }
148
+ }
149
+ formResetCallback() {
150
+ super.formResetCallback();
151
+ this.parts = {};
152
+ this.typed = '';
153
+ }
154
+ connectedCallback() {
155
+ super.connectedCallback();
156
+ this.addEventListener('click', this.handleLabelPress);
157
+ }
158
+ disconnectedCallback() {
159
+ super.disconnectedCallback();
160
+ this.removeEventListener('click', this.handleLabelPress);
161
+ }
162
+ get blocksImplicitSubmission() {
163
+ return this.segmentsShown || super.blocksImplicitSubmission;
164
+ }
165
+ sync() {
166
+ super.sync();
167
+ if (!this.segmentsShown) {
168
+ return;
169
+ }
170
+ const anchor = this.anchor ?? undefined;
171
+ const partial = isPartial(this.segmentedKind, this.parts, this.layout);
172
+ const message = this.validate();
173
+ if (partial) {
174
+ this.internals.setValidity({ badInput: true }, this.badInputMessage ||
175
+ (this.segmentedKind === 'date' ? 'Enter a whole date.' : 'Enter a whole time.'), anchor);
176
+ }
177
+ else if (this.required && this.value === '') {
178
+ this.internals.setValidity({ valueMissing: true }, this.valueMissingMessage ||
179
+ (this.segmentedKind === 'date' ? 'Enter a date.' : 'Enter a time.'), anchor);
180
+ }
181
+ else if (message !== '') {
182
+ this.internals.setValidity({ badInput: true }, message, anchor);
183
+ }
184
+ else {
185
+ this.internals.setValidity({});
186
+ }
187
+ this.refreshFeedback();
188
+ }
189
+ renderSegments() {
190
+ const layout = this.layout;
191
+ const twelve = isTwelveHourLayout(layout);
192
+ const locale = this.segmentLocale;
193
+ return html `<div
194
+ part="segments"
195
+ id=${this.controlId}
196
+ role="group"
197
+ aria-labelledby=${this.labeled ? this.labelId : nothing}
198
+ aria-describedby=${this.describedBy ?? nothing}
199
+ aria-invalid=${this.invalidity ?? nothing}
200
+ @focusin=${this.handleSegmentsFocusIn}
201
+ @focusout=${this.handleSegmentsFocusOut}
202
+ @keypress=${this.handleKeypress}
203
+ >
204
+ ${layout.map((slot) => {
205
+ if ('literal' in slot) {
206
+ return html `<span part="literal" aria-hidden="true">${slot.literal}</span>`;
207
+ }
208
+ const kind = slot.kind;
209
+ const shown = shownValue(kind, this.parts, twelve);
210
+ const { max, min } = segmentRange(kind, this.parts, twelve);
211
+ const text = segmentText(kind, this.parts, twelve, locale);
212
+ const empty = shown === undefined;
213
+ return html `<span
214
+ part="segment${empty ? ' segment-placeholder' : ''}"
215
+ data-segment=${kind}
216
+ role="spinbutton"
217
+ tabindex=${this.isDisabled ? '-1' : '0'}
218
+ inputmode=${kind === 'dayPeriod' ? nothing : 'numeric'}
219
+ aria-label=${segmentName(kind, locale)}
220
+ aria-valuemin=${min}
221
+ aria-valuemax=${max}
222
+ aria-valuenow=${shown ?? nothing}
223
+ aria-valuetext=${empty ? this.emptySegmentLabel : segmentValueText(kind, this.parts, twelve, locale)}
224
+ aria-disabled=${this.isDisabled ? 'true' : nothing}
225
+ aria-readonly=${this.readOnly ? 'true' : nothing}
226
+ @keydown=${this.handleSegmentKeydown}
227
+ >${text}</span
228
+ >`;
229
+ })}
230
+ </div>`;
231
+ }
232
+ }
233
+ __decorate([
234
+ property({ attribute: 'empty-segment-label' })
235
+ ], SegmentedFieldElement.prototype, "emptySegmentLabel", void 0);
236
+ __decorate([
237
+ property({ type: Boolean })
238
+ ], SegmentedFieldElement.prototype, "segmented", void 0);
239
+ __decorate([
240
+ state()
241
+ ], SegmentedFieldElement.prototype, "parts", void 0);
242
+ return SegmentedFieldElement;
243
+ };
@@ -0,0 +1,15 @@
1
+ import type { Placement, Side } from '../../controllers/floating-controller/floating-controller.js';
2
+ /**
3
+ * How far a panel's arrow reaches past the edge it sits on, which is what the
4
+ * panel's offset adds so the gap is measured to the point.
5
+ *
6
+ * A square of side `n` turned forty-five degrees about its center reaches
7
+ * `n / sqrt(2)` from it, and the arrow is placed with its center on the
8
+ * panel's edge. Read from the element rather than assumed, because the size
9
+ * is a hook a theme may move. `offsetWidth` rather than a rect, which reports
10
+ * the turned box and would count the reach twice. Zero for no arrow, and while
11
+ * the panel is closed and unrendered, when there is nothing to place.
12
+ */
13
+ export declare const arrowReach: (arrow: HTMLElement | null | undefined) => number;
14
+ /** The side of its anchor a placement puts a panel on: `top-start` is `top`. */
15
+ export declare const sideOf: (placement: Placement) => Side;
@@ -0,0 +1,2 @@
1
+ export const arrowReach = (arrow) => (arrow?.offsetWidth ?? 0) / Math.SQRT2;
2
+ export const sideOf = (placement) => placement.split('-')[0];
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Whether an element has an accessible name, by the routes a page names a
3
+ * control: its own `aria-label`, `aria-labelledby` or `title`, a `<label>`, a
4
+ * `placeholder`, and its content where its role is named by its content.
5
+ *
6
+ * **An estimate, cheap on purpose.** It answers whether any route holds words,
7
+ * which is what the warning is about, and leaves the order the routes win in
8
+ * to the browser.
9
+ */
10
+ export declare const hasName: (element: HTMLElement) => boolean;
11
+ /**
12
+ * Warns once, through the runtime channel, when a control has no accessible
13
+ * name: none on the host, and none on the control inside it.
14
+ *
15
+ * **Read a task after the control first renders**, so a label that arrives in
16
+ * the same pass counts: slotted, written by a framework beside the control, or
17
+ * forwarded from a `<label for>`. One timer serves every control that asked in
18
+ * that task, and an element is read once in its life, so a name taken away
19
+ * later is not reported.
20
+ *
21
+ * **In every build.** Every runtime warning in the library reports through the
22
+ * same channel whatever the build, and a project that wants the console quiet
23
+ * tells `@fusion.dev/helix-diagnostics` with `core.warnings: false`.
24
+ *
25
+ * `also` answers for a name the component gave through its internals, which no
26
+ * attribute shows. `fix` is the sentence that says how to name it.
27
+ */
28
+ export declare const auditName: (host: HTMLElement, control: () => HTMLElement | null, fix: string, also?: () => boolean) => void;
@@ -0,0 +1,95 @@
1
+ import { reportRuntime } from '../report-runtime/report-runtime.js';
2
+ const nameText = (node) => {
3
+ if (node instanceof HTMLSlotElement) {
4
+ return node.assignedNodes({ flatten: true }).map(nameText).join('');
5
+ }
6
+ if (node.nodeType === Node.TEXT_NODE) {
7
+ return node.textContent ?? '';
8
+ }
9
+ if (!(node instanceof Element)) {
10
+ return '';
11
+ }
12
+ if ((node instanceof HTMLElement && node.hidden) || node.getAttribute('aria-hidden') === 'true') {
13
+ return '';
14
+ }
15
+ const said = node.getAttribute('aria-label') ?? node.getAttribute('alt');
16
+ if (said?.trim()) {
17
+ return said;
18
+ }
19
+ return [...node.childNodes].map(nameText).join('');
20
+ };
21
+ const namesWithWords = (ids, from) => {
22
+ const root = from.getRootNode();
23
+ return (ids ?? '')
24
+ .split(/\s+/)
25
+ .filter(Boolean)
26
+ .some((id) => {
27
+ const found = root.getElementById?.(id);
28
+ return found ? nameText(found).trim() !== '' : false;
29
+ });
30
+ };
31
+ const NAMED_BY_CONTENT = new Set([
32
+ 'button',
33
+ 'checkbox',
34
+ 'link',
35
+ 'menuitem',
36
+ 'menuitemcheckbox',
37
+ 'menuitemradio',
38
+ 'option',
39
+ 'radio',
40
+ 'switch',
41
+ 'tab',
42
+ ]);
43
+ export const hasName = (element) => {
44
+ for (const attribute of ['aria-label', 'title', 'placeholder']) {
45
+ if (element.getAttribute(attribute)?.trim()) {
46
+ return true;
47
+ }
48
+ }
49
+ if (namesWithWords(element.getAttribute('aria-labelledby'), element)) {
50
+ return true;
51
+ }
52
+ const referenced = element
53
+ .ariaLabelledByElements;
54
+ if ((referenced ?? []).some((one) => nameText(one).trim() !== '')) {
55
+ return true;
56
+ }
57
+ const labels = element.labels;
58
+ if ([...(labels ?? [])].some((label) => nameText(label).trim() !== '')) {
59
+ return true;
60
+ }
61
+ const role = element.getAttribute('role') ?? (element.localName === 'button' ? 'button' : '');
62
+ return NAMED_BY_CONTENT.has(role) && nameText(element).trim() !== '';
63
+ };
64
+ const audited = new WeakSet();
65
+ const waiting = new Map();
66
+ const readWaiting = () => {
67
+ const checks = [...waiting.values()];
68
+ waiting.clear();
69
+ for (const check of checks) {
70
+ check();
71
+ }
72
+ };
73
+ export const auditName = (host, control, fix, also) => {
74
+ if (audited.has(host) || waiting.has(host)) {
75
+ return;
76
+ }
77
+ if (waiting.size === 0) {
78
+ setTimeout(readWaiting, 0);
79
+ }
80
+ waiting.set(host, () => {
81
+ if (!host.isConnected) {
82
+ return;
83
+ }
84
+ audited.add(host);
85
+ const inner = control();
86
+ if (also?.() || hasName(host) || (inner !== null && hasName(inner))) {
87
+ return;
88
+ }
89
+ reportRuntime({
90
+ kind: 'missing-name',
91
+ message: `<${host.localName}> has no accessible name, so a screen reader announces it by its role alone. ${fix}`,
92
+ tag: host.localName,
93
+ });
94
+ });
95
+ };