@fusion.dev/helix-core 0.159.0 → 0.161.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.
@@ -7,6 +7,12 @@ export interface VirtualizeOptions {
7
7
  count: () => number;
8
8
  /** How tall one item is along the scroll axis, in pixels. */
9
9
  estimate: () => number;
10
+ /**
11
+ * The space between two items along the scroll axis, in pixels, which the
12
+ * host draws, such as a flex or grid `gap`. Each item counts it as part of
13
+ * its own size. Zero by default.
14
+ */
15
+ gap?: () => number;
10
16
  /**
11
17
  * Rows kept either side of what fits, so a scroll reveals something drawn
12
18
  * rather than something blank. Four by default.
@@ -213,6 +219,20 @@ export declare class VirtualizeController implements ReactiveController {
213
219
  * last move is out of date.
214
220
  */
215
221
  scrolling(): boolean;
222
+ /**
223
+ * Takes the offset as the box's own where the box moved it up only because
224
+ * the set below it got shorter, which the browser does to keep the offset
225
+ * inside the content, and answers where it was before.
226
+ *
227
+ * **Not a reader scrolling.** Rows at the end of the set measured shorter
228
+ * than their estimates shrink the content, and the box pulls its offset back
229
+ * to the new bottom before any `scroll` event arrives. Read as a scroll, the
230
+ * difference went into the spacer as drift and was then paid a second time:
231
+ * in Chromium, End on 2,000 rows of one to five lines left the window 292
232
+ * pixels above the last item, which stayed drawn below the box as the row
233
+ * holding focus.
234
+ */
235
+ private noteClamp;
216
236
  /** Squares the spacer and the scroll offset once the box is still. */
217
237
  private readonly rest;
218
238
  /**
@@ -264,6 +284,8 @@ export declare class VirtualizeController implements ReactiveController {
264
284
  * Reading the drawn rows once corrects the slope, and it is still a line.
265
285
  */
266
286
  private line;
287
+ /** The space the host draws between two items, which each item counts. */
288
+ private gap;
267
289
  /**
268
290
  * Reads the drawn rows' height once for the straight line, for a host that
269
291
  * offers `rows` and does not measure.
@@ -17,7 +17,11 @@ export class VirtualizeController {
17
17
  this.touching = false;
18
18
  this.uneven = false;
19
19
  this.handleScroll = () => {
20
- const top = this.options.container()?.scrollTop ?? 0;
20
+ const box = this.options.container();
21
+ if (box) {
22
+ this.noteClamp(box);
23
+ }
24
+ const top = box?.scrollTop ?? 0;
21
25
  if (top !== this.lastTop) {
22
26
  this.heading = top > this.lastTop ? 1 : -1;
23
27
  this.lastTop = top;
@@ -95,6 +99,15 @@ export class VirtualizeController {
95
99
  this.resting !== undefined ||
96
100
  (this.options.container()?.scrollTop ?? this.lastTop) !== this.lastTop);
97
101
  }
102
+ noteClamp(box) {
103
+ const was = this.lastTop;
104
+ if (!this.touching &&
105
+ box.scrollTop < was &&
106
+ box.scrollTop >= box.scrollHeight - box.clientHeight - 1) {
107
+ this.lastTop = box.scrollTop;
108
+ }
109
+ return was;
110
+ }
98
111
  square() {
99
112
  const box = this.options.container();
100
113
  if (this.drift > 0 && box) {
@@ -161,7 +174,10 @@ export class VirtualizeController {
161
174
  }
162
175
  line() {
163
176
  const estimate = Math.max(1, this.options.estimate());
164
- return this.sample?.estimate === estimate ? this.sample.size : estimate;
177
+ return this.sample?.estimate === estimate ? this.sample.size : estimate + this.gap();
178
+ }
179
+ gap() {
180
+ return Math.max(0, this.options.gap?.() ?? 0);
165
181
  }
166
182
  sampleRows() {
167
183
  const estimate = Math.max(1, this.options.estimate());
@@ -176,7 +192,7 @@ export class VirtualizeController {
176
192
  const uneven = Math.max(...seen) - Math.min(...seen) > 1;
177
193
  if (this.sample?.estimate !== estimate) {
178
194
  const was = this.line();
179
- const size = Math.max(1, seen.reduce((sum, height) => sum + height, 0) / seen.length);
195
+ const size = Math.max(1, seen.reduce((sum, height) => sum + height, 0) / seen.length) + this.gap();
180
196
  this.sample = { estimate, size };
181
197
  if (Math.abs(size - was) >= 0.5) {
182
198
  const box = this.options.container();
@@ -291,12 +307,13 @@ export class VirtualizeController {
291
307
  }
292
308
  const total = Math.max(0, this.options.count());
293
309
  const box = this.options.container();
310
+ const from = box ? this.noteClamp(box) : this.lastTop;
294
311
  const top = box ? this.offset(box) : 0;
295
312
  const anchor = this.target ?? this.heights.indexAt(top, total);
296
313
  const was = this.heights.before(anchor, total);
297
314
  let news = false;
298
315
  for (const { element, index } of rows) {
299
- if (this.heights.record(index, element.getBoundingClientRect().height)) {
316
+ if (this.heights.record(index, element.getBoundingClientRect().height + this.gap())) {
300
317
  news = true;
301
318
  }
302
319
  }
@@ -310,7 +327,7 @@ export class VirtualizeController {
310
327
  this.drift -= moved;
311
328
  }
312
329
  else {
313
- box.scrollTop += moved;
330
+ box.scrollTop = from + moved;
314
331
  this.lastTop = box.scrollTop;
315
332
  }
316
333
  }
@@ -258,6 +258,12 @@ export interface TourAskDetail {
258
258
  * that `event.target` does not.
259
259
  */
260
260
  export interface ChangeDetail {
261
+ /**
262
+ * Why the change was asked for, where a family names its causes. Video
263
+ * Playlist sends `ended`, `next`, `press`, `previous`, and `removed` on
264
+ * `helix-before-activate`.
265
+ */
266
+ cause?: string;
261
267
  /** The value after the change. */
262
268
  value: string;
263
269
  }
package/dist/index.d.ts CHANGED
@@ -46,7 +46,7 @@ export { MixinControllerValue, type MixinControllerValueInterface, } from './mix
46
46
  export { type DismissibleElement, MixinDismissible, } from './mixins/mixin-dismissible/mixin-dismissible.js';
47
47
  export { MixinField, type MixinFieldInterface } from './mixins/mixin-field/mixin-field.js';
48
48
  export { MixinPickerEditing, type MixinPickerEditingInterface, } from './mixins/mixin-picker-editing/mixin-picker-editing.js';
49
- export { MixinPickerFeedback, type MixinPickerFeedbackInterface, } from './mixins/mixin-picker-feedback/mixin-picker-feedback.js';
49
+ export { MixinPickerFeedback, type MixinPickerFeedbackInterface, type PickerFailure, } from './mixins/mixin-picker-feedback/mixin-picker-feedback.js';
50
50
  export { MixinPickerField, type MixinPickerFieldInterface, } from './mixins/mixin-picker-field/mixin-picker-field.js';
51
51
  export { MixinSegmentedField, type MixinSegmentedFieldInterface, } from './mixins/mixin-segmented-field/mixin-segmented-field.js';
52
52
  export { MixinSelectableItem, type MixinSelectableItemInterface, } from './mixins/mixin-selectable-item/mixin-selectable-item.js';
@@ -56,7 +56,7 @@ export { anchorElement } from './utils/anchor-element/anchor-element.js';
56
56
  export { arrowReach, sideOf } from './utils/arrow/arrow.js';
57
57
  export { assignSlots } from './utils/assign-slots/assign-slots.js';
58
58
  export { type DrawAutofillOptions, drawAutofill, } from './utils/autofill-select/autofill-select.js';
59
- export { addDays, addMonths, type Barring, barredBy, type CalendarView, calendarLocale, type DateCalendar, localToday, MONTH_CALENDARS, type MonthCalendar, type MonthSystem, monthOf, monthOut, monthSystem, type PeriodCell, type PeriodPage, periodCells, periodKey, periodMove, sameMonth, todayIn, weekendOf, weekStartOf, YEARS_SHOWN, yearOut, yearPageOf, } from './utils/calendar/calendar.js';
59
+ export { addDays, addMonths, type Barring, barredBy, type CalendarParts, type CalendarView, calendarLocale, type DateCalendar, localToday, MONTH_CALENDARS, type MonthCalendar, type MonthSystem, monthOf, monthOut, monthSystem, type PeriodCell, type PeriodPage, periodCells, periodKey, periodMove, sameMonth, todayIn, weekendOf, weekStartOf, YEARS_SHOWN, yearOut, yearPageOf, } from './utils/calendar/calendar.js';
60
60
  export { capturePointer } from './utils/capture-pointer/capture-pointer.js';
61
61
  export { centerInView } from './utils/center-in-view/center-in-view.js';
62
62
  export { ariaChord, type ChordPress, chordParts, matchesChord, printableChord, selectsAll, } from './utils/chord/chord.js';
@@ -72,7 +72,7 @@ export { createSheet } from './utils/create-sheet/create-sheet.js';
72
72
  export { type Color, type ColorFormat, formatColor, parseColor, toCss, toHsl, toOklch, toRgb, } from './utils/css-color/css-color.js';
73
73
  export { customElement, definedTags, duplicateRegistrations, } from './utils/custom-element/custom-element.js';
74
74
  export { type DataOption, type DataOptionItem, dataOptions, drawOptions, windowOf, } from './utils/data-options/data-options.js';
75
- export { segmentName } from './utils/date-segments/date-segments.js';
75
+ export { readWrittenDate, readWrittenTime, segmentName, } from './utils/date-segments/date-segments.js';
76
76
  export { type DrawDismissOptions, drawDismiss, drawDismissGlyph, } from './utils/draw-dismiss/draw-dismiss.js';
77
77
  export { type DrawIconOptions, drawIcon } from './utils/draw-icon/draw-icon.js';
78
78
  export { CHART_PLOT, drawTooltip, type ReadingCursor } from './utils/draw-tooltip/draw-tooltip.js';
package/dist/index.js CHANGED
@@ -72,7 +72,7 @@ export { createSheet } from './utils/create-sheet/create-sheet.js';
72
72
  export { formatColor, parseColor, toCss, toHsl, toOklch, toRgb, } from './utils/css-color/css-color.js';
73
73
  export { customElement, definedTags, duplicateRegistrations, } from './utils/custom-element/custom-element.js';
74
74
  export { dataOptions, drawOptions, windowOf, } from './utils/data-options/data-options.js';
75
- export { segmentName } from './utils/date-segments/date-segments.js';
75
+ export { readWrittenDate, readWrittenTime, segmentName, } from './utils/date-segments/date-segments.js';
76
76
  export { drawDismiss, drawDismissGlyph, } from './utils/draw-dismiss/draw-dismiss.js';
77
77
  export { drawIcon } from './utils/draw-icon/draw-icon.js';
78
78
  export { CHART_PLOT, drawTooltip } from './utils/draw-tooltip/draw-tooltip.js';
@@ -21,8 +21,12 @@ export declare class MixinControllerActiveInterface {
21
21
  * is written.
22
22
  */
23
23
  defaultActive: string;
24
- /** Requests a new current item, and returns whether the change was applied. */
25
- requestActive(next: string): boolean;
24
+ /**
25
+ * Requests a new current item, and returns whether the change was applied.
26
+ * `cause` says why, where the family names its causes, and reaches
27
+ * `helix-before-activate` as `detail.cause`.
28
+ */
29
+ requestActive(next: string, cause?: string): boolean;
26
30
  /**
27
31
  * Which item is current, given what there is to choose from.
28
32
  *
@@ -23,7 +23,7 @@ export const MixinControllerActive = (Base) => {
23
23
  this.activeState = next;
24
24
  this.requestUpdate('active', previous);
25
25
  }
26
- requestActive(next) {
26
+ requestActive(next, cause) {
27
27
  if (next === this.active) {
28
28
  return true;
29
29
  }
@@ -34,7 +34,7 @@ export const MixinControllerActive = (Base) => {
34
34
  this.activeState = value;
35
35
  this.requestUpdate('active', previous);
36
36
  },
37
- event: (value) => new HelixBeforeActivateEvent({ value }),
37
+ event: (value) => new HelixBeforeActivateEvent(cause === undefined ? { value } : { cause, value }),
38
38
  host: this,
39
39
  next,
40
40
  });
@@ -151,7 +151,8 @@ export const MixinField = (Base) => {
151
151
  super.willUpdate(changed);
152
152
  this.shownFeedback = this.resolvedFeedback;
153
153
  this.toggleState('invalid', this.shownFeedback !== '');
154
- this.internals.ariaInvalid = this.shownFeedback === '' ? null : 'true';
154
+ this.internals.ariaInvalid =
155
+ this.shownFeedback !== '' ? 'true' : this.validates ? 'false' : null;
155
156
  }
156
157
  formResetCallback() {
157
158
  ;
@@ -19,11 +19,23 @@ export declare class MixinPickerFeedbackInterface {
19
19
  * picker and says why.
20
20
  */
21
21
  invalid: boolean;
22
- /** Whether a reader is told the answer is wrong: `invalid`, or `feedback`. */
22
+ /**
23
+ * Whether a reader is told the answer is wrong: `invalid`, `feedback`, a
24
+ * held answer the picker refuses, or a missing answer once the reader has
25
+ * committed one or a form has refused the picker.
26
+ */
23
27
  protected get marked(): boolean;
28
+ /**
29
+ * What the picker's own validation finds: `missing` for a required picker
30
+ * with nothing chosen, `refused` for an answer it holds and refuses, or
31
+ * nothing. Read in `willUpdate`, so a picker answers it from its properties.
32
+ */
33
+ protected get failure(): PickerFailure | undefined;
24
34
  /** The `feedback` line, or nothing while there is none. */
25
35
  protected renderPickerFeedback(): TemplateResult | typeof nothing;
26
36
  }
37
+ /** What a picker's own validation finds, which `marked` reads. */
38
+ export type PickerFailure = 'missing' | 'refused';
27
39
  /**
28
40
  * `feedback` and `invalid` for a picker that stands on its own as a form
29
41
  * control: Color Picker, Date Picker and Time Picker.
@@ -5,7 +5,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
7
  import { html, nothing } from 'lit';
8
- import { property } from 'lit/decorators.js';
8
+ import { property, state } from 'lit/decorators.js';
9
9
  import { hostAria } from '../../utils/host-aria/host-aria.js';
10
10
  export const MixinPickerFeedback = (Base) => {
11
11
  class PickerFeedbackElement extends Base {
@@ -13,16 +13,36 @@ export const MixinPickerFeedback = (Base) => {
13
13
  super(...arguments);
14
14
  this.feedback = '';
15
15
  this.invalid = false;
16
+ this.touched = false;
16
17
  }
17
18
  get marked() {
18
- return this.invalid || this.feedback !== '';
19
+ if (this.invalid || this.feedback !== '') {
20
+ return true;
21
+ }
22
+ const failure = this.failure;
23
+ return failure === 'refused' || (failure === 'missing' && this.touched);
24
+ }
25
+ get failure() {
26
+ return undefined;
27
+ }
28
+ formResetCallback() {
29
+ super.formResetCallback();
30
+ this.touched = false;
31
+ }
32
+ firstUpdated(changed) {
33
+ super.firstUpdated(changed);
34
+ const touch = () => {
35
+ this.touched = true;
36
+ };
37
+ this.addEventListener('invalid', touch);
38
+ this.addEventListener('change', touch);
19
39
  }
20
40
  get validityFeedback() {
21
41
  return this.feedback;
22
42
  }
23
43
  willUpdate(changed) {
24
44
  super.willUpdate(changed);
25
- this.internals.ariaInvalid = this.marked ? 'true' : null;
45
+ this.internals.ariaInvalid = this.marked ? 'true' : 'false';
26
46
  this.toggleState('invalid', this.marked);
27
47
  }
28
48
  updated(changed) {
@@ -42,5 +62,8 @@ export const MixinPickerFeedback = (Base) => {
42
62
  __decorate([
43
63
  property({ reflect: true, type: Boolean })
44
64
  ], PickerFeedbackElement.prototype, "invalid", void 0);
65
+ __decorate([
66
+ state()
67
+ ], PickerFeedbackElement.prototype, "touched", void 0);
45
68
  return PickerFeedbackElement;
46
69
  };
@@ -1,5 +1,6 @@
1
1
  import { type TemplateResult } from 'lit';
2
2
  import type { HelixFormElement } from '../../helix-form-element/helix-form-element.js';
3
+ import type { MonthSystem } from '../../utils/calendar/calendar.js';
3
4
  import { type SegmentedKind, type SegmentOptions } from '../../utils/date-segments/date-segments.js';
4
5
  import type { MixinControllerOpenInterface } from '../mixin-controller-open/mixin-controller-open.js';
5
6
  import type { MixinControllerValueInterface } from '../mixin-controller-value/mixin-controller-value.js';
@@ -34,8 +35,17 @@ export declare class MixinSegmentedFieldInterface {
34
35
  * time, in place of a line of text.
35
36
  */
36
37
  segmented: boolean;
38
+ /**
39
+ * What a screen reader hears before the segments of a range's first date or
40
+ * time, after each segment's own name.
41
+ */
42
+ rangeStartLabel: string;
43
+ /** What a screen reader hears before the segments of a range's second date or time. */
44
+ rangeEndLabel: string;
37
45
  /** Whether the segments are drawn: `segmented`, where the family can hold them. */
38
46
  protected get segmentsShown(): boolean;
47
+ /** How many rows of segments the value takes: two for a range, one otherwise. */
48
+ protected get segmentGroups(): number;
39
49
  /** The row of segments, the group a description is carried to, or `null` while none is drawn. */
40
50
  protected get segmentGroup(): HTMLElement | null;
41
51
  /** Whether this family can show segments in its current shape. */
@@ -46,6 +56,8 @@ export declare class MixinSegmentedFieldInterface {
46
56
  protected get segmentLocale(): string;
47
57
  /** The clock and the seconds a time is laid out with. */
48
58
  protected get segmentOptions(): SegmentOptions;
59
+ /** The calendar a date's parts are counted in, or `undefined` for the Gregorian calendar. */
60
+ protected get segmentSystem(): MonthSystem | undefined;
49
61
  /** The row of segments, in the locale's order, with what it writes between them. */
50
62
  protected renderSegments(): TemplateResult;
51
63
  }
@@ -18,18 +18,23 @@ export const MixinSegmentedField = (Base) => {
18
18
  this.emptyMessage = '{kind, select, date {Enter a date.} other {Enter a time.}}';
19
19
  this.emptySegmentLabel = 'Empty';
20
20
  this.incompleteMessage = '{kind, select, date {Enter a whole date.} other {Enter a whole time.}}';
21
+ this.rangeEndLabel = 'End';
22
+ this.rangeStartLabel = 'Start';
21
23
  this.segmented = false;
22
- this.parts = {};
24
+ this.parts = [{}];
23
25
  this.typed = '';
24
26
  this.own = false;
25
27
  this.touchApple = appleTouch(typeof navigator === 'undefined' ? undefined : navigator);
26
28
  this.entered = undefined;
29
+ this.opener = undefined;
27
30
  this.handleSegmentKeydown = (event) => {
28
31
  const element = event.currentTarget;
29
32
  const kind = element.dataset.segment;
33
+ const row = Number(element.dataset.row ?? 0);
30
34
  const twelve = isTwelveHourLayout(this.layout);
31
35
  if (event.key === 'ArrowDown' && event.altKey) {
32
36
  event.preventDefault();
37
+ this.opener = element;
33
38
  this.handleTriggerClick();
34
39
  return;
35
40
  }
@@ -57,14 +62,15 @@ export const MixinSegmentedField = (Base) => {
57
62
  const edit = segmentKey(event.key, {
58
63
  kind,
59
64
  locale: this.segmentLocale,
60
- segments: this.parts,
65
+ segments: this.parts[row] ?? {},
66
+ system: this.segmentSystem,
61
67
  twelve,
62
68
  typed: this.typed,
63
69
  });
64
70
  if (edit) {
65
71
  event.preventDefault();
66
72
  this.typed = edit.typed;
67
- this.take(edit.segments);
73
+ this.take(row, edit.segments);
68
74
  if (edit.advance) {
69
75
  this.moveFocus(element, 1);
70
76
  }
@@ -96,12 +102,34 @@ export const MixinSegmentedField = (Base) => {
96
102
  };
97
103
  }
98
104
  static { this.words = ''; }
105
+ returnFocus() {
106
+ const opener = this.opener;
107
+ this.opener = undefined;
108
+ if (opener?.isConnected && this.segmentsShown) {
109
+ this.returning = true;
110
+ opener.focus();
111
+ this.returning = false;
112
+ return;
113
+ }
114
+ super.returnFocus();
115
+ }
99
116
  get segmentable() {
100
117
  return true;
101
118
  }
102
119
  get segmentsShown() {
103
120
  return this.segmented && this.segmentable;
104
121
  }
122
+ get segmentGroups() {
123
+ return 1;
124
+ }
125
+ joined(rows) {
126
+ const values = rows.map((one) => segmentsValue(this.segmentedKind, one, this.layout, this.segmentSystem));
127
+ return values.some((one) => one === '') ? '' : values.join(',');
128
+ }
129
+ rowsOf(value) {
130
+ const values = this.segmentGroups > 1 ? value.split(',') : [value];
131
+ return Array.from({ length: this.segmentGroups }, (_, row) => segmentsOf(this.segmentedKind, (values[row] ?? '').trim(), this.segmentSystem));
132
+ }
105
133
  get segmentedKind() {
106
134
  return 'date';
107
135
  }
@@ -111,6 +139,9 @@ export const MixinSegmentedField = (Base) => {
111
139
  get segmentOptions() {
112
140
  return {};
113
141
  }
142
+ get segmentSystem() {
143
+ return undefined;
144
+ }
114
145
  get layout() {
115
146
  return segmentLayout(this.segmentedKind, this.segmentLocale, this.segmentOptions);
116
147
  }
@@ -126,9 +157,9 @@ export const MixinSegmentedField = (Base) => {
126
157
  get segmentElements() {
127
158
  return [...(this.renderRoot?.querySelectorAll('[part~="segment"]') ?? [])];
128
159
  }
129
- take(next) {
130
- this.parts = next;
131
- const value = segmentsValue(this.segmentedKind, next, this.layout);
160
+ take(row, next) {
161
+ this.parts = this.parts.map((one, index) => (index === row ? next : one));
162
+ const value = this.joined(this.parts);
132
163
  if (value === this.value) {
133
164
  this.sync();
134
165
  return;
@@ -136,7 +167,7 @@ export const MixinSegmentedField = (Base) => {
136
167
  this.own = true;
137
168
  if (!this.requestValue(value)) {
138
169
  this.own = false;
139
- this.parts = segmentsOf(this.segmentedKind, this.value);
170
+ this.parts = this.rowsOf(this.value);
140
171
  }
141
172
  }
142
173
  moveFocus(from, by) {
@@ -149,13 +180,13 @@ export const MixinSegmentedField = (Base) => {
149
180
  this.own = false;
150
181
  if (!own &&
151
182
  this.segmentsShown &&
152
- segmentsValue(this.segmentedKind, this.parts, this.layout) !== this.value) {
153
- this.parts = segmentsOf(this.segmentedKind, this.value);
183
+ (this.parts.length !== this.segmentGroups || this.joined(this.parts) !== this.value)) {
184
+ this.parts = this.rowsOf(this.value);
154
185
  }
155
186
  }
156
187
  formResetCallback() {
157
188
  super.formResetCallback();
158
- this.parts = {};
189
+ this.parts = this.rowsOf('');
159
190
  this.typed = '';
160
191
  }
161
192
  connectedCallback() {
@@ -175,7 +206,9 @@ export const MixinSegmentedField = (Base) => {
175
206
  return;
176
207
  }
177
208
  const anchor = this.anchor ?? undefined;
178
- const partial = isPartial(this.segmentedKind, this.parts, this.layout);
209
+ const partial = this.parts.some((one) => isPartial(this.segmentedKind, one, this.layout, this.segmentSystem)) ||
210
+ (this.value === '' &&
211
+ this.parts.some((one) => Object.values(one).some((part) => part !== undefined)));
179
212
  const fault = verdictFlags(this.validate());
180
213
  if (partial) {
181
214
  this.internals.setValidity({ badInput: true }, this.badInputMessage ||
@@ -197,51 +230,67 @@ export const MixinSegmentedField = (Base) => {
197
230
  const layout = this.layout;
198
231
  const twelve = isTwelveHourLayout(layout);
199
232
  const locale = this.segmentLocale;
200
- return html `<div
201
- part="segments"
202
- id=${this.controlId}
203
- role="group"
204
- aria-labelledby=${this.labeled ? this.labelId : nothing}
205
- aria-describedby=${this.describedBy ?? nothing}
206
- aria-invalid=${this.invalidity ?? nothing}
207
- @focusin=${this.handleSegmentsFocusIn}
208
- @focusout=${this.handleSegmentsFocusOut}
209
- @keypress=${this.handleKeypress}
210
- >
211
- ${layout.map((slot) => {
212
- if ('literal' in slot) {
213
- return html `<span part="literal" aria-hidden="true">${slot.literal}</span>`;
214
- }
215
- const kind = slot.kind;
216
- const shown = shownValue(kind, this.parts, twelve);
217
- const { max, min } = segmentRange(kind, this.parts, twelve);
218
- const text = segmentText(kind, this.parts, twelve, locale);
219
- const empty = shown === undefined;
220
- const id = `${this.controlId}-${kind}`;
221
- const editable = !this.isDisabled && !this.readOnly;
222
- const spin = !this.touchApple;
223
- return html `<span
233
+ const system = this.segmentSystem;
234
+ const rows = this.segmentGroups;
235
+ const row = (index) => {
236
+ const parts = this.parts[index] ?? {};
237
+ const prefix = index === 0 ? this.controlId : `${this.controlId}-end`;
238
+ const rowName = rows > 1 ? `${prefix}-row` : '';
239
+ return html `${rows > 1
240
+ ? html `<span id=${rowName} hidden
241
+ >${index === 0 ? this.rangeStartLabel : this.rangeEndLabel}</span
242
+ >`
243
+ : nothing}${layout.map((slot) => {
244
+ if ('literal' in slot) {
245
+ return html `<span part="literal" aria-hidden="true">${slot.literal}</span>`;
246
+ }
247
+ const kind = slot.kind;
248
+ const shown = shownValue(kind, parts, twelve);
249
+ const { max, min } = segmentRange(kind, parts, twelve, system);
250
+ const text = segmentText(kind, parts, twelve, locale);
251
+ const empty = shown === undefined;
252
+ const id = `${prefix}-${kind}`;
253
+ const editable = !this.isDisabled && !this.readOnly;
254
+ const spin = !this.touchApple;
255
+ const named = [id, rowName, this.labeled ? this.labelId : ''].filter(Boolean).join(' ');
256
+ return html `<span
224
257
  part="segment${empty ? ' segment-placeholder' : ''}"
225
258
  id=${id}
259
+ data-row=${rows > 1 ? index : nothing}
226
260
  data-segment=${kind}
227
261
  role=${spin ? 'spinbutton' : 'textbox'}
228
262
  tabindex=${this.isDisabled ? '-1' : '0'}
229
263
  inputmode=${kind === 'dayPeriod' ? nothing : 'numeric'}
230
264
  enterkeyhint=${editable ? 'next' : nothing}
231
- aria-label=${this.labeled ? `${segmentName(kind, locale)},` : segmentName(kind, locale)}
232
- aria-labelledby=${this.labeled ? `${id} ${this.labelId}` : nothing}
265
+ aria-label=${named === id ? segmentName(kind, locale) : `${segmentName(kind, locale)},`}
266
+ aria-labelledby=${named === id ? nothing : named}
233
267
  aria-invalid=${this.invalidity ?? nothing}
234
268
  aria-valuemin=${spin ? min : nothing}
235
269
  aria-valuemax=${spin ? max : nothing}
236
270
  aria-valuenow=${spin ? (shown ?? nothing) : nothing}
237
- aria-valuetext=${spin ? (empty ? this.emptySegmentLabel : segmentValueText(kind, this.parts, twelve, locale)) : nothing}
271
+ aria-valuetext=${spin ? (empty ? this.emptySegmentLabel : segmentValueText(kind, parts, twelve, locale, system)) : nothing}
238
272
  aria-disabled=${this.isDisabled ? 'true' : nothing}
239
273
  aria-required=${this.required ? 'true' : nothing}
240
274
  aria-readonly=${this.readOnly ? 'true' : nothing}
241
275
  @keydown=${this.handleSegmentKeydown}
242
276
  >${text}</span
243
277
  >`;
244
- })}
278
+ })}`;
279
+ };
280
+ return html `<div
281
+ part="segments"
282
+ id=${this.controlId}
283
+ role="group"
284
+ aria-labelledby=${this.labeled ? this.labelId : nothing}
285
+ aria-describedby=${this.describedBy ?? nothing}
286
+ aria-invalid=${this.invalidity ?? nothing}
287
+ @focusin=${this.handleSegmentsFocusIn}
288
+ @focusout=${this.handleSegmentsFocusOut}
289
+ @keypress=${this.handleKeypress}
290
+ >
291
+ ${Array.from({ length: rows }, (_, index) => index === 0
292
+ ? row(0)
293
+ : html `<span part="literal range-separator" aria-hidden="true"> – </span>${row(index)}`)}
245
294
  </div>`;
246
295
  }
247
296
  }
@@ -257,6 +306,14 @@ export const MixinSegmentedField = (Base) => {
257
306
  words,
258
307
  property({ attribute: 'incomplete-message' })
259
308
  ], SegmentedFieldElement.prototype, "incompleteMessage", void 0);
309
+ __decorate([
310
+ words,
311
+ property({ attribute: 'range-end-label' })
312
+ ], SegmentedFieldElement.prototype, "rangeEndLabel", void 0);
313
+ __decorate([
314
+ words,
315
+ property({ attribute: 'range-start-label' })
316
+ ], SegmentedFieldElement.prototype, "rangeStartLabel", void 0);
260
317
  __decorate([
261
318
  property({ type: Boolean })
262
319
  ], SegmentedFieldElement.prototype, "segmented", void 0);