@fusion.dev/helix-core 0.142.0 → 0.143.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 (47) hide show
  1. package/custom-elements.json +1475 -54
  2. package/dist/controllers/aria-forward-controller/aria-forward-controller.d.ts +55 -0
  3. package/dist/controllers/aria-forward-controller/aria-forward-controller.js +72 -16
  4. package/dist/controllers/arrival-controller/arrival-controller.d.ts +12 -0
  5. package/dist/controllers/arrival-controller/arrival-controller.js +10 -0
  6. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +10 -0
  7. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +5 -1
  8. package/dist/controllers/copy-controller/copy-controller.js +5 -0
  9. package/dist/controllers/dismiss-controller/dismiss-controller.js +40 -3
  10. package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.d.ts +79 -1
  11. package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.js +80 -10
  12. package/dist/controllers/popover-controller/popover-controller.d.ts +11 -0
  13. package/dist/controllers/popover-controller/popover-controller.js +6 -0
  14. package/dist/controllers/source-controller/source-controller.d.ts +22 -0
  15. package/dist/controllers/source-controller/source-controller.js +27 -1
  16. package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +16 -0
  17. package/dist/controllers/virtualize-controller/virtualize-controller.js +24 -2
  18. package/dist/events/events.d.ts +102 -4
  19. package/dist/events/events.js +84 -0
  20. package/dist/helix-element/react-bridge.js +6 -4
  21. package/dist/helix-form-element/helix-form-element.d.ts +131 -2
  22. package/dist/helix-form-element/helix-form-element.js +216 -6
  23. package/dist/index.d.ts +4 -3
  24. package/dist/index.js +4 -3
  25. package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +8 -0
  26. package/dist/mixins/mixin-chart-source/mixin-chart-source.js +4 -0
  27. package/dist/mixins/mixin-collection/mixin-collection.d.ts +4 -2
  28. package/dist/mixins/mixin-collection/mixin-collection.js +8 -6
  29. package/dist/mixins/mixin-controller-open/mixin-controller-open.js +19 -3
  30. package/dist/mixins/mixin-dismissible/mixin-dismissible.js +39 -0
  31. package/dist/mixins/mixin-field/mixin-field.js +38 -8
  32. package/dist/mixins/mixin-picker-editing/mixin-picker-editing.d.ts +8 -3
  33. package/dist/mixins/mixin-picker-editing/mixin-picker-editing.js +6 -0
  34. package/dist/mixins/mixin-picker-field/mixin-picker-field.d.ts +27 -4
  35. package/dist/mixins/mixin-picker-field/mixin-picker-field.js +83 -1
  36. package/dist/utils/draw-tooltip/draw-tooltip.d.ts +3 -0
  37. package/dist/utils/draw-tooltip/draw-tooltip.js +5 -0
  38. package/dist/utils/read-series/read-series.d.ts +28 -0
  39. package/dist/utils/read-series/read-series.js +31 -4
  40. package/dist/utils/report-runtime/report-runtime.d.ts +10 -0
  41. package/dist/utils/scroll-lock/scroll-lock.d.ts +16 -0
  42. package/dist/utils/scroll-lock/scroll-lock.js +30 -0
  43. package/dist/utils/written-list/written-list.d.ts +11 -2
  44. package/dist/utils/written-list/written-list.js +3 -1
  45. package/dist/version.d.ts +1 -1
  46. package/dist/version.js +1 -1
  47. package/package.json +4 -4
@@ -11,8 +11,17 @@ import { HelixElement } from '../helix-element/helix-element.js';
11
11
  * `internals` comes from `HelixElement`, which attaches it for every component.
12
12
  * `attachInternals()` throws on a second call, so only one class attaches it.
13
13
  *
14
- * The class carries only what every form participant needs. `value`, validity,
15
- * and the reset and restore callbacks are added with the first input component.
14
+ * The class carries only what every form participant needs. `value` and the
15
+ * reset and restore callbacks are added with the first input component.
16
+ *
17
+ * **The validity API is the platform's, read through `internals`**, so
18
+ * `checkValidity()`, `reportValidity()` and `setCustomValidity()` work on the
19
+ * host as they do on a native control. A custom message is laid over whatever
20
+ * the control itself reports, which is why `setValidity` is wrapped below.
21
+ *
22
+ * **Focus goes to the control's first tab stop**, for `focus()` and for a
23
+ * press on a `<label>` that names the host. A host is not focusable itself, so
24
+ * without this a label press focused nothing on half the controls.
16
25
  *
17
26
  * @since 0.1.0
18
27
  * @status experimental
