@fusion.dev/helix-core 0.142.1 → 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.
- package/custom-elements.json +1475 -54
- package/dist/controllers/aria-forward-controller/aria-forward-controller.d.ts +55 -0
- package/dist/controllers/aria-forward-controller/aria-forward-controller.js +72 -16
- package/dist/controllers/arrival-controller/arrival-controller.d.ts +12 -0
- package/dist/controllers/arrival-controller/arrival-controller.js +10 -0
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +10 -0
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +5 -1
- package/dist/controllers/copy-controller/copy-controller.js +5 -0
- package/dist/controllers/dismiss-controller/dismiss-controller.js +40 -3
- package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.d.ts +79 -1
- package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.js +80 -10
- package/dist/controllers/popover-controller/popover-controller.d.ts +11 -0
- package/dist/controllers/popover-controller/popover-controller.js +6 -0
- package/dist/controllers/source-controller/source-controller.d.ts +22 -0
- package/dist/controllers/source-controller/source-controller.js +27 -1
- package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +16 -0
- package/dist/controllers/virtualize-controller/virtualize-controller.js +24 -2
- package/dist/events/events.d.ts +102 -4
- package/dist/events/events.js +84 -0
- package/dist/helix-element/react-bridge.js +6 -4
- package/dist/helix-form-element/helix-form-element.d.ts +131 -2
- package/dist/helix-form-element/helix-form-element.js +216 -6
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -3
- package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +8 -0
- package/dist/mixins/mixin-chart-source/mixin-chart-source.js +4 -0
- package/dist/mixins/mixin-collection/mixin-collection.d.ts +4 -2
- package/dist/mixins/mixin-collection/mixin-collection.js +8 -6
- package/dist/mixins/mixin-controller-open/mixin-controller-open.js +19 -3
- package/dist/mixins/mixin-dismissible/mixin-dismissible.js +39 -0
- package/dist/mixins/mixin-field/mixin-field.js +38 -8
- package/dist/mixins/mixin-picker-editing/mixin-picker-editing.d.ts +8 -3
- package/dist/mixins/mixin-picker-editing/mixin-picker-editing.js +6 -0
- package/dist/mixins/mixin-picker-field/mixin-picker-field.d.ts +27 -4
- package/dist/mixins/mixin-picker-field/mixin-picker-field.js +83 -1
- package/dist/utils/draw-tooltip/draw-tooltip.d.ts +3 -0
- package/dist/utils/draw-tooltip/draw-tooltip.js +5 -0
- package/dist/utils/read-series/read-series.d.ts +28 -0
- package/dist/utils/read-series/read-series.js +31 -4
- package/dist/utils/report-runtime/report-runtime.d.ts +10 -0
- package/dist/utils/scroll-lock/scroll-lock.d.ts +16 -0
- package/dist/utils/scroll-lock/scroll-lock.js +30 -0
- package/dist/utils/written-list/written-list.d.ts +11 -2
- package/dist/utils/written-list/written-list.js +3 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -4
|
@@ -16,6 +16,19 @@ export const MixinField = (Base) => {
|
|
|
16
16
|
this.controlId = 'control';
|
|
17
17
|
this.labelId = 'label';
|
|
18
18
|
this.namedBy = null;
|
|
19
|
+
this.filledSlots = new Set();
|
|
20
|
+
this.noteSlotted = (event) => {
|
|
21
|
+
const slot = event.target;
|
|
22
|
+
if (slot.name !== 'label' && slot.name !== 'hint') {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const filled = slot.assignedNodes().length > 0;
|
|
26
|
+
if (filled !== this.filledSlots.has(slot.name)) {
|
|
27
|
+
const next = new Set(this.filledSlots);
|
|
28
|
+
next[filled ? 'add' : 'delete'](slot.name);
|
|
29
|
+
this.filledSlots = next;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
19
32
|
this.feedback = '';
|
|
20
33
|
this.hint = '';
|
|
21
34
|
this.label = '';
|
|
@@ -24,7 +37,15 @@ export const MixinField = (Base) => {
|
|
|
24
37
|
this.shownFeedback = '';
|
|
25
38
|
}
|
|
26
39
|
get labeled() {
|
|
27
|
-
return this.label !== '' ||
|
|
40
|
+
return (this.label !== '' ||
|
|
41
|
+
Boolean(this.getAttribute('aria-label')) ||
|
|
42
|
+
this.filledSlots.has('label'));
|
|
43
|
+
}
|
|
44
|
+
bareSlot(name) {
|
|
45
|
+
return name === 'label' ? html `<slot name="label"></slot>` : html `<slot name="hint"></slot>`;
|
|
46
|
+
}
|
|
47
|
+
get validityFeedback() {
|
|
48
|
+
return this.validates ? this.feedback : '';
|
|
28
49
|
}
|
|
29
50
|
get validates() {
|
|
30
51
|
return this.constructor.formAssociated === true;
|
|
@@ -42,15 +63,15 @@ export const MixinField = (Base) => {
|
|
|
42
63
|
if (this.resolvedFeedback !== '') {
|
|
43
64
|
return FEEDBACK_ID;
|
|
44
65
|
}
|
|
45
|
-
return this.hint === '' ? undefined : HINT_ID;
|
|
66
|
+
return this.hint === '' && !this.filledSlots.has('hint') ? undefined : HINT_ID;
|
|
46
67
|
}
|
|
47
68
|
get invalidity() {
|
|
48
69
|
return this.resolvedFeedback === '' ? undefined : 'true';
|
|
49
70
|
}
|
|
50
71
|
renderGroupLabel() {
|
|
51
72
|
const hidden = this.label === '' ? this.getAttribute('aria-label') : null;
|
|
52
|
-
if (this.label === '' && !hidden) {
|
|
53
|
-
return
|
|
73
|
+
if (this.label === '' && !hidden && !this.filledSlots.has('label')) {
|
|
74
|
+
return this.bareSlot('label');
|
|
54
75
|
}
|
|
55
76
|
return html `<span part="label" id=${this.labelId} ?data-hidden=${hidden !== null}
|
|
56
77
|
><slot name="label">${this.label || hidden}</slot></span
|
|
@@ -58,8 +79,8 @@ export const MixinField = (Base) => {
|
|
|
58
79
|
}
|
|
59
80
|
renderLabel() {
|
|
60
81
|
const hidden = this.label === '' ? this.getAttribute('aria-label') : null;
|
|
61
|
-
if (this.label === '' && !hidden) {
|
|
62
|
-
return
|
|
82
|
+
if (this.label === '' && !hidden && !this.filledSlots.has('label')) {
|
|
83
|
+
return this.bareSlot('label');
|
|
63
84
|
}
|
|
64
85
|
return html `<label
|
|
65
86
|
part="label"
|
|
@@ -70,10 +91,15 @@ export const MixinField = (Base) => {
|
|
|
70
91
|
>`;
|
|
71
92
|
}
|
|
72
93
|
renderHint() {
|
|
73
|
-
if (this.
|
|
94
|
+
if (this.resolvedFeedback !== '') {
|
|
74
95
|
return nothing;
|
|
75
96
|
}
|
|
76
|
-
|
|
97
|
+
if (this.hint === '' && !this.filledSlots.has('hint')) {
|
|
98
|
+
return this.bareSlot('hint');
|
|
99
|
+
}
|
|
100
|
+
return html `<p part="hint" id=${HINT_ID}
|
|
101
|
+
><slot name="hint">${this.hint}</slot></p
|
|
102
|
+
>`;
|
|
77
103
|
}
|
|
78
104
|
renderFeedback() {
|
|
79
105
|
const message = this.resolvedFeedback;
|
|
@@ -133,11 +159,15 @@ export const MixinField = (Base) => {
|
|
|
133
159
|
};
|
|
134
160
|
this.addEventListener('invalid', touch);
|
|
135
161
|
this.addEventListener('change', touch);
|
|
162
|
+
this.renderRoot.addEventListener('slotchange', this.noteSlotted);
|
|
136
163
|
}
|
|
137
164
|
}
|
|
138
165
|
__decorate([
|
|
139
166
|
property({ attribute: 'aria-label' })
|
|
140
167
|
], FieldElement.prototype, "namedBy", void 0);
|
|
168
|
+
__decorate([
|
|
169
|
+
state()
|
|
170
|
+
], FieldElement.prototype, "filledSlots", void 0);
|
|
141
171
|
__decorate([
|
|
142
172
|
property()
|
|
143
173
|
], FieldElement.prototype, "feedback", void 0);
|
|
@@ -30,6 +30,11 @@ export declare class MixinPickerEditingInterface {
|
|
|
30
30
|
* returns `false`.
|
|
31
31
|
*/
|
|
32
32
|
requestValue(next: string): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Requests the panel open or closed. While `readOnly` is set, it does not
|
|
35
|
+
* open, and it returns `false`.
|
|
36
|
+
*/
|
|
37
|
+
requestOpen(next: boolean): boolean;
|
|
33
38
|
}
|
|
34
39
|
/**
|
|
35
40
|
* Grafts the typing half of a picker field: it opens on arrival, holds the
|
|
@@ -43,9 +48,9 @@ export declare class MixinPickerEditingInterface {
|
|
|
43
48
|
* know when the caret is in the field, which is `editing`; and a reader who
|
|
44
49
|
* reaches such a field is choosing, so arriving opens the picker. A color has
|
|
45
50
|
* neither: `#ff0000` is what is typed and what is shown, and a reader who has
|
|
46
|
-
* the hex pastes it and never opens anything. `readOnly`
|
|
47
|
-
*
|
|
48
|
-
*
|
|
51
|
+
* the hex pastes it and never opens anything. `readOnly` comes along here for
|
|
52
|
+
* the two that take this mixin; Color Input declares its own, the same
|
|
53
|
+
* property refusing the same two requests.
|
|
49
54
|
*/
|
|
50
55
|
export declare const MixinPickerEditing: <T extends Constructor<EditingBase>>(Base: T) => Constructor<MixinPickerEditingInterface> & T;
|
|
51
56
|
export {};
|
|
@@ -32,6 +32,12 @@ export const MixinPickerEditing = (Base) => {
|
|
|
32
32
|
}
|
|
33
33
|
return super.requestValue(next);
|
|
34
34
|
}
|
|
35
|
+
requestOpen(next) {
|
|
36
|
+
if (next && this.readOnly) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
return super.requestOpen(next);
|
|
40
|
+
}
|
|
35
41
|
}
|
|
36
42
|
__decorate([
|
|
37
43
|
property({ attribute: 'readonly', reflect: true, type: Boolean })
|
|
@@ -45,7 +45,22 @@ export declare class MixinPickerFieldInterface {
|
|
|
45
45
|
protected get display(): string;
|
|
46
46
|
/** Puts the caret in the field, as clicking its label does. */
|
|
47
47
|
focus(options?: FocusOptions): void;
|
|
48
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* The text control's own `change`, which a reader's edit ends with, said
|
|
50
|
+
* under the host's name.
|
|
51
|
+
*/
|
|
52
|
+
protected readonly handleCommit: (event: Event) => void;
|
|
53
|
+
/**
|
|
54
|
+
* The trigger's press: opens the panel with focus in the picker, or closes
|
|
55
|
+
* it.
|
|
56
|
+
*/
|
|
57
|
+
protected readonly handleTriggerClick: () => void;
|
|
58
|
+
/** Enter in the text submits the form, as it does in a native text field. */
|
|
59
|
+
protected readonly handleKeypress: (event: KeyboardEvent) => void;
|
|
60
|
+
/**
|
|
61
|
+
* Escape shuts the panel without moving the value, Tab out of the text shuts
|
|
62
|
+
* a panel the reader is passing, and ArrowDown in the text moves into it.
|
|
63
|
+
*/
|
|
49
64
|
protected readonly handleKeydown: (event: KeyboardEvent) => void;
|
|
50
65
|
/** The overlay, placed against the field rather than the trigger. */
|
|
51
66
|
protected readonly panel: PopoverController;
|
|
@@ -56,6 +71,14 @@ export declare class MixinPickerFieldInterface {
|
|
|
56
71
|
* Nothing, unless a family fits what its panel holds to the room it has.
|
|
57
72
|
*/
|
|
58
73
|
protected placed(room: number | undefined): void;
|
|
74
|
+
/**
|
|
75
|
+
* Takes a value the reader picked in the panel, and says it is settled.
|
|
76
|
+
*
|
|
77
|
+
* Returns whether it was taken. A family whose typing announces `input`
|
|
78
|
+
* alone sends `change` from here for a pick, since a pick is a finished
|
|
79
|
+
* choice.
|
|
80
|
+
*/
|
|
81
|
+
protected requestPicked(next: string): boolean;
|
|
59
82
|
/** Puts the field back where the answer differs from what was typed. */
|
|
60
83
|
protected restore(): void;
|
|
61
84
|
/** Puts the caret back in the field without the field reopening the panel. */
|
|
@@ -103,9 +126,9 @@ export declare class MixinPickerFieldInterface {
|
|
|
103
126
|
* applies it.
|
|
104
127
|
*
|
|
105
128
|
* **Opening on focus, `readOnly` and the idle rendering are not here.** They
|
|
106
|
-
* are `MixinPickerEditing`'s, because Color Input
|
|
107
|
-
* is pasted and read at a glance, so its field neither renders
|
|
108
|
-
*
|
|
129
|
+
* are `MixinPickerEditing`'s, because Color Input takes neither of the first
|
|
130
|
+
* and last: a hex is pasted and read at a glance, so its field neither renders
|
|
131
|
+
* nor opens on arrival. Color Input declares its own `readOnly`.
|
|
109
132
|
*
|
|
110
133
|
* **Three things the two typed families share stay with them.** `renderTrigger`
|
|
111
134
|
* differs only by its icon and would still be the wrong thing to lift: a part a
|
|
@@ -29,7 +29,63 @@ export const MixinPickerField = (Base) => {
|
|
|
29
29
|
event.stopPropagation();
|
|
30
30
|
this.requestOpen(false);
|
|
31
31
|
this.returnFocus();
|
|
32
|
+
return;
|
|
32
33
|
}
|
|
34
|
+
const fromText = event.composedPath()[0] === this.control;
|
|
35
|
+
if (event.key === 'Tab' && this.open && fromText) {
|
|
36
|
+
this.requestOpen(false);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (event.key === 'ArrowDown' && fromText && !this.isDisabled) {
|
|
40
|
+
event.preventDefault();
|
|
41
|
+
if (this.open) {
|
|
42
|
+
this.partNamed('picker')?.focus();
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
this.fromTrigger = true;
|
|
46
|
+
if (!this.requestOpen(true)) {
|
|
47
|
+
this.fromTrigger = false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
this.handleSurfaceKeydown = (event) => {
|
|
53
|
+
const surface = this.partNamed('surface');
|
|
54
|
+
if (event.key !== 'Escape' ||
|
|
55
|
+
!this.open ||
|
|
56
|
+
event.defaultPrevented ||
|
|
57
|
+
!surface ||
|
|
58
|
+
!event.composedPath().includes(surface)) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
event.stopPropagation();
|
|
62
|
+
event.preventDefault();
|
|
63
|
+
this.requestOpen(false);
|
|
64
|
+
this.returnFocus();
|
|
65
|
+
};
|
|
66
|
+
this.handleFocusOut = (event) => {
|
|
67
|
+
const to = event.relatedTarget;
|
|
68
|
+
if (!this.open || !(to instanceof Node)) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (this.contains(to) || this.renderRoot?.contains?.(to)) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
this.requestOpen(false);
|
|
75
|
+
};
|
|
76
|
+
this.handleCommit = (event) => {
|
|
77
|
+
event.stopPropagation();
|
|
78
|
+
this.announceChange();
|
|
79
|
+
};
|
|
80
|
+
this.fromTrigger = false;
|
|
81
|
+
this.handleKeypress = (event) => {
|
|
82
|
+
if (event.key === 'Enter' && !event.defaultPrevented) {
|
|
83
|
+
this.submitImplicitly();
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
this.handleTriggerClick = () => {
|
|
87
|
+
this.fromTrigger = !this.open;
|
|
88
|
+
this.toggleOpen();
|
|
33
89
|
};
|
|
34
90
|
this.returning = false;
|
|
35
91
|
}
|
|
@@ -78,9 +134,29 @@ export const MixinPickerField = (Base) => {
|
|
|
78
134
|
}
|
|
79
135
|
this.refreshFeedback();
|
|
80
136
|
}
|
|
137
|
+
get blocksImplicitSubmission() {
|
|
138
|
+
return this.control !== null;
|
|
139
|
+
}
|
|
140
|
+
requestPicked(next) {
|
|
141
|
+
const taken = this.requestValue(next);
|
|
142
|
+
if (taken && this.announcesInput) {
|
|
143
|
+
this.announceChange();
|
|
144
|
+
}
|
|
145
|
+
return taken;
|
|
146
|
+
}
|
|
147
|
+
connectedCallback() {
|
|
148
|
+
super.connectedCallback();
|
|
149
|
+
this.addEventListener('keydown', this.handleSurfaceKeydown);
|
|
150
|
+
this.addEventListener('focusout', this.handleFocusOut);
|
|
151
|
+
}
|
|
152
|
+
disconnectedCallback() {
|
|
153
|
+
super.disconnectedCallback();
|
|
154
|
+
this.removeEventListener('keydown', this.handleSurfaceKeydown);
|
|
155
|
+
this.removeEventListener('focusout', this.handleFocusOut);
|
|
156
|
+
}
|
|
81
157
|
returnFocus() {
|
|
82
158
|
this.returning = true;
|
|
83
|
-
this.
|
|
159
|
+
this.anchor?.focus();
|
|
84
160
|
this.returning = false;
|
|
85
161
|
}
|
|
86
162
|
focus(options) {
|
|
@@ -92,6 +168,12 @@ export const MixinPickerField = (Base) => {
|
|
|
92
168
|
}
|
|
93
169
|
updated(changed) {
|
|
94
170
|
super.updated(changed);
|
|
171
|
+
if (changed.has('open')) {
|
|
172
|
+
if (this.open && this.fromTrigger) {
|
|
173
|
+
this.partNamed('picker')?.focus();
|
|
174
|
+
}
|
|
175
|
+
this.fromTrigger = false;
|
|
176
|
+
}
|
|
95
177
|
this.sync();
|
|
96
178
|
}
|
|
97
179
|
}
|
|
@@ -89,5 +89,8 @@ export interface TooltipAt {
|
|
|
89
89
|
* **Hidden from assistive technology, on purpose.** The table is the accessible
|
|
90
90
|
* representation and the drawing is `aria-hidden`; a reader on a keyboard gets
|
|
91
91
|
* the table, which they can reach, rather than a tooltip which they cannot.
|
|
92
|
+
* The mark and the bubble carry `aria-hidden` themselves, so they stay hidden
|
|
93
|
+
* in a chart whose plot also holds something a reader should hear, such as
|
|
94
|
+
* the middle of a donut.
|
|
92
95
|
*/
|
|
93
96
|
export declare const drawTooltip: (rows: Row[], at: number, tag: StaticValue, where?: TooltipAt, slotOf?: (cell: number) => number | undefined) => TemplateResult | typeof nothing;
|
|
@@ -3,6 +3,7 @@ import { styleMap } from 'lit/directives/style-map.js';
|
|
|
3
3
|
import { html } from 'lit/static-html.js';
|
|
4
4
|
import { seriesIndex } from '../../mixins/mixin-chart-source/mixin-chart-source.js';
|
|
5
5
|
export const CHART_PLOT = 'helix-chart-plot';
|
|
6
|
+
const CHART_TOOLTIP = 'helix-chart-tooltip';
|
|
6
7
|
export const drawTooltip = (rows, at, tag, where, slotOf) => {
|
|
7
8
|
const row = rows[at];
|
|
8
9
|
if (!row || rows.length === 0) {
|
|
@@ -15,6 +16,8 @@ export const drawTooltip = (rows, at, tag, where, slotOf) => {
|
|
|
15
16
|
y: '0%',
|
|
16
17
|
};
|
|
17
18
|
return html `<div
|
|
19
|
+
aria-describedby=${CHART_TOOLTIP}
|
|
20
|
+
aria-hidden="true"
|
|
18
21
|
id="helix-chart-mark"
|
|
19
22
|
part="mark"
|
|
20
23
|
style=${styleMap(place.fixed
|
|
@@ -22,12 +25,14 @@ export const drawTooltip = (rows, at, tag, where, slotOf) => {
|
|
|
22
25
|
: { insetBlockStart: place.y, insetInlineStart: place.x })}
|
|
23
26
|
></div>
|
|
24
27
|
<${tag}
|
|
28
|
+
aria-hidden="true"
|
|
25
29
|
dir=${place.dir ?? nothing}
|
|
26
30
|
exportparts="arrow: tooltip-arrow, arrow-bottom: tooltip-arrow-bottom,
|
|
27
31
|
arrow-left: tooltip-arrow-left, arrow-right: tooltip-arrow-right,
|
|
28
32
|
arrow-top: tooltip-arrow-top, tooltip, tooltip-bottom, tooltip-left, tooltip-right,
|
|
29
33
|
tooltip-top"
|
|
30
34
|
for="helix-chart-mark"
|
|
35
|
+
id=${CHART_TOOLTIP}
|
|
31
36
|
open
|
|
32
37
|
part="tooltip-anchor"
|
|
33
38
|
placement=${place.placement}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
/** One point in a series: what it is called, what it is worth, and when. */
|
|
2
2
|
export interface SeriesPoint {
|
|
3
|
+
/**
|
|
4
|
+
* The row the reading is in, counting from zero, which is its place among
|
|
5
|
+
* the rows `readRows` reads and so the category it is drawn at.
|
|
6
|
+
*/
|
|
7
|
+
at: number;
|
|
3
8
|
/**
|
|
4
9
|
* The row's own label, taken from its header cell.
|
|
5
10
|
*/
|
|
@@ -50,8 +55,31 @@ export interface Series {
|
|
|
50
55
|
* the second reads `12abc` as 12: a value that is nearly a number is a mistake
|
|
51
56
|
* worth dropping rather than rounding off, and a dropped point is visible where
|
|
52
57
|
* a silently truncated one is not.
|
|
58
|
+
*
|
|
59
|
+
* **A dropped point keeps its place.** Each point carries `at`, the row it was
|
|
60
|
+
* read from, and a chart draws it at that row's category. Counted by position
|
|
61
|
+
* in `points`, a blank in March slid April's reading under March's label, and a
|
|
62
|
+
* blank in the first column took a category off the axis for every series.
|
|
63
|
+
* `byRow` lays a series out one slot per row with the blanks left empty.
|
|
53
64
|
*/
|
|
54
65
|
export declare const readSeries: (table: Element) => Series[];
|
|
66
|
+
/**
|
|
67
|
+
* A series laid out one slot per row, with nothing in a slot whose cell was
|
|
68
|
+
* blank or did not parse.
|
|
69
|
+
*
|
|
70
|
+
* This is the shape a chart drawing categories wants: slot `i` is the reading at
|
|
71
|
+
* the `i`th row, which is the label under it and the row the tooltip reads.
|
|
72
|
+
*/
|
|
73
|
+
export declare const byRow: (series: Series | undefined, rows: number) => (SeriesPoint | undefined)[];
|
|
74
|
+
/**
|
|
75
|
+
* A series cut into its unbroken runs, where a run ends at a row with no
|
|
76
|
+
* reading.
|
|
77
|
+
*
|
|
78
|
+
* A line drawn through a gap claims a value in the gap. Drawn as runs, the
|
|
79
|
+
* line stops at the last reading before a blank and starts again at the first
|
|
80
|
+
* one after it.
|
|
81
|
+
*/
|
|
82
|
+
export declare const unbroken: (series: Series) => SeriesPoint[][];
|
|
55
83
|
/** One cell of a row, as a reader sees it and as a chart measures it. */
|
|
56
84
|
export interface RowCell {
|
|
57
85
|
/**
|
|
@@ -6,13 +6,13 @@ const isGrid = (element) => element.getAttribute('role') === 'table' ||
|
|
|
6
6
|
/^helix-(headless-)?table$/i.test(element.tagName);
|
|
7
7
|
const readGrid = (grid) => {
|
|
8
8
|
const columns = [...grid.querySelectorAll(COLUMNS)];
|
|
9
|
-
const rows =
|
|
9
|
+
const rows = gridRows(grid);
|
|
10
10
|
const names = columns.slice(1).map((cell) => cell.textContent?.trim() ?? '');
|
|
11
11
|
const cellsOf = (row) => [...row.querySelectorAll(CELLS)];
|
|
12
12
|
const count = Math.max(0, ...rows.map((row) => cellsOf(row).length - 1));
|
|
13
13
|
return Array.from({ length: count }, (_, column) => ({
|
|
14
14
|
name: names[column] ?? grid.getAttribute('label') ?? '',
|
|
15
|
-
points: rows.flatMap((row) => {
|
|
15
|
+
points: rows.flatMap((row, at) => {
|
|
16
16
|
const cells = cellsOf(row);
|
|
17
17
|
const head = cells[0];
|
|
18
18
|
const cell = cells[column + 1];
|
|
@@ -26,6 +26,7 @@ const readGrid = (grid) => {
|
|
|
26
26
|
return Number.isFinite(value) && raw.trim() !== ''
|
|
27
27
|
? [
|
|
28
28
|
{
|
|
29
|
+
at,
|
|
29
30
|
label: head?.textContent?.trim() ?? '',
|
|
30
31
|
value,
|
|
31
32
|
...(tone ? { tone } : {}),
|
|
@@ -36,6 +37,7 @@ const readGrid = (grid) => {
|
|
|
36
37
|
}),
|
|
37
38
|
}));
|
|
38
39
|
};
|
|
40
|
+
const gridRows = (grid) => [...grid.querySelectorAll(ROWS)].filter((row) => row.querySelector(CELLS) !== null);
|
|
39
41
|
const plotted = (series) => series.filter((one) => one.points.length > 0);
|
|
40
42
|
export const readSeries = (table) => {
|
|
41
43
|
if (isGrid(table)) {
|
|
@@ -49,7 +51,7 @@ export const readSeries = (table) => {
|
|
|
49
51
|
const columns = Math.max(0, ...rows.map((row) => row.cells.length - 1));
|
|
50
52
|
return plotted(Array.from({ length: columns }, (_, column) => ({
|
|
51
53
|
name: names[column] ?? element.caption?.textContent?.trim() ?? '',
|
|
52
|
-
points: rows.flatMap((row) => {
|
|
54
|
+
points: rows.flatMap((row, at) => {
|
|
53
55
|
const head = row.cells[0];
|
|
54
56
|
const cell = row.cells[column + 1];
|
|
55
57
|
if (!cell) {
|
|
@@ -62,6 +64,7 @@ export const readSeries = (table) => {
|
|
|
62
64
|
return Number.isFinite(value) && raw.trim() !== ''
|
|
63
65
|
? [
|
|
64
66
|
{
|
|
67
|
+
at,
|
|
65
68
|
label: head?.textContent?.trim() ?? '',
|
|
66
69
|
value,
|
|
67
70
|
...(tone ? { tone } : {}),
|
|
@@ -72,6 +75,30 @@ export const readSeries = (table) => {
|
|
|
72
75
|
}),
|
|
73
76
|
})));
|
|
74
77
|
};
|
|
78
|
+
export const byRow = (series, rows) => {
|
|
79
|
+
const slots = Array.from({ length: rows }, () => undefined);
|
|
80
|
+
for (const point of series?.points ?? []) {
|
|
81
|
+
if (point.at >= 0 && point.at < rows) {
|
|
82
|
+
slots[point.at] = point;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return slots;
|
|
86
|
+
};
|
|
87
|
+
export const unbroken = (series) => {
|
|
88
|
+
const runs = [];
|
|
89
|
+
let previous;
|
|
90
|
+
for (const point of series.points) {
|
|
91
|
+
const run = runs.at(-1);
|
|
92
|
+
if (run && previous && point.at === previous.at + 1) {
|
|
93
|
+
run.push(point);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
runs.push([point]);
|
|
97
|
+
}
|
|
98
|
+
previous = point;
|
|
99
|
+
}
|
|
100
|
+
return runs;
|
|
101
|
+
};
|
|
75
102
|
export const readRows = (table) => {
|
|
76
103
|
const cell = (name, element) => {
|
|
77
104
|
if (!element) {
|
|
@@ -100,7 +127,7 @@ export const readRows = (table) => {
|
|
|
100
127
|
const names = [...table.querySelectorAll(COLUMNS)]
|
|
101
128
|
.slice(1)
|
|
102
129
|
.map((one) => one.textContent?.trim() ?? '');
|
|
103
|
-
return
|
|
130
|
+
return gridRows(table).map((one) => {
|
|
104
131
|
const cells = [...one.querySelectorAll(CELLS)];
|
|
105
132
|
return row(cells[0], cells.slice(1), names);
|
|
106
133
|
});
|
|
@@ -26,6 +26,8 @@ export type RuntimeKind =
|
|
|
26
26
|
| 'max-past-fold'
|
|
27
27
|
/** A rating's `min` is no score between 0 and its `max`. */
|
|
28
28
|
| 'min-out-of-range'
|
|
29
|
+
/** A Stat was given one of Trend's directions, `up`, `down` or `neutral`, which lay nothing out. */
|
|
30
|
+
| 'misread-direction'
|
|
29
31
|
/** A React handler prop names an event the element never dispatches itself. */
|
|
30
32
|
| 'misplaced-handler'
|
|
31
33
|
/** The three.js driver could not load its layout, so the notes stay where they started. */
|
|
@@ -34,6 +36,8 @@ export type RuntimeKind =
|
|
|
34
36
|
| 'missing-row-key'
|
|
35
37
|
/** A stencil names parts the shape does not have. */
|
|
36
38
|
| 'missing-stencil-parts'
|
|
39
|
+
/** A Skip Link's `href` names no element in its tree, so a press moves focus nowhere. */
|
|
40
|
+
| 'missing-target'
|
|
37
41
|
/** A tooltip's `for` names no element in its own tree. */
|
|
38
42
|
| 'missing-trigger'
|
|
39
43
|
/** An area chart was handed a negative number, which it draws as none. */
|
|
@@ -42,6 +46,8 @@ export type RuntimeKind =
|
|
|
42
46
|
| 'not-a-graph'
|
|
43
47
|
/** The browser refused picture in picture. */
|
|
44
48
|
| 'picture-in-picture-refused'
|
|
49
|
+
/** A Grid's `min` is no length it can write, so the grid lays out by its column count. */
|
|
50
|
+
| 'refused-length'
|
|
45
51
|
/** A React handler prop is a removed callback, which never ran from JSX. */
|
|
46
52
|
| 'removed-callback'
|
|
47
53
|
/** A React handler prop uses Solid's `on:`, which React reads as another event's name. */
|
|
@@ -56,8 +62,12 @@ export type RuntimeKind =
|
|
|
56
62
|
| 'two-default-operators'
|
|
57
63
|
/** A React handler prop names an event nothing dispatches. */
|
|
58
64
|
| 'unknown-event'
|
|
65
|
+
/** An Icon's `name` is in no icon set it can read, so it draws nothing. */
|
|
66
|
+
| 'unknown-icon'
|
|
59
67
|
/** A tooltip's trigger does not name it in `aria-describedby`, so it is not read out. */
|
|
60
68
|
| 'unlinked-trigger'
|
|
69
|
+
/** A Text rendered as a label wraps or names a control, which its shadow root cannot reach. */
|
|
70
|
+
| 'unreachable-label'
|
|
61
71
|
/** A virtualized list's box has no height of its own, so its window never settles. */
|
|
62
72
|
| 'unsettled-window'
|
|
63
73
|
/** A slotted trigger is a plain element, which cannot be told it opens something. */
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stops the page behind a modal surface from scrolling.
|
|
3
|
+
*
|
|
4
|
+
* `showModal()` makes the page inert and leaves it scrollable, so a wheel over
|
|
5
|
+
* the backdrop moves the page under the dialog. `overflow: hidden` on the root
|
|
6
|
+
* stops that, and `scrollbar-gutter: stable` keeps the space a classic
|
|
7
|
+
* scrollbar took, so the page does not shift sideways when it goes. What the
|
|
8
|
+
* root carried before is put back when the last holder lets go.
|
|
9
|
+
*
|
|
10
|
+
* Dialog, Drawer and Lightbox hold it, and Toast reads who does.
|
|
11
|
+
*/
|
|
12
|
+
export declare const lockScroll: (holder: Element) => void;
|
|
13
|
+
/** Lets go of the page, and gives it back once nothing else holds it. */
|
|
14
|
+
export declare const unlockScroll: (holder: Element) => void;
|
|
15
|
+
/** The surfaces holding the page still, in the order they took it. */
|
|
16
|
+
export declare const scrollHolders: () => Element[];
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const SCROLL_LOCK = Symbol.for('fusion.helix.scroll-lock');
|
|
2
|
+
const record = () => globalThis[SCROLL_LOCK];
|
|
3
|
+
export const lockScroll = (holder) => {
|
|
4
|
+
const globals = globalThis;
|
|
5
|
+
const root = document.documentElement;
|
|
6
|
+
globals[SCROLL_LOCK] ??= { gutter: '', holders: new Set(), overflow: '' };
|
|
7
|
+
const lock = globals[SCROLL_LOCK];
|
|
8
|
+
if (lock.holders.has(holder)) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
if (lock.holders.size === 0) {
|
|
12
|
+
lock.overflow = root.style.overflow;
|
|
13
|
+
lock.gutter = root.style.scrollbarGutter;
|
|
14
|
+
if (window.innerWidth > root.clientWidth) {
|
|
15
|
+
root.style.scrollbarGutter = 'stable';
|
|
16
|
+
}
|
|
17
|
+
root.style.overflow = 'hidden';
|
|
18
|
+
}
|
|
19
|
+
lock.holders.add(holder);
|
|
20
|
+
};
|
|
21
|
+
export const unlockScroll = (holder) => {
|
|
22
|
+
const lock = record();
|
|
23
|
+
if (!lock?.holders.delete(holder) || lock.holders.size > 0) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const root = document.documentElement;
|
|
27
|
+
root.style.overflow = lock.overflow;
|
|
28
|
+
root.style.scrollbarGutter = lock.gutter;
|
|
29
|
+
};
|
|
30
|
+
export const scrollHolders = () => [...(record()?.holders ?? [])];
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a list property may hold: what an attribute writes, or an array a
|
|
3
|
+
* framework assigned to the property.
|
|
4
|
+
*
|
|
5
|
+
* **An array as well, because React 19 sets the property.**
|
|
6
|
+
* `values={[1240, 1318]}` arrives as an array, and splitting it as a string
|
|
7
|
+
* threw inside the render and left the old drawing on the screen.
|
|
8
|
+
*/
|
|
9
|
+
export type WrittenList = string | readonly (number | string)[] | null | undefined;
|
|
1
10
|
/**
|
|
2
11
|
* A list a consumer wrote into an attribute, split the way they write one.
|
|
3
12
|
*
|
|
@@ -20,7 +29,7 @@
|
|
|
20
29
|
* `for` take whitespace and only whitespace, and accepting a comma there would
|
|
21
30
|
* resolve an id nobody can write.
|
|
22
31
|
*/
|
|
23
|
-
export declare const writtenList: (source:
|
|
32
|
+
export declare const writtenList: (source: WrittenList) => string[];
|
|
24
33
|
/**
|
|
25
34
|
* The same list read as numbers, with everything that is not one dropped.
|
|
26
35
|
*
|
|
@@ -33,4 +42,4 @@ export declare const writtenList: (source: string | undefined) => string[];
|
|
|
33
42
|
* answer, because a number that makes sense here is a bound, a reading and a
|
|
34
43
|
* playback rate, and only the caller knows that a rate of zero is not a rate.
|
|
35
44
|
*/
|
|
36
|
-
export declare const writtenNumbers: (source:
|
|
45
|
+
export declare const writtenNumbers: (source: WrittenList) => number[];
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
export const writtenList = (source) => (source
|
|
1
|
+
export const writtenList = (source) => (Array.isArray(source) ? source.join(' ') : String(source ?? ''))
|
|
2
|
+
.split(/[\s,]+/)
|
|
3
|
+
.filter((one) => one !== '');
|
|
2
4
|
export const writtenNumbers = (source) => writtenList(source)
|
|
3
5
|
.map(Number)
|
|
4
6
|
.filter((one) => Number.isFinite(one));
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.143.0';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusion.dev/helix-core",
|
|
3
3
|
"description": "Helix element primitives: base classes, mixins, decorators, utilities.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.143.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ross Sheppard <ross.sheppard@fusion.dev>",
|
|
7
7
|
"repository": {
|
|
@@ -34,11 +34,11 @@
|
|
|
34
34
|
"bugs": "https://github.com/project-fusion/fusion/issues",
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@floating-ui/dom": "^1.8.0",
|
|
37
|
-
"@fusion.dev/helix-icons": "0.
|
|
37
|
+
"@fusion.dev/helix-icons": "0.143.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@fusion.dev/helix-cem": "0.
|
|
41
|
-
"@fusion.dev/helix-testing": "0.
|
|
40
|
+
"@fusion.dev/helix-cem": "0.143.0",
|
|
41
|
+
"@fusion.dev/helix-testing": "0.143.0",
|
|
42
42
|
"@lit-labs/ssr": "^4.1.0",
|
|
43
43
|
"lit": "^3.2.1"
|
|
44
44
|
},
|