@fusion.dev/helix-core 0.25.1 → 0.30.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 (36) hide show
  1. package/custom-elements.json +833 -114
  2. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +23 -1
  3. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +23 -26
  4. package/dist/controllers/dismiss-controller/dismiss-controller.d.ts +27 -0
  5. package/dist/controllers/dismiss-controller/dismiss-controller.js +17 -2
  6. package/dist/controllers/floating-controller/floating-controller.d.ts +13 -0
  7. package/dist/controllers/floating-controller/floating-controller.js +3 -1
  8. package/dist/controllers/indicator-controller/indicator-controller.d.ts +16 -0
  9. package/dist/controllers/indicator-controller/indicator-controller.js +7 -0
  10. package/dist/controllers/popover-controller/popover-controller.d.ts +22 -0
  11. package/dist/controllers/popover-controller/popover-controller.js +12 -0
  12. package/dist/controllers/source-controller/source-controller.d.ts +45 -3
  13. package/dist/controllers/source-controller/source-controller.js +33 -21
  14. package/dist/helix-element/helix-element.d.ts +19 -0
  15. package/dist/helix-element/helix-element.js +5 -94
  16. package/dist/helix-element/react-bridge.d.ts +104 -0
  17. package/dist/helix-element/react-bridge.js +94 -0
  18. package/dist/helix-form-element/helix-form-element.d.ts +19 -0
  19. package/dist/helix-form-element/helix-form-element.js +6 -0
  20. package/dist/index.d.ts +2 -1
  21. package/dist/index.js +2 -1
  22. package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +1 -1
  23. package/dist/mixins/mixin-chart-source/mixin-chart-source.js +2 -5
  24. package/dist/mixins/mixin-controller-value/mixin-controller-value.js +2 -2
  25. package/dist/mixins/mixin-field/mixin-field.js +2 -2
  26. package/dist/mixins/mixin-picker-editing/mixin-picker-editing.d.ts +48 -0
  27. package/dist/mixins/mixin-picker-editing/mixin-picker-editing.js +43 -0
  28. package/dist/mixins/mixin-picker-field/mixin-picker-field.d.ts +118 -0
  29. package/dist/mixins/mixin-picker-field/mixin-picker-field.js +79 -0
  30. package/dist/utils/draw-icon/draw-icon.d.ts +1 -1
  31. package/dist/utils/draw-tooltip/draw-tooltip.js +1 -0
  32. package/package.json +4 -4
  33. package/dist/mixins/mixin-collection/index.d.ts +0 -1
  34. package/dist/mixins/mixin-collection/index.js +0 -1
  35. package/dist/utils/close-markdown/close-markdown.d.ts +0 -39
  36. package/dist/utils/close-markdown/close-markdown.js +0 -51