@@ -109,6 +118,16 @@ export declare class HelixFormElement extends HelixElement {
109
118
  * middle of removing.
110
119
  */
111
120
  formDisabledCallback(disabled: boolean): void;
121
+ /**
122
+ * Whether an ancestor `<fieldset disabled>` holds the control, outside its
123
+ * first `<legend>`, read from the tree.
124
+ *
125
+ * For the one case the callback cannot report: a control disabled itself and
126
+ * by a fieldset, whose own `disabled` is then removed. The effective state does
127
+ * not change, so the platform does not call, and the fieldset's hold has to
128
+ * be read.
129
+ */
130
+ private get heldByFieldset();
112
131
  /**
113
132
  * Called by the browser when the form is reset. It does nothing here, and
114
133
  * every override calls it.
@@ -158,6 +177,116 @@ export declare class HelixFormElement extends HelixElement {
158
177
  * Here rather than on each control.
159
178
  */
160
179
  name: string;
180
+ constructor();
181
+ /** The platform's own `setValidity`, which the wrapper above calls. */
182
+ private readonly reportValidityTo;
183
+ /** What the control last reported, before any custom error is laid over it. */
184
+ private reported;
185
+ /** The message `setCustomValidity` was given. */
186
+ private customValidity;
187
+ /** The `validityFeedback` the platform was last told about. */
188
+ private appliedFeedback;
189
+ /**
190
+ * @internal A message that marks the control invalid, from a layer above this
191
+ * one. `MixinField` answers with `feedback`, so a field showing what is wrong
192
+ * does not submit.
193
+ */
194
+ protected get validityFeedback(): string;
195
+ /**
196
+ * Hands the platform the control's own verdict with any custom error on top,
197
+ * anchored where focus can go.
198
+ */
199
+ private applyValidity;
200
+ /**
201
+ * Where the browser points its bubble: the anchor a control gave if focus
202
+ * can go there, and its first tab stop otherwise.
203
+ *
204
+ * A group anchors on a part that holds its members and cannot take focus,
205
+ * and the platform then refuses the submission, logs that the control is
206
+ * not focusable, and leaves the reader on the submit button.
207
+ */
208
+ protected validationAnchor(anchor?: HTMLElement): HTMLElement | undefined;
209
+ /**
210
+ * Whether the control meets its constraints, and why not.
211
+ *
212
+ * @reference https://developer.mozilla.org/docs/Web/API/ElementInternals/validity ElementInternals.validity
213
+ */
214
+ get validity(): ValidityState;
215
+ /** The message the browser would show for the control, or an empty string. */
216
+ get validationMessage(): string;
217
+ /** Whether the control is checked when its form is submitted. */
218
+ get willValidate(): boolean;
219
+ /**
220
+ * Returns whether the control is valid, and fires `invalid` on it when it is
221
+ * not.
222
+ */
223
+ checkValidity(): boolean;
224
+ /**
225
+ * Returns whether the control is valid, and when it is not, fires `invalid`
226
+ * and shows the browser's message at the control.
227
+ */
228
+ reportValidity(): boolean;
229
+ /**
230
+ * Marks the control invalid with `message`, or clears that with an empty
231
+ * string, as on a native control. The form does not submit while it is set.
232
+ */
233
+ setCustomValidity(message: string): void;
234
+ /**
235
+ * The element focus goes to: the control's first tab stop, or `null` where
236
+ * there is none.
237
+ *
238
+ * Read through shadow roots and slots, so a group lands on the member that
239
+ * holds its tab stop, which is the chosen radio in a radio group.
240
+ */
241
+ protected get focusTarget(): HTMLElement | null;
242
+ /** Moves focus to the control, as focusing a native control does. */
243
+ focus(options?: FocusOptions): void;
244
+ /**
245
+ * Focuses the control when its `<label>` is pressed.
246
+ *
247
+ * The label sends the host a click of the browser's own after trying to
248
+ * focus it, and a host that is not focusable takes nothing from that try.
249
+ * Only a click that starts at the host and that the browser sent: one from
250
+ * inside has landed where it meant to, and `click()` from a script does not
251
+ * focus a native control either.
252
+ */
253
+ private readonly focusFromLabel;
254
+ /**
255
+ * Re-anchors the verdict as the form checks it, because the control's
256
+ * members may have rendered since it last reported, and the anchor read then
257
+ * was the best there was.
258
+ */
259
+ private readonly refreshAnchor;
260
+ connectedCallback(): void;
261
+ disconnectedCallback(): void;
262
+ /**
263
+ * @internal Whether this control is a field that counts against implicit
264
+ * submission: a single line of text, as `<input type="text">` is.
265
+ */
266
+ protected get blocksImplicitSubmission(): boolean;
267
+ /** @internal Whether this control submits its form, for finding the form's default button. */
268
+ protected get submitsForm(): boolean;
269
+ /** @internal Activates the control as its form's default button. */
270
+ protected activate(): void;
271
+ /**
272
+ * Submits the form the way pressing Enter in a native text field does.
273
+ *
274
+ * The control's own input is in a shadow root and has no form owner, so the
275
+ * platform never runs implicit submission for it. This runs it: the form's
276
+ * default button is activated, the first submit button in it, a Helix one
277
+ * included. With none, the form is submitted when it holds no more than one
278
+ * field that counts against implicit submission, and nothing happens
279
+ * otherwise.
280
+ *
281
+ * @reference https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#implicit-submission implicit submission
282
+ */
283
+ protected submitImplicitly(): void;
284
+ /** The form's default button, the first submit button in it, native or Helix. */
285
+ protected static defaultButton(form: HTMLFormElement): Element | undefined;
286
+ /** Whether `target` is a native field of `form` that Enter submits it from. */
287
+ protected static submitsOnEnter(target: EventTarget | undefined, form: HTMLFormElement): boolean;
288
+ /** Whether `element` is a submit button, native or Helix. */
289
+ protected static submits(element: Element): boolean;
161
290
  /**
162
291
  * The form the control belongs to, or `null`.
163
292
  *
@@ -6,13 +6,55 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
6
6
  };
7
7
  import { property, state } from 'lit/decorators.js';
8
8
  import { HelixElement } from '../helix-element/helix-element.js';
9
- export class HelixFormElement extends HelixElement {
10
- constructor() {
11
- super(...arguments);
12
- this.disabled = false;
13
- this.formDisabled = false;
14
- this.name = '';
9
+ const VALIDITY_FLAGS = [
10
+ 'badInput',
11
+ 'customError',
12
+ 'patternMismatch',
13
+ 'rangeOverflow',
14
+ 'rangeUnderflow',
15
+ 'stepMismatch',
16
+ 'tooLong',
17
+ 'tooShort',
18
+ 'typeMismatch',
19
+ 'valueMissing',
20
+ ];
21
+ const BLOCKING_TYPES = new Set([
22
+ 'date',
23
+ 'datetime-local',
24
+ 'email',
25
+ 'month',
26
+ 'number',
27
+ 'password',
28
+ 'search',
29
+ 'tel',
30
+ 'text',
31
+ 'time',
32
+ 'url',
33
+ 'week',
34
+ ]);
35
+ const takesFocus = (element) => (element.tabIndex >= 0 || element.hasAttribute('tabindex')) &&
36
+ !element.disabled &&
37
+ (typeof element.checkVisibility !== 'function' || element.checkVisibility());
38
+ const isTabStop = (element) => element instanceof HTMLElement &&
39
+ element.tabIndex >= 0 &&
40
+ !element.disabled &&
41
+ !element.hidden &&
42
+ !element.inert &&
43
+ (typeof element.checkVisibility !== 'function' || element.checkVisibility());
44
+ const firstTabStop = (node) => {
45
+ const children = node instanceof HTMLSlotElement ? node.assignedElements({ flatten: true }) : [...node.children];
46
+ for (const child of children) {
47
+ if (isTabStop(child)) {
48
+ return child;
49
+ }
50
+ const found = (child.shadowRoot ? firstTabStop(child.shadowRoot) : null) ?? firstTabStop(child);
51
+ if (found) {
52
+ return found;
53
+ }
15
54
  }
55
+ return null;
56
+ };
57
+ export class HelixFormElement extends HelixElement {
16
58
  static { this.formAssociated = true; }
17
59
  get isDisabled() {
18
60
  return this.disabled || this.formDisabled;
@@ -20,10 +62,27 @@ export class HelixFormElement extends HelixElement {
20
62
  formDisabledCallback(disabled) {
21
63
  this.formDisabled = disabled && !this.disabled;
22
64
  }
65
+ get heldByFieldset() {
66
+ let fieldset = this.parentElement?.closest('fieldset[disabled]') ?? null;
67
+ while (fieldset) {
68
+ const legend = fieldset.querySelector(':scope > legend');
69
+ if (!legend?.contains(this)) {
70
+ return true;
71
+ }
72
+ fieldset = fieldset.parentElement?.closest('fieldset[disabled]') ?? null;
73
+ }
74
+ return false;
75
+ }
23
76
  formResetCallback() { }
24
77
  willUpdate(changed) {
25
78
  super.willUpdate(changed);
79
+ if (changed.get('disabled') === true && !this.disabled && this.isConnected) {
80
+ this.formDisabled = this.heldByFieldset;
81
+ }
26
82
  this.internals.ariaDisabled = this.isDisabled ? 'true' : null;
83
+ if (this.validityFeedback !== this.appliedFeedback) {
84
+ this.applyValidity();
85
+ }
27
86
  }
28
87
  announceInput() {
29
88
  this.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
@@ -31,6 +90,157 @@ export class HelixFormElement extends HelixElement {
31
90
  announceChange() {
32
91
  this.dispatchEvent(new Event('change', { bubbles: true, composed: false }));
33
92
  }
93
+ constructor() {
94
+ super();
95
+ this.disabled = false;
96
+ this.formDisabled = false;
97
+ this.name = '';
98
+ this.reported = { anchor: undefined, flags: {}, message: '' };
99
+ this.customValidity = '';
100
+ this.appliedFeedback = '';
101
+ this.focusFromLabel = (event) => {
102
+ if (!event.isTrusted || event.composedPath()[0] !== this || this.isDisabled) {
103
+ return;
104
+ }
105
+ if (!this.matches(':focus-within')) {
106
+ this.focus();
107
+ }
108
+ };
109
+ this.refreshAnchor = () => {
110
+ if (!this.internals.validity.valid) {
111
+ this.applyValidity();
112
+ }
113
+ };
114
+ const internals = this.internals;
115
+ const report = internals.setValidity.bind(internals);
116
+ this.reportValidityTo = report;
117
+ internals.setValidity = (flags, message, anchor) => {
118
+ this.reported = { anchor, flags: flags ?? {}, message: message ?? '' };
119
+ this.applyValidity();
120
+ };
121
+ }
122
+ get validityFeedback() {
123
+ return '';
124
+ }
125
+ applyValidity() {
126
+ const feedback = this.validityFeedback;
127
+ this.appliedFeedback = feedback;
128
+ const custom = this.customValidity || feedback;
129
+ const flags = {};
130
+ for (const flag of VALIDITY_FLAGS) {
131
+ if (this.reported.flags[flag] === true) {
132
+ flags[flag] = true;
133
+ }
134
+ }
135
+ if (custom !== '') {
136
+ flags.customError = true;
137
+ }
138
+ if (Object.keys(flags).length === 0) {
139
+ this.reportValidityTo({});
140
+ return;
141
+ }
142
+ this.reportValidityTo(flags, custom || this.reported.message, this.validationAnchor(this.reported.anchor));
143
+ }
144
+ validationAnchor(anchor) {
145
+ if (anchor && takesFocus(anchor)) {
146
+ return anchor;
147
+ }
148
+ return this.focusTarget ?? anchor;
149
+ }
150
+ get validity() {
151
+ return this.internals.validity;
152
+ }
153
+ get validationMessage() {
154
+ return this.internals.validationMessage;
155
+ }
156
+ get willValidate() {
157
+ return this.internals.willValidate;
158
+ }
159
+ checkValidity() {
160
+ return this.internals.checkValidity();
161
+ }
162
+ reportValidity() {
163
+ this.applyValidity();
164
+ return this.internals.reportValidity();
165
+ }
166
+ setCustomValidity(message) {
167
+ this.customValidity = message;
168
+ this.applyValidity();
169
+ this.requestUpdate();
170
+ }
171
+ get focusTarget() {
172
+ const root = this.shadowRoot;
173
+ return root ? firstTabStop(root) : null;
174
+ }
175
+ focus(options) {
176
+ const target = this.focusTarget;
177
+ if (target) {
178
+ target.focus(options);
179
+ return;
180
+ }
181
+ super.focus(options);
182
+ }
183
+ connectedCallback() {
184
+ super.connectedCallback();
185
+ this.addEventListener('click', this.focusFromLabel);
186
+ this.addEventListener('invalid', this.refreshAnchor);
187
+ }
188
+ disconnectedCallback() {
189
+ super.disconnectedCallback();
190
+ this.removeEventListener('click', this.focusFromLabel);
191
+ this.removeEventListener('invalid', this.refreshAnchor);
192
+ }
193
+ get blocksImplicitSubmission() {
194
+ return false;
195
+ }
196
+ get submitsForm() {
197
+ return false;
198
+ }
199
+ activate() {
200
+ this.click();
201
+ }
202
+ submitImplicitly() {
203
+ const { form } = this;
204
+ if (!form) {
205
+ return;
206
+ }
207
+ const elements = [...form.elements];
208
+ const button = HelixFormElement.defaultButton(form);
209
+ if (button) {
210
+ if (button.matches(':disabled')) {
211
+ return;
212
+ }
213
+ if (button instanceof HelixFormElement) {
214
+ button.activate();
215
+ }
216
+ else {
217
+ ;
218
+ button.click();
219
+ }
220
+ return;
221
+ }
222
+ const blocking = elements.filter((element) => element instanceof HelixFormElement
223
+ ? element.blocksImplicitSubmission
224
+ : element instanceof HTMLInputElement && BLOCKING_TYPES.has(element.type));
225
+ if (blocking.length <= 1) {
226
+ form.requestSubmit();
227
+ }
228
+ }
229
+ static defaultButton(form) {
230
+ return [...form.elements].find((element) => HelixFormElement.submits(element));
231
+ }
232
+ static submitsOnEnter(target, form) {
233
+ return (target instanceof HTMLInputElement && target.form === form && BLOCKING_TYPES.has(target.type));
234
+ }
235
+ static submits(element) {
236
+ if (element instanceof HelixFormElement) {
237
+ return element.submitsForm;
238
+ }
239
+ if (element instanceof HTMLButtonElement) {
240
+ return element.type === 'submit';
241
+ }
242
+ return (element instanceof HTMLInputElement && (element.type === 'submit' || element.type === 'image'));
243
+ }
34
244
  get form() {
35
245
  return this.internals.form;
36
246
  }
package/dist/index.d.ts CHANGED
@@ -12,7 +12,7 @@ export { FullscreenController } from './controllers/fullscreen-controller/fullsc
12
12
  export { HostClickController } from './controllers/host-click-controller/host-click-controller.js';
13
13
  export { IdleController } from './controllers/idle-controller/idle-controller.js';
14
14
  export { IndicatorController, type IndicatorOptions, } from './controllers/indicator-controller/indicator-controller.js';
15
- export { KeyboardNavigationController, type KeyboardNavigationOptions, type NavigableItem, } from './controllers/keyboard-navigation-controller/keyboard-navigation-controller.js';
15
+ export { KeyboardNavigationController, type KeyboardNavigationOptions, type NavigableItem, Typeahead, typesCharacter, } from './controllers/keyboard-navigation-controller/keyboard-navigation-controller.js';
16
16
  export { MediaSessionController, type SessionHandlers, SKIP_SECONDS, } from './controllers/media-session-controller/media-session-controller.js';
17
17
  export { MAX_FRAME_STEP, PaintGateController, type PaintGateOptions, } from './controllers/paint-gate-controller/paint-gate-controller.js';
18
18
  export { PlacementController } from './controllers/placement-controller/placement-controller.js';
@@ -22,7 +22,7 @@ export { SourceController, type SourceHost, } from './controllers/source-control
22
22
  export { type TokenColor, TokenColorController, } from './controllers/token-color-controller/token-color-controller.js';
23
23
  export { type Rendered, VirtualizeController, type VirtualizeOptions, type Window as VirtualizeWindow, } from './controllers/virtualize-controller/virtualize-controller.js';
24
24
  export { eventsForTag, registerElementEvents } from './events/element-events.js';
25
- export { type CanvasView, type CaretDetail, type ChangeDetail, type CompleteDetail, type ConnectDetail, type ConsoleDetail, type CopyDetail, type CreateDetail, type DismissDetail, type FacetDetail, type FilterDetail, type GraphFilterDetail, type GraphLayoutDetail, type GraphPartChangeDetail, type GraphSignalDetail, HelixActivateEvent, HelixBeforeActivateEvent, HelixBeforeChangeEvent, HelixBeforeCloseEvent, HelixBeforeCollapseEvent, HelixBeforeEditEvent, HelixBeforeMoveEvent, HelixBeforeOpenEvent, HelixBeforePanelResizeEvent, HelixBeforePinEvent, HelixBeforeReconnectEvent, HelixBeforeRelabelEvent, HelixBeforeResizeEvent, HelixBeforeSplitEvent, HelixBeforeToggleEvent, HelixCanvasEditEvent, HelixCanvasPanEvent, HelixCanvasViewEvent, HelixCanvasZoomEvent, HelixCaretEvent, HelixCompleteEvent, HelixConnectEvent, HelixConsoleEvent, HelixCopyEvent, HelixCreateEvent, HelixDismissEvent, HelixFacetEvent, HelixFilterEvent, HelixGraphFilterEvent, HelixGraphLayoutEvent, HelixGraphPanEvent, HelixGraphPartChangeEvent, HelixGraphReadyEvent, HelixGraphSignalEvent, HelixGraphZoomEvent, HelixHistoryEvent, HelixMapClusterEvent, HelixMapPanEvent, HelixMapPartChangeEvent, HelixMapReadyEvent, HelixMapRotateEvent, HelixMapTiltEvent, HelixMapZoomEvent, HelixMoveEvent, HelixNavigateEvent, HelixNextEvent, HelixPreviousEvent, HelixReadEvent, HelixReconnectEvent, HelixRelabelEvent, HelixRemoveEvent, HelixReorderEvent, HelixResizeEvent, HelixSampleEndEvent, HelixSampleStartEvent, HelixScrolledEvent, HelixSelectEvent, HelixSendEvent, HelixShortcutEvent, HelixSortEvent, HelixTourAskEvent, type MapClusterDetail, type MapPanDetail, type MapPartChangeDetail, type MapRotateDetail, type MapTiltDetail, type MapZoomDetail, type MoveDetail, type NavigateDetail, type OpenDetail, type ReadDetail, type ReconnectDetail, type RelabelDetail, type RemoveDetail, type ResizeDetail, type ScrolledDetail, type SelectDetail, type SendDetail, type ShortcutDetail, type SkipDetail, type SortDetail, type ToggleDetail, type ViewportPanDetail, type ViewportZoomDetail, } from './events/events.js';
25
+ export { type CanvasView, type CaretDetail, type ChangeDetail, type CompleteDetail, type ConnectDetail, type ConsoleDetail, type CopyDetail, type CreateDetail, type DismissDetail, type FacetDetail, type FilterDetail, type GraphFilterDetail, type GraphLayoutDetail, type GraphPartChangeDetail, type GraphSignalDetail, HelixActivateEvent, HelixBeforeActivateEvent, HelixBeforeChangeEvent, HelixBeforeCloseEvent, HelixBeforeCollapseEvent, HelixBeforeEditEvent, HelixBeforeMoveEvent, HelixBeforeOpenEvent, HelixBeforePanelResizeEvent, HelixBeforePinEvent, HelixBeforeReconnectEvent, HelixBeforeRelabelEvent, HelixBeforeResizeEvent, HelixBeforeSplitEvent, HelixBeforeToggleEvent, HelixCanvasEditEvent, HelixCanvasPanEvent, HelixCanvasViewEvent, HelixCanvasZoomEvent, HelixCaretEvent, HelixCollapseEvent, HelixCompleteEvent, HelixConnectEvent, HelixConsoleEvent, HelixCopyEvent, HelixCreateEvent, HelixDismissEvent, HelixFacetEvent, HelixFilterEvent, HelixFilteredEvent, HelixGraphFilterEvent, HelixGraphLayoutEvent, HelixGraphPanEvent, HelixGraphPartChangeEvent, HelixGraphReadyEvent, HelixGraphSignalEvent, HelixGraphZoomEvent, HelixHistoryEvent, HelixMapClusterEvent, HelixMapPanEvent, HelixMapPartChangeEvent, HelixMapReadyEvent, HelixMapRotateEvent, HelixMapTiltEvent, HelixMapZoomEvent, HelixMoveEvent, HelixNavigateEvent, HelixNextEvent, HelixPinEvent, HelixPreviousEvent, HelixReadEvent, HelixReconnectEvent, HelixRelabelEvent, HelixRemoveEvent, HelixReorderEvent, HelixReorderedEvent, HelixResizedEvent, HelixResizeEvent, HelixSampleEndEvent, HelixSampleStartEvent, HelixScrolledEvent, HelixSelectEvent, HelixSelectedEvent, HelixSendEvent, HelixShortcutEvent, HelixSortEvent, HelixSortedEvent, HelixTourAskEvent, type MapClusterDetail, type MapPanDetail, type MapPartChangeDetail, type MapRotateDetail, type MapTiltDetail, type MapZoomDetail, type MoveDetail, type NavigateDetail, type OpenDetail, type ReadDetail, type ReconnectDetail, type RelabelDetail, type RemoveDetail, type ResizeDetail, type ScrolledDetail, type SelectDetail, type SendDetail, type ShortcutDetail, type SkipDetail, type SortDetail, type ToggleDetail, type ViewportPanDetail, type ViewportZoomDetail, } from './events/events.js';
26
26
  export { HelixElement, whenHydrated } from './helix-element/helix-element.js';
27
27
  export { HelixFormElement } from './helix-form-element/helix-form-element.js';
28
28
  export { MixinChartSource, type MixinChartSourceInterface, PALETTE, PLOT_HEIGHT, PLOT_WIDTH, seriesIndex, seriesNames, } from './mixins/mixin-chart-source/mixin-chart-source.js';
@@ -61,7 +61,7 @@ export { markRegion, type RegionOptions } from './utils/mark-region/mark-region.
61
61
  export { copiesOf, type MediaCopy, sameCopies, } from './utils/media-copy/media-copy.js';
62
62
  export { onUnlessFalse } from './utils/on-unless-false/on-unless-false.js';
63
63
  export { type ColorBytes, readColor } from './utils/read-color/read-color.js';
64
- export { type Row, type RowCell, readRows, readSeries, type Series, type SeriesPoint, } from './utils/read-series/read-series.js';
64
+ export { byRow, type Row, type RowCell, readRows, readSeries, type Series, type SeriesPoint, unbroken, } from './utils/read-series/read-series.js';
65
65
  export { type Relative, relativeTime, } from './utils/relative-time/relative-time.js';
66
66
  export { measuring, RENDER_SLOT, reportRender } from './utils/report-render/report-render.js';
67
67
  export { RUNTIME_LOG, RUNTIME_SLOT, type RuntimeEvent, type RuntimeKind, reportRuntime, } from './utils/report-runtime/report-runtime.js';
@@ -73,6 +73,7 @@ export { holdCaret, restoreValue } from './utils/restore-value/restore-value.js'
73
73
  export { extent, linear, type Scale } from './utils/scale/scale.js';
74
74
  export { ticks } from './utils/scale/ticks.js';
75
75
  export { type ScrollItemOptions, scrollItemIntoView, } from './utils/scroll-item-into-view/scroll-item-into-view.js';
76
+ export { lockScroll, scrollHolders, unlockScroll } from './utils/scroll-lock/scroll-lock.js';
76
77
  export { slotFilled } from './utils/slot-filled/slot-filled.js';
77
78
  export { slottedWords } from './utils/slotted-words/slotted-words.js';
78
79
  export { splitValues } from './utils/split-values/split-values.js';
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ export { FullscreenController } from './controllers/fullscreen-controller/fullsc
12
12
  export { HostClickController } from './controllers/host-click-controller/host-click-controller.js';
13
13
  export { IdleController } from './controllers/idle-controller/idle-controller.js';
14
14
  export { IndicatorController, } from './controllers/indicator-controller/indicator-controller.js';
15
- export { KeyboardNavigationController, } from './controllers/keyboard-navigation-controller/keyboard-navigation-controller.js';
15
+ export { KeyboardNavigationController, Typeahead, typesCharacter, } from './controllers/keyboard-navigation-controller/keyboard-navigation-controller.js';
16
16
  export { MediaSessionController, SKIP_SECONDS, } from './controllers/media-session-controller/media-session-controller.js';
17
17
  export { MAX_FRAME_STEP, PaintGateController, } from './controllers/paint-gate-controller/paint-gate-controller.js';
18
18
  export { PlacementController } from './controllers/placement-controller/placement-controller.js';
@@ -22,7 +22,7 @@ export { SourceController, } from './controllers/source-controller/source-contro
22
22
  export { TokenColorController, } from './controllers/token-color-controller/token-color-controller.js';
23
23
  export { VirtualizeController, } from './controllers/virtualize-controller/virtualize-controller.js';
24
24
  export { eventsForTag, registerElementEvents } from './events/element-events.js';
25
- export { HelixActivateEvent, HelixBeforeActivateEvent, HelixBeforeChangeEvent, HelixBeforeCloseEvent, HelixBeforeCollapseEvent, HelixBeforeEditEvent, HelixBeforeMoveEvent, HelixBeforeOpenEvent, HelixBeforePanelResizeEvent, HelixBeforePinEvent, HelixBeforeReconnectEvent, HelixBeforeRelabelEvent, HelixBeforeResizeEvent, HelixBeforeSplitEvent, HelixBeforeToggleEvent, HelixCanvasEditEvent, HelixCanvasPanEvent, HelixCanvasViewEvent, HelixCanvasZoomEvent, HelixCaretEvent, HelixCompleteEvent, HelixConnectEvent, HelixConsoleEvent, HelixCopyEvent, HelixCreateEvent, HelixDismissEvent, HelixFacetEvent, HelixFilterEvent, HelixGraphFilterEvent, HelixGraphLayoutEvent, HelixGraphPanEvent, HelixGraphPartChangeEvent, HelixGraphReadyEvent, HelixGraphSignalEvent, HelixGraphZoomEvent, HelixHistoryEvent, HelixMapClusterEvent, HelixMapPanEvent, HelixMapPartChangeEvent, HelixMapReadyEvent, HelixMapRotateEvent, HelixMapTiltEvent, HelixMapZoomEvent, HelixMoveEvent, HelixNavigateEvent, HelixNextEvent, HelixPreviousEvent, HelixReadEvent, HelixReconnectEvent, HelixRelabelEvent, HelixRemoveEvent, HelixReorderEvent, HelixResizeEvent, HelixSampleEndEvent, HelixSampleStartEvent, HelixScrolledEvent, HelixSelectEvent, HelixSendEvent, HelixShortcutEvent, HelixSortEvent, HelixTourAskEvent, } from './events/events.js';
25
+ export { HelixActivateEvent, HelixBeforeActivateEvent, HelixBeforeChangeEvent, HelixBeforeCloseEvent, HelixBeforeCollapseEvent, HelixBeforeEditEvent, HelixBeforeMoveEvent, HelixBeforeOpenEvent, HelixBeforePanelResizeEvent, HelixBeforePinEvent, HelixBeforeReconnectEvent, HelixBeforeRelabelEvent, HelixBeforeResizeEvent, HelixBeforeSplitEvent, HelixBeforeToggleEvent, HelixCanvasEditEvent, HelixCanvasPanEvent, HelixCanvasViewEvent, HelixCanvasZoomEvent, HelixCaretEvent, HelixCollapseEvent, HelixCompleteEvent, HelixConnectEvent, HelixConsoleEvent, HelixCopyEvent, HelixCreateEvent, HelixDismissEvent, HelixFacetEvent, HelixFilterEvent, HelixFilteredEvent, HelixGraphFilterEvent, HelixGraphLayoutEvent, HelixGraphPanEvent, HelixGraphPartChangeEvent, HelixGraphReadyEvent, HelixGraphSignalEvent, HelixGraphZoomEvent, HelixHistoryEvent, HelixMapClusterEvent, HelixMapPanEvent, HelixMapPartChangeEvent, HelixMapReadyEvent, HelixMapRotateEvent, HelixMapTiltEvent, HelixMapZoomEvent, HelixMoveEvent, HelixNavigateEvent, HelixNextEvent, HelixPinEvent, HelixPreviousEvent, HelixReadEvent, HelixReconnectEvent, HelixRelabelEvent, HelixRemoveEvent, HelixReorderEvent, HelixReorderedEvent, HelixResizedEvent, HelixResizeEvent, HelixSampleEndEvent, HelixSampleStartEvent, HelixScrolledEvent, HelixSelectEvent, HelixSelectedEvent, HelixSendEvent, HelixShortcutEvent, HelixSortEvent, HelixSortedEvent, HelixTourAskEvent, } from './events/events.js';
26
26
  export { HelixElement, whenHydrated } from './helix-element/helix-element.js';
27
27
  export { HelixFormElement } from './helix-form-element/helix-form-element.js';
28
28
  export { MixinChartSource, PALETTE, PLOT_HEIGHT, PLOT_WIDTH, seriesIndex, seriesNames, } from './mixins/mixin-chart-source/mixin-chart-source.js';
@@ -61,7 +61,7 @@ export { markRegion } from './utils/mark-region/mark-region.js';
61
61
  export { copiesOf, sameCopies, } from './utils/media-copy/media-copy.js';
62
62
  export { onUnlessFalse } from './utils/on-unless-false/on-unless-false.js';
63
63
  export { readColor } from './utils/read-color/read-color.js';
64
- export { readRows, readSeries, } from './utils/read-series/read-series.js';
64
+ export { byRow, readRows, readSeries, unbroken, } from './utils/read-series/read-series.js';
65
65
  export { relativeTime, } from './utils/relative-time/relative-time.js';
66
66
  export { measuring, RENDER_SLOT, reportRender } from './utils/report-render/report-render.js';
67
67
  export { RUNTIME_LOG, RUNTIME_SLOT, reportRuntime, } from './utils/report-runtime/report-runtime.js';
@@ -73,6 +73,7 @@ export { holdCaret, restoreValue } from './utils/restore-value/restore-value.js'
73
73
  export { extent, linear } from './utils/scale/scale.js';
74
74
  export { ticks } from './utils/scale/ticks.js';
75
75
  export { scrollItemIntoView, } from './utils/scroll-item-into-view/scroll-item-into-view.js';
76
+ export { lockScroll, scrollHolders, unlockScroll } from './utils/scroll-lock/scroll-lock.js';
76
77
  export { slotFilled } from './utils/slot-filled/slot-filled.js';
77
78
  export { slottedWords } from './utils/slotted-words/slotted-words.js';
78
79
  export { splitValues } from './utils/split-values/split-values.js';
@@ -128,6 +128,14 @@ export declare class MixinChartSourceInterface {
128
128
  * words and is already written.
129
129
  */
130
130
  label: string;
131
+ /**
132
+ * The locale the chart's own numbers are written in: its axis, its
133
+ * percentages and its totals, such as `de` for `1.240` and `32 %`.
134
+ *
135
+ * Absent uses the runtime's own. The table's cells are shown as the table
136
+ * writes them, whatever this says.
137
+ */
138
+ locale: string;
131
139
  /**
132
140
  * The table's columns, one series each. Empty when there is no table.
133
141
  */
@@ -21,6 +21,7 @@ export const MixinChartSource = (Base) => {
21
21
  this.pointer = new ChartPointerController(this);
22
22
  this.for = '';
23
23
  this.label = '';
24
+ this.locale = '';
24
25
  }
25
26
  willUpdate(changed) {
26
27
  super.willUpdate(changed);
@@ -56,5 +57,8 @@ export const MixinChartSource = (Base) => {
56
57
  __decorate([
57
58
  property()
58
59
  ], ChartSourceElement.prototype, "label", void 0);
60
+ __decorate([
61
+ property()
62
+ ], ChartSourceElement.prototype, "locale", void 0);
59
63
  return ChartSourceElement;
60
64
  };
@@ -16,9 +16,11 @@ export declare class CollectionElement {
16
16
  /**
17
17
  * The status line, or nothing where the collection has content to show.
18
18
  *
19
- * Rendered by the component wherever its rows would be.
19
+ * Rendered by the component wherever its rows would be. `cell` makes the line
20
+ * a row holding one cell of that role, for a table or a grid, whose children
21
+ * may only be rows.
20
22
  */
21
- protected renderStatus(hasContent: boolean): TemplateResult | typeof nothing;
23
+ protected renderStatus(hasContent: boolean, cell?: 'cell' | 'gridcell'): TemplateResult | typeof nothing;
22
24
  /**
23
25
  * The same words for a screen reader, in a region that is always there.
24
26
  *
@@ -42,14 +42,16 @@ export const MixinCollection = (Base) => {
42
42
  super.firstUpdated(changed);
43
43
  this.finishHydration();
44
44
  }
45
- renderStatus(hasContent) {
46
- if (this.loading) {
47
- return html `<p part="loading">${this.loadingText}</p>`;
48
- }
49
- if (hasContent) {
45
+ renderStatus(hasContent, cell) {
46
+ if (!this.loading && hasContent) {
50
47
  return nothing;
51
48
  }
52
- return html `<p part="empty">${this.emptyText}</p>`;
49
+ const text = this.loading ? this.loadingText : this.emptyText;
50
+ const words = cell ? html `<span role=${cell}>${text}</span>` : text;
51
+ const row = cell ? 'row' : nothing;
52
+ return this.loading
53
+ ? html `<p part="loading" role=${row}>${words}</p>`
54
+ : html `<p part="empty" role=${row}>${words}</p>`;
53
55
  }
54
56
  renderLive(hasContent) {
55
57
  const said = this.loading ? this.loadingText : hasContent ? this.announced : this.emptyText;
@@ -13,6 +13,7 @@ export const MixinControllerOpen = (Base) => {
13
13
  super(...arguments);
14
14
  this.defaultOpen = false;
15
15
  this.openState = undefined;
16
+ this.announcedOpen = undefined;
16
17
  }
17
18
  get open() {
18
19
  return this.openState ?? this.defaultOpen;
@@ -26,6 +27,15 @@ export const MixinControllerOpen = (Base) => {
26
27
  super.willUpdate(changed);
27
28
  this.toggleState('open', this.open);
28
29
  }
30
+ updated(changed) {
31
+ super.updated(changed);
32
+ if (this.announcedOpen === undefined) {
33
+ this.announcedOpen = this.open;
34
+ }
35
+ else if (this.announcedOpen !== this.open) {
36
+ this.announceToggle(this.open);
37
+ }
38
+ }
29
39
  requestOpen(next) {
30
40
  if (this.open === next) {
31
41
  return true;
@@ -46,11 +56,17 @@ export const MixinControllerOpen = (Base) => {
46
56
  return applied;
47
57
  }
48
58
  announceToggle(open) {
49
- const event = new Event('toggle', { bubbles: false, cancelable: false, composed: false });
50
- Object.assign(event, {
59
+ this.announcedOpen = open;
60
+ const states = {
51
61
  newState: open ? 'open' : 'closed',
52
62
  oldState: open ? 'closed' : 'open',
53
- });
63
+ };
64
+ if (typeof ToggleEvent === 'function') {
65
+ this.dispatchEvent(new ToggleEvent('toggle', states));
66
+ return;
67
+ }
68
+ const event = new Event('toggle', { bubbles: false, cancelable: false, composed: false });
69
+ Object.assign(event, states);
54
70
  this.dispatchEvent(event);
55
71
  }
56
72
  toggleOpen() {
@@ -6,6 +6,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
6
6
  };
7
7
  import { property } from 'lit/decorators.js';
8
8
  import { HelixDismissEvent } from '../../events/events.js';
9
+ import { deepActiveElement, deepContains, } from '../../utils/focusable-elements/focusable-elements.js';
9
10
  export const MixinDismissible = (Base) => {
10
11
  class Dismissible extends Base {
11
12
  constructor() {
@@ -21,10 +22,48 @@ export const MixinDismissible = (Base) => {
21
22
  return new HelixDismissEvent({ label: own.trim() });
22
23
  }
23
24
  dismiss() {
25
+ const neighbors = deepContains(this, deepActiveElement()) ? this.neighbors() : [];
24
26
  const event = this.dismissEvent();
25
27
  this.dispatchEvent(event);
28
+ if (neighbors.length > 0 && !event.defaultPrevented) {
29
+ this.keepFocus(neighbors);
30
+ }
26
31
  return !event.defaultPrevented;
27
32
  }
33
+ neighbors() {
34
+ const same = (node) => node instanceof HTMLElement && node.localName === this.localName;
35
+ const after = [];
36
+ for (let node = this.nextElementSibling; node; node = node.nextElementSibling) {
37
+ if (same(node)) {
38
+ after.push(node);
39
+ }
40
+ }
41
+ const before = [];
42
+ for (let node = this.previousElementSibling; node; node = node.previousElementSibling) {
43
+ if (same(node)) {
44
+ before.push(node);
45
+ }
46
+ }
47
+ return [...after, ...before];
48
+ }
49
+ keepFocus(neighbors) {
50
+ const settle = () => {
51
+ const active = document.activeElement;
52
+ if (this.isConnected || (active !== null && active !== document.body)) {
53
+ return;
54
+ }
55
+ for (const neighbor of neighbors) {
56
+ const control = neighbor.isConnected &&
57
+ neighbor.shadowRoot?.querySelector('[part~="dismiss"]');
58
+ if (control) {
59
+ control.focus();
60
+ return;
61
+ }
62
+ }
63
+ };
64
+ settle();
65
+ requestAnimationFrame(settle);
66
+ }
28
67
  }
29
68
  __decorate([
30
69
  property({ reflect: true, type: Boolean })