@fusion.dev/helix-core 0.26.0 → 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.
- package/custom-elements.json +833 -114
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +23 -1
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +23 -26
- package/dist/controllers/dismiss-controller/dismiss-controller.d.ts +27 -0
- package/dist/controllers/dismiss-controller/dismiss-controller.js +17 -2
- package/dist/controllers/floating-controller/floating-controller.d.ts +13 -0
- package/dist/controllers/floating-controller/floating-controller.js +3 -1
- package/dist/controllers/indicator-controller/indicator-controller.d.ts +16 -0
- package/dist/controllers/indicator-controller/indicator-controller.js +7 -0
- package/dist/controllers/popover-controller/popover-controller.d.ts +22 -0
- package/dist/controllers/popover-controller/popover-controller.js +12 -0
- package/dist/controllers/source-controller/source-controller.d.ts +45 -3
- package/dist/controllers/source-controller/source-controller.js +33 -21
- package/dist/helix-element/helix-element.d.ts +19 -0
- package/dist/helix-element/helix-element.js +5 -94
- package/dist/helix-element/react-bridge.d.ts +104 -0
- package/dist/helix-element/react-bridge.js +94 -0
- package/dist/helix-form-element/helix-form-element.d.ts +19 -0
- package/dist/helix-form-element/helix-form-element.js +6 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +1 -1
- package/dist/mixins/mixin-chart-source/mixin-chart-source.js +2 -5
- package/dist/mixins/mixin-controller-value/mixin-controller-value.js +2 -2
- package/dist/mixins/mixin-field/mixin-field.js +2 -2
- package/dist/mixins/mixin-picker-editing/mixin-picker-editing.d.ts +48 -0
- package/dist/mixins/mixin-picker-editing/mixin-picker-editing.js +43 -0
- package/dist/mixins/mixin-picker-field/mixin-picker-field.d.ts +118 -0
- package/dist/mixins/mixin-picker-field/mixin-picker-field.js +79 -0
- package/dist/utils/draw-icon/draw-icon.d.ts +1 -1
- package/dist/utils/draw-tooltip/draw-tooltip.js +1 -0
- package/package.json +4 -4
- package/dist/mixins/mixin-collection/index.d.ts +0 -1
- package/dist/mixins/mixin-collection/index.js +0 -1
- package/dist/utils/close-markdown/close-markdown.d.ts +0 -39
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
27
|
+
this.announceInput();
|
|
28
28
|
if (!this.announcesInput) {
|
|
29
|
-
this.
|
|
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.
|
|
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.
|
|
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/
|
|
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>
|