@@ -0,0 +1,104 @@
1
+ import type { LitElement } from 'lit';
2
+ /** The props React is holding for a node, if React is holding any. */
3
+ export declare const reactPropsOf: (node: EventTarget) => Record<string, unknown> | undefined;
4
+ /**
5
+ * Says so when a handler prop is attached to nothing.
6
+ *
7
+ * **TypeScript will not catch any of this, and that is not our tooling's
8
+ * fault.** A JSX attribute whose name is not a valid identifier is exempt from
9
+ * checking on an intrinsic element, and every one of ours carries a hyphen. So
10
+ * `onhelix-opne` type-checks, `on:helix-open` type-checks, and a made-up
11
+ * `totally-invented` prop type-checks, all against fully generated types that
12
+ * do reject a wrong *value* on a normal attribute. Measured with `tsc`.
13
+ *
14
+ * That leaves runtime as the only place a typo can be caught, and React's props
15
+ * bag as the only place to look, which the binding above already reads. Once per
16
+ * element, because a warning that repeats on every keystroke is one a consumer
17
+ * silences.
18
+ *
19
+ * `on:helix-open` gets its own message because it is not a mistake so much as
20
+ * the wrong dialect. It is what the Solid types offer, correctly, and React
21
+ * binds it as an event named `:helix-open`.
22
+ */
23
+ export declare const auditReactProps: (host: EventTarget, props: Record<string, unknown>) => void;
24
+ /**
25
+ * The prop React would be holding for this event, if it is holding one.
26
+ *
27
+ * **A relayed native event needs a different answer from one of ours, and
28
+ * getting it wrong doubles every call.** Measured against React 19.2.8:
29
+ *
30
+ * ```
31
+ * onhelix-before-change <- helix-before-change REACHED (React binds it with addEventListener)
32
+ * onChange <- change REACHED (React's delegated root listener)
33
+ * onLoad <- load not reached
34
+ * onError <- error not reached
35
+ * onEnded <- ended not reached
36
+ * ```
37
+ *
38
+ * React binds a hyphenated prop directly on the element, so our own events are
39
+ * reached by name and are safe to bind again: same type, same callback, same
40
+ * capture flag is a no-op by specification. A native name is different. React
41
+ * recognizes `onChange` and `onLoad` as its own registration names and attaches
42
+ * nothing to the element for either; it relies on a delegated listener at the
43
+ * root, which sees an event only if it **bubbles**, and on a non-delegated list
44
+ * it sets up for `<video>`, `<audio>` and `<img>` and never for a custom
45
+ * element.
46
+ *
47
+ * So a bubbling relay already arrives and must not be bound again: React's
48
+ * listener is at the root rather than on the element, so nothing deduplicates
49
+ * it and the consumer's handler runs twice. A non-bubbling relay arrives
50
+ * nowhere, and is the bug this exists to close: `<helix-avatar>` relays `load`
51
+ * and `error`, `<helix-video>` relays `ended`, `play` and `pause`, all of them
52
+ * declared public API and all of them otherwise silent in React.
53
+ *
54
+ * The name is React's own casing, `onError` rather than `onerror`, because that
55
+ * is what a consumer writes and therefore what is in the props bag. Every
56
+ * native we relay is a single word, so capitalising the first letter is the
57
+ * whole of the rule; a two-word name like `loadeddata` would be `onLoadedData`
58
+ * and needs the list extending rather than the rule generalising.
59
+ */
60
+ export declare const reactPropFor: (event: Event) => string | undefined;
61
+ export declare const bindReactHandler: (host: EventTarget, event: Event) => void;
62
+ /**
63
+ * Assigns the object and function properties React is holding and never applied.
64
+ *
65
+ * **The same hydration bug as the handlers above, and it needed its own fix.**
66
+ * React skips every prop it would have to apply imperatively while hydrating,
67
+ * not only listeners. Strings, numbers and booleans arrive anyway, because the
68
+ * server wrote them as attributes and Lit converts them back. An object or a
69
+ * function arrives by no route at all, and both halves of React agree to drop
70
+ * it: the server renderer reaches its custom element branch, finds
71
+ * `typeof value === 'object'` and `continue`s, so nothing is written into the
72
+ * HTML; and the client's hydrate path only diffs and warns for a custom element,
73
+ * never reaching `setPropOnCustomElement`. Two skips that match, so there is no
74
+ * mismatch to report and no warning anywhere.
75
+ *
76
+ * **The binding trick above does not transfer, which is why this is separate.**
77
+ * A listener only has to exist by the time somebody interacts, so binding it at
78
+ * dispatch is late enough that hydration has certainly finished and early enough
79
+ * to catch the event being sent. A property has to be right before the first
80
+ * paint. `connectedCallback` is the obvious early moment and is the wrong one,
81
+ * measured: an element upgrades when its definition loads, which falls either
82
+ * side of hydration depending on how the consumer imported it, and on the side
83
+ * this is for it is too early to see anything.
84
+ *
85
+ * **Only what the class declared, and only what could not have arrived as an
86
+ * attribute.** The first is what stops an arbitrary JSX prop being written onto
87
+ * the element; the second is what keeps this to the properties that are actually
88
+ * broken, rather than re-applying a string that is already correct. Three
89
+ * qualify today, `rows` on the data table and `cell` and `compare` on a table
90
+ * column; the remaining `attribute: false` properties are booleans and strings a
91
+ * parent writes at runtime, which no consumer passes and this never touches.
92
+ *
93
+ * **`null` is the one place this does not reproduce a client mount, and it is
94
+ * deliberate.** `typeof null` is `'object'`, and React assigns a null prop on a
95
+ * client mount while dropping it on the server, so it is genuinely inside the
96
+ * gap this closes. Assigning it would faithfully reproduce a crash: the server
97
+ * left the class default standing, and replacing that default with `null` is the
98
+ * one case where closing the gap makes the page worse than leaving it open.
99
+ *
100
+ * Idempotent, so the handover needs no bookkeeping. Lit compares by identity, so
101
+ * React's next commit assigning the same value schedules no update, and one
102
+ * assigning a different value is a change whoever writes it.
103
+ */
104
+ export declare const applyReactProperties: (host: LitElement) => boolean;
@@ -0,0 +1,94 @@
1
+ import { eventsForTag } from '../events/element-events.js';
2
+ import { helixEventNames } from '../events/events.js';
3
+ const REACT_PROPS = '__reactProps$';
4
+ let reactPropsKey;
5
+ export const reactPropsOf = (node) => {
6
+ const stash = node;
7
+ if (reactPropsKey !== undefined && stash[reactPropsKey] !== undefined) {
8
+ return stash[reactPropsKey];
9
+ }
10
+ const found = Object.keys(stash).find((key) => key.startsWith(REACT_PROPS));
11
+ if (found === undefined) {
12
+ return undefined;
13
+ }
14
+ reactPropsKey = found;
15
+ return stash[found];
16
+ };
17
+ const audited = new WeakSet();
18
+ const REMOVED = new Set(['onActiveChange', 'onCheckedChange', 'onOpenChange', 'onValueChange']);
19
+ export const auditReactProps = (host, props) => {
20
+ if (audited.has(host)) {
21
+ return;
22
+ }
23
+ audited.add(host);
24
+ const tag = host instanceof Element ? `<${host.localName}>` : 'A Helix element';
25
+ for (const key of Object.keys(props)) {
26
+ if (key.startsWith('on:')) {
27
+ console.warn(`${tag} was given \`${key}\`, which is Solid's syntax. React binds the name after \`on\` ` +
28
+ `verbatim, so this listens for an event called \`${key.slice(2)}\`. Write \`on${key.slice(3)}\`.`);
29
+ continue;
30
+ }
31
+ if (REMOVED.has(key)) {
32
+ console.warn(`${tag} was given \`${key}\`, which was removed. It never worked from JSX, because React ` +
33
+ `binds any \`on\` prop as an event of that name. Listen for the element's change event instead.`);
34
+ continue;
35
+ }
36
+ if (!key.startsWith('on') || !key.includes('-')) {
37
+ continue;
38
+ }
39
+ const name = key.slice(2);
40
+ if (!helixEventNames.has(name)) {
41
+ console.warn(`${tag} was given \`${key}\`, and nothing dispatches \`${name}\`. TypeScript cannot ` +
42
+ `check a hyphenated JSX prop, so this is the only place it shows up.`);
43
+ continue;
44
+ }
45
+ const mine = host instanceof Element ? eventsForTag(host.localName) : undefined;
46
+ if (mine && !mine.has(name)) {
47
+ console.warn(`${tag} was given \`${key}\`, but it dispatches ${[...mine].map((one) => `\`${one}\``).join(', ')}. ` +
48
+ `\`${name}\` is sent by a different element, and React only wires an \`on\` prop on the ` +
49
+ 'element that dispatches, so this handler will never run. Put it on that element.');
50
+ }
51
+ }
52
+ };
53
+ const REACT_WIRES = new Set(['scroll', 'scrollend']);
54
+ export const reactPropFor = (event) => {
55
+ if (event.type.includes('-')) {
56
+ return `on${event.type}`;
57
+ }
58
+ if (event.bubbles || REACT_WIRES.has(event.type)) {
59
+ return undefined;
60
+ }
61
+ return `on${event.type.charAt(0).toUpperCase()}${event.type.slice(1)}`;
62
+ };
63
+ export const bindReactHandler = (host, event) => {
64
+ const props = reactPropsOf(host);
65
+ if (props === undefined) {
66
+ return;
67
+ }
68
+ auditReactProps(host, props);
69
+ const key = reactPropFor(event);
70
+ const handler = key === undefined ? undefined : props[key];
71
+ if (typeof handler === 'function') {
72
+ host.addEventListener(event.type, handler);
73
+ }
74
+ };
75
+ export const applyReactProperties = (host) => {
76
+ const props = reactPropsOf(host);
77
+ if (props === undefined) {
78
+ return false;
79
+ }
80
+ const target = host;
81
+ for (const name of host.constructor.elementProperties.keys()) {
82
+ if (typeof name !== 'string' || !Object.hasOwn(props, name)) {
83
+ continue;
84
+ }
85
+ const value = props[name];
86
+ if (value === null || (typeof value !== 'object' && typeof value !== 'function')) {
87
+ continue;
88
+ }
89
+ if (target[name] !== value) {
90
+ target[name] = value;
91
+ }
92
+ }
93
+ return true;
94
+ };
@@ -112,6 +112,25 @@ export declare class HelixFormElement extends HelixElement {
112
112
  */
113
113
  formDisabledCallback(disabled: boolean): void;
114
114
  protected willUpdate(changed: PropertyValues): void;
115
+ /**
116
+ * Announces `input` from the host, the way the control it stands for would.
117
+ *
118
+ * Composed, as the native one is: `input` crosses shadow boundaries by
119
+ * specification, so a consumer listening above a composition hears it.
120
+ */
121
+ protected announceInput(): void;
122
+ /**
123
+ * Announces `change` from the host, the way the control it stands for would.
124
+ *
125
+ * **Not composed, and that is the shape of the native event rather than a
126
+ * choice made here.** `change` stops at a shadow boundary, so a composed
127
+ * control hears its inner control's and relays it under the same name from
128
+ * its own host, which is what `onChange` binds. Every control here that
129
+ * commits a value announces it through this, whether the commit came from an
130
+ * inner control's own `change`, a step, a scrub or a completed code, so the
131
+ * event is one shape in one place.
132
+ */
133
+ protected announceChange(): void;
115
134
  /**
116
135
  * Name submitted with the form.
117
136
  *
@@ -24,6 +24,12 @@ export class HelixFormElement extends HelixElement {
24
24
  super.willUpdate(changed);
25
25
  this.internals.ariaDisabled = this.isDisabled ? 'true' : null;
26
26
  }
27
+ announceInput() {
28
+ this.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
29
+ }
30
+ announceChange() {
31
+ this.dispatchEvent(new Event('change', { bubbles: true, composed: false }));
32
+ }
27
33
  get form() {
28
34
  return this.internals.form;
29
35
  }
package/dist/index.d.ts CHANGED
@@ -25,10 +25,11 @@ export { MixinControllerOpen, type MixinControllerOpenInterface, } from './mixin
25
25
  export { MixinControllerValue, type MixinControllerValueInterface, } from './mixins/mixin-controller-value/mixin-controller-value.js';
26
26
  export { type DismissibleElement, MixinDismissible, } from './mixins/mixin-dismissible/mixin-dismissible.js';
27
27
  export { MixinField, type MixinFieldInterface } from './mixins/mixin-field/mixin-field.js';
28
+ export { MixinPickerEditing, type MixinPickerEditingInterface, } from './mixins/mixin-picker-editing/mixin-picker-editing.js';
29
+ export { MixinPickerField, type MixinPickerFieldInterface, } from './mixins/mixin-picker-field/mixin-picker-field.js';
28
30
  export { MixinTokens, type MixinTokensInterface } from './mixins/mixin-tokens/mixin-tokens.js';
29
31
  export { anchorElement } from './utils/anchor-element/anchor-element.js';
30
32
  export { capturePointer } from './utils/capture-pointer/capture-pointer.js';
31
- export { closeMarkdown } from './utils/close-markdown/close-markdown.js';
32
33
  export { closestInstance } from './utils/closest-instance/closest-instance.js';
33
34
  export { createSheet } from './utils/create-sheet/create-sheet.js';
34
35
  export { customElement, definedTags, duplicateRegistrations, } from './utils/custom-element/custom-element.js';
package/dist/index.js CHANGED
@@ -25,10 +25,11 @@ export { MixinControllerOpen, } from './mixins/mixin-controller-open/mixin-contr
25
25
  export { MixinControllerValue, } from './mixins/mixin-controller-value/mixin-controller-value.js';
26
26
  export { MixinDismissible, } from './mixins/mixin-dismissible/mixin-dismissible.js';
27
27
  export { MixinField } from './mixins/mixin-field/mixin-field.js';
28
+ export { MixinPickerEditing, } from './mixins/mixin-picker-editing/mixin-picker-editing.js';
29
+ export { MixinPickerField, } from './mixins/mixin-picker-field/mixin-picker-field.js';
28
30
  export { MixinTokens } from './mixins/mixin-tokens/mixin-tokens.js';
29
31
  export { anchorElement } from './utils/anchor-element/anchor-element.js';
30
32
  export { capturePointer } from './utils/capture-pointer/capture-pointer.js';
31
- export { closeMarkdown } from './utils/close-markdown/close-markdown.js';
32
33
  export { closestInstance } from './utils/closest-instance/closest-instance.js';
33
34
  export { createSheet } from './utils/create-sheet/create-sheet.js';
34
35
  export { customElement, definedTags, duplicateRegistrations, } from './utils/custom-element/custom-element.js';
@@ -1,6 +1,6 @@
1
1
  import { ChartPointerController } from '../../controllers/chart-pointer-controller/chart-pointer-controller.js';
2
2
  import type { HelixElement } from '../../helix-element/helix-element.js';
3
- import { type Row, type Series } from '../../utils/read-series/read-series.js';
3
+ import type { Row, Series } from '../../utils/read-series/read-series.js';
4
4
  type Constructor<T> = new (...args: any[]) => T;
5
5
  /**
6
6
  * How many colors the series palette has, and so where it starts over.
@@ -7,7 +7,6 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { property } from 'lit/decorators.js';
8
8
  import { ChartPointerController } from '../../controllers/chart-pointer-controller/chart-pointer-controller.js';
9
9
  import { SourceController } from '../../controllers/source-controller/source-controller.js';
10
- import { readRows, readSeries } from '../../utils/read-series/read-series.js';
11
10
  export const PALETTE = 24;
12
11
  export const seriesIndex = (depth) => (depth % PALETTE) + 1;
13
12
  export const MixinChartSource = (Base) => {
@@ -26,15 +25,13 @@ export const MixinChartSource = (Base) => {
26
25
  }
27
26
  }
28
27
  get series() {
29
- const element = this.source.element;
30
- return element ? readSeries(element) : [];
28
+ return this.source.series;
31
29
  }
32
30
  get named() {
33
31
  return this.label || this.source.element?.querySelector('caption')?.textContent?.trim() || '';
34
32
  }
35
33
  get sourceRows() {
36
- const element = this.source.element;
37
- return element ? readRows(element) : [];
34
+ return this.source.rows;
38
35
  }
39
36
  get readings() {
40
37
  return this.sourceRows.length;
@@ -24,9 +24,9 @@ export const MixinControllerValue = (Base) => {
24
24
  this.requestUpdate('value', previous);
25
25
  }
26
26
  announceNatively() {
27
- this.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
27
+ this.announceInput();
28
28
  if (!this.announcesInput) {
29
- this.dispatchEvent(new Event('change', { bubbles: true, composed: false }));
29
+ this.announceChange();
30
30
  }
31
31
  }
32
32
  requestValue(next) {
@@ -72,7 +72,7 @@ export const MixinField = (Base) => {
72
72
  </p>`;
73
73
  }
74
74
  linkHost() {
75
- const label = this.renderRoot.querySelector('[part="label"]');
75
+ const label = this.partNamed('label');
76
76
  hostAria(this.internals, {
77
77
  describedBy: [HINT_ID, FEEDBACK_ID]
78
78
  .map((id) => this.renderRoot.querySelector(`#${id}`))
@@ -89,7 +89,7 @@ export const MixinField = (Base) => {
89
89
  }
90
90
  reportRequired(empty, message, anchor) {
91
91
  if (this.required && empty) {
92
- this.internals.setValidity({ valueMissing: true }, message, this.renderRoot?.querySelector(`[part="${anchor}"]`) ?? undefined);
92
+ this.internals.setValidity({ valueMissing: true }, message, this.partNamed(anchor) ?? undefined);
93
93
  }
94
94
  else {
95
95
  this.internals.setValidity({});
@@ -0,0 +1,48 @@
1
+ import type { HelixFormElement } from '../../helix-form-element/helix-form-element.js';
2
+ import type { MixinControllerOpenInterface } from '../mixin-controller-open/mixin-controller-open.js';
3
+ import type { MixinControllerValueInterface } from '../mixin-controller-value/mixin-controller-value.js';
4
+ import type { MixinPickerFieldInterface } from '../mixin-picker-field/mixin-picker-field.js';
5
+ type Constructor<T> = new (...args: any[]) => T;
6
+ /** What this is applied over: a picker field, which `MixinPickerField` makes. */
7
+ type EditingBase = HelixFormElement & MixinControllerOpenInterface & MixinControllerValueInterface & MixinPickerFieldInterface;
8
+ /**
9
+ * The public surface `MixinPickerEditing` adds.
10
+ *
11
+ * Declared separately because TypeScript cannot emit declarations for an
12
+ * anonymous class that has private members (TS4094). Casting the mixin's return
13
+ * to this interface keeps the internals private and the build working.
14
+ */
15
+ export declare class MixinPickerEditingInterface {
16
+ /**
17
+ * Whether the caret is in the field, which is when it holds the value.
18
+ */
19
+ protected editing: boolean;
20
+ /** The caret has left, so the field renders again. */
21
+ protected readonly handleBlur: () => void;
22
+ /** Focusing the field opens the picker, and only from outside the element. */
23
+ protected readonly handleFocus: (event: FocusEvent) => void;
24
+ /**
25
+ * Whether the control refuses changes while still submitting its value.
26
+ */
27
+ readOnly: boolean;
28
+ /** Refuses every change while read-only. See `readOnly`. */
29
+ requestValue(next: string): boolean;
30
+ }
31
+ /**
32
+ * Grafts the typing half of a picker field: it opens on arrival, holds the
33
+ * value while the caret is in it, and refuses while read-only.
34
+ *
35
+ * **Over `MixinPickerField` and not part of it, because Color Input takes the
36
+ * one and not the other.** Date Input and Time Input show a rendering while
37
+ * nobody is in the field, `4 August 2026` and `2:30 PM`, and the value itself
38
+ * while somebody is, because `Intl` formats and does not parse and a document
39
+ * being edited must not be a rendering of something else. That split needs to
40
+ * know when the caret is in the field, which is `editing`; and a reader who
41
+ * reaches such a field is choosing, so arriving opens the picker. A color has
42
+ * neither: `#ff0000` is what is typed and what is shown, and a reader who has
43
+ * the hex pastes it and never opens anything. `readOnly` is the third thing
44
+ * Color Input does not have, and a mixin cannot give it one without changing
45
+ * its API.
46
+ */
47
+ export declare const MixinPickerEditing: <T extends Constructor<EditingBase>>(Base: T) => Constructor<MixinPickerEditingInterface> & T;
48
+ export {};
@@ -0,0 +1,43 @@
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 { property, state } from 'lit/decorators.js';
8
+ export const MixinPickerEditing = (Base) => {
9
+ class PickerEditingElement extends Base {
10
+ constructor() {
11
+ super(...arguments);
12
+ this.readOnly = false;
13
+ this.editing = false;
14
+ this.handleBlur = () => {
15
+ this.editing = false;
16
+ };
17
+ this.handleFocus = (event) => {
18
+ this.editing = !this.returning;
19
+ const from = event.relatedTarget;
20
+ if (this.open || this.isDisabled || this.returning) {
21
+ return;
22
+ }
23
+ if (from instanceof Node && this.renderRoot?.contains?.(from)) {
24
+ return;
25
+ }
26
+ this.requestOpen(true);
27
+ };
28
+ }
29
+ requestValue(next) {
30
+ if (this.readOnly) {
31
+ return false;
32
+ }
33
+ return super.requestValue(next);
34
+ }
35
+ }
36
+ __decorate([
37
+ property({ attribute: 'readonly', reflect: true, type: Boolean })
38
+ ], PickerEditingElement.prototype, "readOnly", void 0);
39
+ __decorate([
40
+ state()
41
+ ], PickerEditingElement.prototype, "editing", void 0);
42
+ return PickerEditingElement;
43
+ };
@@ -0,0 +1,118 @@
1
+ import { PopoverController } from '../../controllers/popover-controller/popover-controller.js';
2
+ import type { HelixFormElement } from '../../helix-form-element/helix-form-element.js';
3
+ import type { MixinControllerOpenInterface } from '../mixin-controller-open/mixin-controller-open.js';
4
+ import type { MixinControllerValueInterface } from '../mixin-controller-value/mixin-controller-value.js';
5
+ import type { MixinFieldInterface } from '../mixin-field/mixin-field.js';
6
+ type Constructor<T> = new (...args: any[]) => T;
7
+ /**
8
+ * What this is applied over: a form field that opens, holds a value and has a
9
+ * label, which is the three controller mixins and `MixinField` together.
10
+ */
11
+ type PickerBase = HelixFormElement & MixinControllerOpenInterface & MixinControllerValueInterface & MixinFieldInterface;
12
+ /**
13
+ * The public surface `MixinPickerField` adds.
14
+ *
15
+ * Declared separately because TypeScript cannot emit declarations for an
16
+ * anonymous class that has private members (TS4094). Casting the mixin's return
17
+ * to this interface keeps the internals private and the build working.
18
+ */
19
+ export declare class MixinPickerFieldInterface {
20
+ /**
21
+ * What the label points at and focus lands on.
22
+ *
23
+ * The text control, unless a family says otherwise: Color Input in `swatch`
24
+ * has no text control, and its trigger is the only thing in the field a label
25
+ * can name.
26
+ */
27
+ protected get anchor(): HTMLElement | null;
28
+ /** The text input, or `null` where there is no DOM to hold one. */
29
+ protected get control(): HTMLInputElement | null;
30
+ /**
31
+ * What the field shows, which is the value unless a family renders it.
32
+ *
33
+ * Date Input and Time Input show a written date or time while nobody is in
34
+ * the field and the value itself while somebody is; Color Input shows the
35
+ * value either way. What `restore` puts back is whichever the family says.
36
+ */
37
+ protected get display(): string;
38
+ /** Puts the caret in the field, the way a label pointing at one would. */
39
+ focus(options?: FocusOptions): void;
40
+ /** Escape shuts the panel without moving the value. */
41
+ protected readonly handleKeydown: (event: KeyboardEvent) => void;
42
+ /** The overlay, placed against the field rather than the trigger. */
43
+ protected readonly panel: PopoverController;
44
+ /** Puts the field back where the answer differs from what was typed. */
45
+ protected restore(): void;
46
+ /** Puts the caret back in the field without the field reopening the panel. */
47
+ protected returnFocus(): void;
48
+ /** Whether this element is moving the focus itself, right now. */
49
+ protected returning: boolean;
50
+ /** Reports the value and the validity the host stands behind. */
51
+ protected sync(): void;
52
+ /**
53
+ * What is wrong with the value, in the reader's words, or nothing.
54
+ *
55
+ * The one check the platform cannot make: the control is `type="text"` and
56
+ * has no idea what a date, a time or a color is, so the family says what one
57
+ * looks like and `sync` reports it as `badInput`. Empty means the platform's
58
+ * own verdict stands.
59
+ */
60
+ protected validate(): string;
61
+ }
62
+ /**
63
+ * Grafts a floating picker onto a text field, and the protocol between them.
64
+ *
65
+ * **Three families are this shape and were three copies of it.** Date Input,
66
+ * Time Input and Color Input each render a text control, a trigger and a
67
+ * `popover="manual"` surface holding a picker that already exists on its own,
68
+ * and each owed the same wiring: the panel that floats the surface against the
69
+ * field, the Escape that shuts it and hands the caret back, the restore that
70
+ * puts the control back where a refusal left the value, and the sync that
71
+ * reports the value and a verdict the platform cannot reach. Measured on the
72
+ * diff that made the lift, 14 Sep 2026, comments counted: 223 lines left Date
73
+ * Input and 32 came back, 185 left Time Input and 32 came back, 87 left
74
+ * Color Input and 11 came back.
75
+ *
76
+ * **What it does not own is the picker, the typing or the value's shape.**
77
+ * Which element floats in the surface, what a keystroke means, what the field
78
+ * shows when idle and what is wrong with a value are each the family's, which
79
+ * is why `display` and `validate` are seams here and the template stays where
80
+ * it was. A family renders `field`, `control`, `trigger` and `surface`; this
81
+ * reads them.
82
+ *
83
+ * **The parts this reads are the host's, so the name is a parameter.** A
84
+ * literal `[part="control"]` here would be a lookup nothing in this file
85
+ * renders, which is the fault `helix component`'s `selectors` exists to catch
86
+ * and would rightly report. `MixinField.reportRequired` takes its anchor the
87
+ * same way, and for the same reason: a mixin reads a part out of whatever host
88
+ * applies it.
89
+ *
90
+ * **Opening on focus, `readOnly` and the idle rendering are not here.** They
91
+ * are `MixinPickerEditing`'s, because Color Input has none of the three: a hex
92
+ * is pasted and read at a glance, so its field neither renders nor opens on
93
+ * arrival, and giving it `readOnly` would be a property its API does not have.
94
+ *
95
+ * **Three things the two typed families share stay with them.** `renderTrigger`
96
+ * differs only by its icon and would still be the wrong thing to lift: a part a
97
+ * mixin renders belongs to every host that applies it, which is how
98
+ * `helix component`'s `parts` reads a chain, so `trigger-icon` would be
99
+ * credited to Color Input, whose trigger is a swatch and documents no such
100
+ * part. `renderTag` names its part after the family, `date` or `time`, and
101
+ * writes the label on the remove control differently in each. `valueAsDate`
102
+ * and `valueAsNumber` are public, convert a different way, and carry a
103
+ * different note onto each family's page, which a mixin could only carry as
104
+ * one.
105
+ *
106
+ * **Four more families look like this and are not.** Select has no text to
107
+ * hand the caret to: its panel hangs off the trigger and dismissal refocuses
108
+ * that. Combobox has the text and a different agreement with it: a press
109
+ * anywhere in the field is inside, the gap is four, Escape closes without the
110
+ * `returning` flag because nothing there opens on arrival, and the control is
111
+ * reconciled against a completion rather than restored to a value. Input and
112
+ * Textarea open nothing, and their `readOnly` is the native attribute rather
113
+ * than a gate on `requestValue`, so a programmatic change still lands. What
114
+ * those two share with this is `focus()` and the platform's verdict copied in
115
+ * `sync`, which is not a protocol.
116
+ */
117
+ export declare const MixinPickerField: <T extends Constructor<PickerBase>>(Base: T) => Constructor<MixinPickerFieldInterface> & T;
118
+ export {};
@@ -0,0 +1,79 @@
1
+ import { PopoverController } from '../../controllers/popover-controller/popover-controller.js';
2
+ export const MixinPickerField = (Base) => {
3
+ class PickerFieldElement extends Base {
4
+ constructor() {
5
+ super(...arguments);
6
+ this.panel = new PopoverController(this, {
7
+ anchor: () => this.partNamed('field'),
8
+ boundary: () => [
9
+ this.partNamed('surface'),
10
+ this.partNamed('trigger'),
11
+ ],
12
+ offset: () => 8,
13
+ onDismiss: () => this.requestOpen(false),
14
+ open: () => this.open,
15
+ placement: 'bottom-start',
16
+ surface: () => this.partNamed('surface'),
17
+ });
18
+ this.handleKeydown = (event) => {
19
+ if (event.key === 'Escape' && this.open) {
20
+ event.stopPropagation();
21
+ this.requestOpen(false);
22
+ this.returnFocus();
23
+ }
24
+ };
25
+ this.returning = false;
26
+ }
27
+ get anchor() {
28
+ return this.control;
29
+ }
30
+ get control() {
31
+ return this.partNamed('control');
32
+ }
33
+ get display() {
34
+ return this.value;
35
+ }
36
+ validate() {
37
+ return '';
38
+ }
39
+ restore() {
40
+ const control = this.control;
41
+ if (control && control.value !== this.display) {
42
+ control.value = this.display;
43
+ }
44
+ this.sync();
45
+ }
46
+ sync() {
47
+ this.internals.setFormValue(this.value);
48
+ const control = this.control;
49
+ if (!control) {
50
+ return;
51
+ }
52
+ const message = this.validate();
53
+ if (message === '') {
54
+ this.internals.setValidity(control.validity, control.validationMessage, control);
55
+ }
56
+ else {
57
+ this.internals.setValidity({ badInput: true }, message, control);
58
+ }
59
+ this.refreshFeedback();
60
+ }
61
+ returnFocus() {
62
+ this.returning = true;
63
+ this.control?.focus();
64
+ this.returning = false;
65
+ }
66
+ focus(options) {
67
+ this.anchor?.focus(options);
68
+ }
69
+ firstUpdated(changed) {
70
+ super.firstUpdated(changed);
71
+ this.sync();
72
+ }
73
+ updated(changed) {
74
+ super.updated(changed);
75
+ this.sync();
76
+ }
77
+ }
78
+ return PickerFieldElement;
79
+ };
@@ -29,7 +29,7 @@ export interface DrawIconOptions {
29
29
  * the thing static analysis has to read.
30
30
  *
31
31
  * `data-glyph` rather than a class, because the sizing lives in
32
- * `helix/styled/src/icon/glyph.css` and a component's own `--icon-size`
32
+ * `helix/styled/src/shared/glyph/glyph.css` and a component's own `--icon-size`
33
33
  * declarations have to keep working untouched: the variable was read by
34
34
  * `helix-icon`, and now it is read by the rule that sheet carries. Without it
35
35
  * every one of those declarations would go quietly dead and every mark in six
@@ -31,6 +31,7 @@ export const drawTooltip = (rows, at, tag, where) => {
31
31
  ${row.cells.map((cell, index) => html `<span
32
32
  data-series=${cell.value === undefined ? nothing : seriesIndex(index)}
33
33
  part="tooltip-cell"
34
+ style=${cell.value === undefined ? nothing : `--chart-series: var(--chart-series-${seriesIndex(index)})`}
34
35
  >
35
36
  <span part="tooltip-name">${cell.name}</span>
36
37
  <span part="tooltip-value">${cell.text}</span>