@fusion.dev/helix-headless 0.171.0 → 0.173.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 +1350 -124
- package/dist/button-group/root/button-group-root.d.ts +30 -0
- package/dist/button-group/root/button-group-root.js +50 -4
- package/dist/canvas/root/canvas-root.d.ts +7 -0
- package/dist/canvas/root/canvas-root.js +7 -0
- package/dist/chat/composer/chat-composer.d.ts +28 -1
- package/dist/chat/composer/chat-composer.js +22 -3
- package/dist/chat/root/chat-root.d.ts +38 -2
- package/dist/chat/root/chat-root.js +38 -1
- package/dist/color-input/root/color-input-root.d.ts +15 -1
- package/dist/color-input/root/color-input-root.js +19 -1
- package/dist/color-picker/root/color-picker-root.d.ts +5 -1
- package/dist/color-picker/root/color-picker-root.js +1 -1
- package/dist/copy-button/root/copy-button-root.d.ts +43 -3
- package/dist/copy-button/root/copy-button-root.js +49 -3
- package/dist/data-table/root/data-table-root.d.ts +5 -1
- package/dist/data-table/root/data-table-root.js +1 -0
- package/dist/dialog/root/dialog-root.js +1 -0
- package/dist/drawer/root/drawer-root.d.ts +4 -0
- package/dist/drawer/root/drawer-root.js +1 -0
- package/dist/element-events.generated.js +3 -0
- package/dist/graph/root/graph-root.d.ts +22 -0
- package/dist/graph/root/graph-root.js +21 -5
- package/dist/lightbox/root/lightbox-root.js +1 -0
- package/dist/list/root/list-root.d.ts +6 -1
- package/dist/list/root/list-root.js +3 -2
- package/dist/listbox/root/listbox-root.d.ts +2 -1
- package/dist/listbox/root/listbox-root.js +3 -2
- package/dist/menu/item/menu-item.d.ts +2 -0
- package/dist/menu/item/menu-item.js +2 -0
- package/dist/menu/root/menu-root.d.ts +73 -12
- package/dist/menu/root/menu-root.js +103 -7
- package/dist/outline/root/outline-root.d.ts +57 -0
- package/dist/outline/root/outline-root.js +98 -29
- package/dist/popover/root/popover-root.d.ts +59 -6
- package/dist/popover/root/popover-root.js +51 -3
- package/dist/select/root/select-root.d.ts +2 -2
- package/dist/select/root/select-root.js +2 -2
- package/dist/speed-dial/root/speed-dial-root.d.ts +14 -0
- package/dist/speed-dial/root/speed-dial-root.js +34 -2
- package/dist/splitter/root/splitter-root.d.ts +25 -2
- package/dist/splitter/root/splitter-root.js +19 -4
- package/dist/tag/group/tag-group.d.ts +8 -1
- package/dist/tag/group/tag-group.js +8 -0
- package/dist/tag/root/tag-root.d.ts +12 -0
- package/dist/tag/root/tag-root.js +12 -0
- package/dist/textarea/root/textarea-root.d.ts +8 -0
- package/dist/textarea/root/textarea-root.js +16 -0
- package/dist/time-picker/root/time-picker-root.d.ts +14 -0
- package/dist/time-picker/root/time-picker-root.js +8 -1
- package/dist/timeline/item/timeline-item.d.ts +5 -1
- package/dist/timeline/item/timeline-item.js +9 -0
- package/dist/timeline/root/timeline-root.d.ts +23 -0
- package/dist/timeline/root/timeline-root.js +17 -3
- package/dist/tooltip/root/tooltip-root.d.ts +10 -0
- package/dist/tooltip/root/tooltip-root.js +14 -2
- package/dist/virtual-list/reorder/index.d.ts +1 -0
- package/dist/virtual-list/reorder/index.js +4 -0
- package/dist/virtual-list/reorder/virtual-list-reorder.d.ts +86 -0
- package/dist/virtual-list/reorder/virtual-list-reorder.js +193 -0
- package/dist/virtual-list/root/virtual-list-root.d.ts +49 -11
- package/dist/virtual-list/root/virtual-list-root.js +10 -1
- package/lint.json +1 -1
- package/package.json +5 -5
- package/types/jsx.d.ts +880 -48
- package/types/vue.d.ts +174 -19
- package/types/words.d.ts +22 -0
|
@@ -57,6 +57,12 @@ import { type PropertyValues, type TemplateResult } from 'lit';
|
|
|
57
57
|
* a named `group`, and the toolbar's own arrow keys walk its buttons, so a
|
|
58
58
|
* reader meets one toolbar. Under `single` it is a `radiogroup` either way.
|
|
59
59
|
*
|
|
60
|
+
* **Disabled.** `disabled` on the group disables every button in it, Helix
|
|
61
|
+
* and native alike, and says so on the group with `aria-disabled`, as React
|
|
62
|
+
* Aria's `isDisabled` does. A button disabled on its own stays disabled when
|
|
63
|
+
* the group is enabled again, and a button that leaves the group gets back
|
|
64
|
+
* what it said for itself.
|
|
65
|
+
*
|
|
60
66
|
* @since 0.1.0
|
|
61
67
|
* @status stable
|
|
62
68
|
* @category actions
|
|
@@ -73,6 +79,11 @@ import { type PropertyValues, type TemplateResult } from 'lit';
|
|
|
73
79
|
* @cssstate pressed - While a group with `selection-mode` has a button pressed.
|
|
74
80
|
*/
|
|
75
81
|
export declare class HelixHeadlessButtonGroup extends HelixElement {
|
|
82
|
+
/**
|
|
83
|
+
* Disables every button in the group, and says so on the group with
|
|
84
|
+
* `aria-disabled`. Reflected, so a stylesheet can select on it.
|
|
85
|
+
*/
|
|
86
|
+
disabled: boolean;
|
|
76
87
|
/**
|
|
77
88
|
* Keeps at least one button pressed once one is, as React Aria's
|
|
78
89
|
* `disallowEmptySelection`: a press that would leave none pressed is
|
|
@@ -122,6 +133,12 @@ export declare class HelixHeadlessButtonGroup extends HelixElement {
|
|
|
122
133
|
* not reflect, as a native control's `value` does not.
|
|
123
134
|
*/
|
|
124
135
|
value: string;
|
|
136
|
+
/**
|
|
137
|
+
* The pressed values as a list, in a selection mode, and an empty list
|
|
138
|
+
* otherwise. Written, it presses exactly those buttons.
|
|
139
|
+
*/
|
|
140
|
+
get values(): string[];
|
|
141
|
+
set values(next: string[]);
|
|
125
142
|
/**
|
|
126
143
|
* The Helix buttons of a group in a selection mode, in the order they are
|
|
127
144
|
* written, and none otherwise. Empty on a server, which has no tree.
|
|
@@ -146,6 +163,19 @@ export declare class HelixHeadlessButtonGroup extends HelixElement {
|
|
|
146
163
|
*/
|
|
147
164
|
private readonly told;
|
|
148
165
|
private watcher;
|
|
166
|
+
/** The children that can be disabled: Helix buttons and native ones alike. Empty on a server. */
|
|
167
|
+
private get members();
|
|
168
|
+
/** The members this group has disabled, so one that leaves is given back its own answer. */
|
|
169
|
+
private readonly held;
|
|
170
|
+
/**
|
|
171
|
+
* Passes `disabled` down to the members, and gives a member that left
|
|
172
|
+
* what it said for itself.
|
|
173
|
+
*
|
|
174
|
+
* A member already disabled is not written to again: the watcher hears
|
|
175
|
+
* every write to a native button's `disabled`, changed or not, and would
|
|
176
|
+
* answer it with this.
|
|
177
|
+
*/
|
|
178
|
+
private cascade;
|
|
149
179
|
/**
|
|
150
180
|
* Tells every button whether it is pressed and whether it holds the stop,
|
|
151
181
|
* and puts back the ones that left.
|
|
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
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
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
import { auditName, commaList, customElement, HelixElement, hostAria, inlineStep, pressedValue, reportRuntime, requestValue, rovingStop, toolbarAround, whenHydrated, } from '@fusion.dev/helix-core';
|
|
7
|
+
import { auditName, commaList, customElement, HelixElement, hostAria, inheritDisabled, inlineStep, pressedValue, reportRuntime, requestValue, rovingStop, toolbarAround, whenHydrated, } from '@fusion.dev/helix-core';
|
|
8
8
|
import { html } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
10
|
import { arrowTarget } from '../arrows.js';
|
|
@@ -12,11 +12,13 @@ const isGroupButton = (child) => child instanceof HTMLElement && 'focusable' in
|
|
|
12
12
|
let HelixHeadlessButtonGroup = class HelixHeadlessButtonGroup extends HelixElement {
|
|
13
13
|
constructor() {
|
|
14
14
|
super(...arguments);
|
|
15
|
+
this.disabled = false;
|
|
15
16
|
this.disallowEmptySelection = false;
|
|
16
17
|
this.label = '';
|
|
17
18
|
this.value = '';
|
|
18
19
|
this.lastFocused = undefined;
|
|
19
20
|
this.told = new Map();
|
|
21
|
+
this.held = new Set();
|
|
20
22
|
this.warned = false;
|
|
21
23
|
this.handlePress = (event) => {
|
|
22
24
|
const mode = this.selectionMode;
|
|
@@ -48,6 +50,12 @@ let HelixHeadlessButtonGroup = class HelixHeadlessButtonGroup extends HelixEleme
|
|
|
48
50
|
}
|
|
49
51
|
};
|
|
50
52
|
}
|
|
53
|
+
get values() {
|
|
54
|
+
return this.selectionMode ? commaList(this.value) : [];
|
|
55
|
+
}
|
|
56
|
+
set values(next) {
|
|
57
|
+
this.value = next.join(',');
|
|
58
|
+
}
|
|
51
59
|
get buttons() {
|
|
52
60
|
return this.selectionMode && this.children ? [...this.children].filter(isGroupButton) : [];
|
|
53
61
|
}
|
|
@@ -57,7 +65,33 @@ let HelixHeadlessButtonGroup = class HelixHeadlessButtonGroup extends HelixEleme
|
|
|
57
65
|
pressed(button) {
|
|
58
66
|
return button.value !== '' && commaList(this.value).includes(button.value);
|
|
59
67
|
}
|
|
68
|
+
get members() {
|
|
69
|
+
return this.children
|
|
70
|
+
? [...this.children].filter((child) => child instanceof HTMLElement && 'disabled' in child)
|
|
71
|
+
: [];
|
|
72
|
+
}
|
|
73
|
+
cascade() {
|
|
74
|
+
const members = this.members;
|
|
75
|
+
for (const gone of this.held) {
|
|
76
|
+
if (!members.includes(gone)) {
|
|
77
|
+
inheritDisabled(false, [gone]);
|
|
78
|
+
this.held.delete(gone);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (this.disabled) {
|
|
82
|
+
const live = members.filter((member) => !member.disabled);
|
|
83
|
+
inheritDisabled(true, live);
|
|
84
|
+
for (const member of live) {
|
|
85
|
+
this.held.add(member);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else if (this.held.size > 0) {
|
|
89
|
+
inheritDisabled(false, [...this.held]);
|
|
90
|
+
this.held.clear();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
60
93
|
sync() {
|
|
94
|
+
this.cascade();
|
|
61
95
|
const buttons = this.buttons;
|
|
62
96
|
const roving = this.roving;
|
|
63
97
|
const stop = roving
|
|
@@ -86,11 +120,16 @@ let HelixHeadlessButtonGroup = class HelixHeadlessButtonGroup extends HelixEleme
|
|
|
86
120
|
});
|
|
87
121
|
}
|
|
88
122
|
}
|
|
89
|
-
|
|
90
|
-
|
|
123
|
+
const reading = Boolean(this.selectionMode) || this.disabled;
|
|
124
|
+
for (const child of reading && this.children ? this.children : []) {
|
|
125
|
+
if (child.localName.includes('-') &&
|
|
126
|
+
!isGroupButton(child) &&
|
|
127
|
+
!customElements.get(child.localName)) {
|
|
91
128
|
void customElements.whenDefined(child.localName).then(() => this.sync());
|
|
92
129
|
}
|
|
93
|
-
else if (
|
|
130
|
+
else if (this.selectionMode &&
|
|
131
|
+
child.localName === 'button' &&
|
|
132
|
+
!child.hasAttribute('aria-pressed')) {
|
|
94
133
|
this.warn(child);
|
|
95
134
|
}
|
|
96
135
|
}
|
|
@@ -167,6 +206,7 @@ let HelixHeadlessButtonGroup = class HelixHeadlessButtonGroup extends HelixEleme
|
|
|
167
206
|
role: radios ? 'radiogroup' : toolbar ? 'toolbar' : 'group',
|
|
168
207
|
});
|
|
169
208
|
this.internals.ariaOrientation = toolbar ? (this.orientation ?? null) : null;
|
|
209
|
+
this.internals.ariaDisabled = this.disabled ? 'true' : null;
|
|
170
210
|
this.toggleState('pressed', Boolean(this.selectionMode) && commaList(this.value).length > 0);
|
|
171
211
|
}
|
|
172
212
|
firstUpdated(changed) {
|
|
@@ -181,6 +221,9 @@ let HelixHeadlessButtonGroup = class HelixHeadlessButtonGroup extends HelixEleme
|
|
|
181
221
|
return html `<slot></slot>`;
|
|
182
222
|
}
|
|
183
223
|
};
|
|
224
|
+
__decorate([
|
|
225
|
+
property({ reflect: true, type: Boolean })
|
|
226
|
+
], HelixHeadlessButtonGroup.prototype, "disabled", void 0);
|
|
184
227
|
__decorate([
|
|
185
228
|
property({ attribute: 'disallow-empty-selection', type: Boolean })
|
|
186
229
|
], HelixHeadlessButtonGroup.prototype, "disallowEmptySelection", void 0);
|
|
@@ -196,6 +239,9 @@ __decorate([
|
|
|
196
239
|
__decorate([
|
|
197
240
|
property()
|
|
198
241
|
], HelixHeadlessButtonGroup.prototype, "value", void 0);
|
|
242
|
+
__decorate([
|
|
243
|
+
property({ attribute: false })
|
|
244
|
+
], HelixHeadlessButtonGroup.prototype, "values", null);
|
|
199
245
|
HelixHeadlessButtonGroup = __decorate([
|
|
200
246
|
customElement('helix-headless-button-group')
|
|
201
247
|
], HelixHeadlessButtonGroup);
|
|
@@ -132,6 +132,7 @@ declare const HelixHeadlessCanvas_base: (new (...args: any[]) => import("@fusion
|
|
|
132
132
|
* @slot footer - Content below the board.
|
|
133
133
|
* @slot fullscreen-icon - Replaces the glyph inside the fullscreen button.
|
|
134
134
|
* @slot header - Content above the board.
|
|
135
|
+
* @slot loading - Content shown while `loading` is set, such as a progress bar while the document arrives.
|
|
135
136
|
* @slot pan-down-icon - Replaces the glyph inside the pan down button.
|
|
136
137
|
* @slot pan-end-icon - Replaces the glyph inside the button that pans toward the end of a line of text.
|
|
137
138
|
* @slot pan-start-icon - Replaces the glyph inside the button that pans toward the start of a line of text.
|
|
@@ -192,6 +193,7 @@ declare const HelixHeadlessCanvas_base: (new (...args: any[]) => import("@fusion
|
|
|
192
193
|
* @cssstate fullscreen - While the board fills the screen.
|
|
193
194
|
* @cssstate holding - An item picked up with a click follows the pointer until the next press puts it down.
|
|
194
195
|
* @cssstate idle - The user has been still for `idle-delay` milliseconds. The board's own controls stay; select on it to hide chrome of your own.
|
|
196
|
+
* @cssstate loading - While `loading` is set.
|
|
195
197
|
* @cssstate pressing - While a press on the board, its cards or its connections is under way.
|
|
196
198
|
* @cssstate sharing - The page keeps the plain wheel and one finger, with `gestures="cooperative"` or while a board with `gestures="activate"` waits to be pressed.
|
|
197
199
|
* @cssstate using - A board with `gestures="activate"` has been pressed and has the gestures.
|
|
@@ -275,6 +277,11 @@ export declare class HelixHeadlessCanvas extends HelixHeadlessCanvas_base {
|
|
|
275
277
|
* `leads-to-label` and `{comesFrom}` is `comes-from-label`, each filled in.
|
|
276
278
|
*/
|
|
277
279
|
linkedLabel: string;
|
|
280
|
+
/**
|
|
281
|
+
* Marks the board as busy while its document arrives: the board is
|
|
282
|
+
* `aria-busy`, and the `loading` slot is shown.
|
|
283
|
+
*/
|
|
284
|
+
loading: boolean;
|
|
278
285
|
/**
|
|
279
286
|
* What screen readers hear after a key moves a card, with `{name}` for the
|
|
280
287
|
* card and `{x}` and `{y}` for where it now is, in the document's units.
|
|
@@ -57,6 +57,7 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
|
|
|
57
57
|
this.labelEditorLabel = 'Label for {name}';
|
|
58
58
|
this.leadsToLabel = 'leads to {cards}';
|
|
59
59
|
this.linkedLabel = '{leadsTo}; {comesFrom}';
|
|
60
|
+
this.loading = false;
|
|
60
61
|
this.movedLabel = '{name} moved to {x}, {y}';
|
|
61
62
|
this.pastedLabel = '{count, plural, one {# card pasted} other {# cards pasted}}';
|
|
62
63
|
this.readOnly = false;
|
|
@@ -2133,6 +2134,7 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
|
|
|
2133
2134
|
this.toggleState('empty', this.nodes.length === 0);
|
|
2134
2135
|
this.toggleState('fullscreen', this.filled);
|
|
2135
2136
|
this.toggleState('idle', this.resting);
|
|
2137
|
+
this.toggleState('loading', this.loading);
|
|
2136
2138
|
}
|
|
2137
2139
|
place(origin) {
|
|
2138
2140
|
const all = this.nodes;
|
|
@@ -2411,6 +2413,7 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
|
|
|
2411
2413
|
role="group"
|
|
2412
2414
|
aria-label=${this.label || this.getAttribute?.('aria-label') || this.fallbackLabel}
|
|
2413
2415
|
aria-roledescription=${(this.label || this.getAttribute?.('aria-label')) && this.roleLabel ? this.roleLabel : nothing}
|
|
2416
|
+
aria-busy=${this.loading ? 'true' : nothing}
|
|
2414
2417
|
aria-keyshortcuts="ArrowUp ArrowDown ArrowLeft ArrowRight Plus Minus"
|
|
2415
2418
|
tabindex="0"
|
|
2416
2419
|
@focusin=${this.idle.stir}
|
|
@@ -2461,6 +2464,7 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
|
|
|
2461
2464
|
${this.renderMarquee(origin)}
|
|
2462
2465
|
</div>
|
|
2463
2466
|
<slot name="empty"></slot>
|
|
2467
|
+
${this.loading ? html `<slot name="loading"></slot>` : nothing}
|
|
2464
2468
|
${this.renderGestures()}
|
|
2465
2469
|
</div>
|
|
2466
2470
|
${this.renderControls()} ${this.renderMinimap()}
|
|
@@ -2525,6 +2529,9 @@ __decorate([
|
|
|
2525
2529
|
words,
|
|
2526
2530
|
property({ attribute: 'linked-label' })
|
|
2527
2531
|
], HelixHeadlessCanvas.prototype, "linkedLabel", void 0);
|
|
2532
|
+
__decorate([
|
|
2533
|
+
property({ reflect: true, type: Boolean })
|
|
2534
|
+
], HelixHeadlessCanvas.prototype, "loading", void 0);
|
|
2528
2535
|
__decorate([
|
|
2529
2536
|
words,
|
|
2530
2537
|
property({ attribute: 'moved-label' })
|
|
@@ -28,6 +28,11 @@ import { type PropertyValues, type TemplateResult } from 'lit';
|
|
|
28
28
|
* Sending an empty required composer counts as trying. Tabbing past an empty
|
|
29
29
|
* composer marks nothing.
|
|
30
30
|
*
|
|
31
|
+
* **`feedback` is a message of your own about the draft**, such as one your
|
|
32
|
+
* server sent back. While it is set, the composer is invalid at once, does
|
|
33
|
+
* not send or submit, shows the message under the field, and a screen reader
|
|
34
|
+
* hears it as it appears. Clear it to let the draft go.
|
|
35
|
+
*
|
|
31
36
|
* **`typing` says whether the user is writing**, and `helix-typing` is sent
|
|
32
37
|
* each time it changes: when they start, and when they send or stop for
|
|
33
38
|
* `typing-timeout` milliseconds. Pass it on to show "is typing" to the other
|
|
@@ -48,13 +53,14 @@ import { type PropertyValues, type TemplateResult } from 'lit';
|
|
|
48
53
|
*
|
|
49
54
|
* @csspart composer - The whole composer.
|
|
50
55
|
* @csspart end - The box around the `end` slot.
|
|
56
|
+
* @csspart feedback - The `feedback` message, under the field while it is set.
|
|
51
57
|
* @csspart field - The text field.
|
|
52
58
|
* @csspart mark - The icon inside the send button.
|
|
53
59
|
* @csspart send - The send button.
|
|
54
60
|
* @csspart start - The box around the `start` slot.
|
|
55
61
|
* @csspart tools - The row under the field.
|
|
56
62
|
*
|
|
57
|
-
* @cssstate invalid - The draft fails `required`, `minlength`, `maxlength`, or a custom error, and the reader has tried to send or submit it, or edited the field and left it.
|
|
63
|
+
* @cssstate invalid - The draft fails `required`, `minlength`, `maxlength`, or a custom error, and the reader has tried to send or submit it, or edited the field and left it. Or `feedback` is set.
|
|
58
64
|
*/
|
|
59
65
|
export declare class HelixHeadlessChatComposer extends HelixFormElement {
|
|
60
66
|
/**
|
|
@@ -62,6 +68,12 @@ export declare class HelixHeadlessChatComposer extends HelixFormElement {
|
|
|
62
68
|
* `registerWords('de', { 'helix-chat-composer': { sendLabel: 'Senden' } })`.
|
|
63
69
|
*/
|
|
64
70
|
static words: string;
|
|
71
|
+
/**
|
|
72
|
+
* A message about the draft, shown under the field. While set, the composer
|
|
73
|
+
* is invalid, does not send, and does not submit, as a field's `feedback`
|
|
74
|
+
* does, and the message is announced as it appears.
|
|
75
|
+
*/
|
|
76
|
+
feedback: string;
|
|
65
77
|
/**
|
|
66
78
|
* The field's name, read by screen readers.
|
|
67
79
|
*/
|
|
@@ -123,6 +135,17 @@ export declare class HelixHeadlessChatComposer extends HelixFormElement {
|
|
|
123
135
|
private writing;
|
|
124
136
|
/** Whether the reader has tried to send or submit the draft, or edited it and left. */
|
|
125
137
|
private touched;
|
|
138
|
+
/**
|
|
139
|
+
* Whether the reader has typed in the field since it last held a fresh
|
|
140
|
+
* draft: since it rendered, since a send cleared it, or since a reset.
|
|
141
|
+
*
|
|
142
|
+
* **A send clears the field, and the field still sends `change` as it is
|
|
143
|
+
* left**, because the platform compares the text with what it held when it
|
|
144
|
+
* was focused. Measured in Chromium before this: a required composer sent
|
|
145
|
+
* "hi", cleared, and on Tab showed itself invalid over an empty draft
|
|
146
|
+
* nobody had written.
|
|
147
|
+
*/
|
|
148
|
+
private edited;
|
|
126
149
|
/** Whether the composer shows that its draft is wrong. */
|
|
127
150
|
private shownInvalid;
|
|
128
151
|
/** The pause after which the user has stopped writing. */
|
|
@@ -147,10 +170,14 @@ export declare class HelixHeadlessChatComposer extends HelixFormElement {
|
|
|
147
170
|
* back to. `value` does not reflect, so its attribute stays as written.
|
|
148
171
|
*/
|
|
149
172
|
private get declared();
|
|
173
|
+
/** @internal `feedback` marks the composer invalid, as a field's does. */
|
|
174
|
+
protected get validityFeedback(): string;
|
|
150
175
|
/** Puts back the draft the markup declared when the form is reset. */
|
|
151
176
|
formResetCallback(): void;
|
|
152
177
|
/** The reader has had a go at the draft, so whether it is wrong can be shown. */
|
|
153
178
|
private readonly touch;
|
|
179
|
+
/** The field's `change`, which counts only after the reader typed in it. */
|
|
180
|
+
private readonly handleChange;
|
|
154
181
|
/** Brings back the draft when the browser restores the form. */
|
|
155
182
|
formStateRestoreCallback(state: string | File | FormData | null): void;
|
|
156
183
|
/**
|
|
@@ -13,6 +13,7 @@ import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
|
|
13
13
|
let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFormElement {
|
|
14
14
|
constructor() {
|
|
15
15
|
super(...arguments);
|
|
16
|
+
this.feedback = '';
|
|
16
17
|
this.label = 'Message';
|
|
17
18
|
this.placeholder = '';
|
|
18
19
|
this.required = false;
|
|
@@ -24,15 +25,22 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
|
|
|
24
25
|
this.isChatComposer = true;
|
|
25
26
|
this.writing = false;
|
|
26
27
|
this.touched = false;
|
|
28
|
+
this.edited = false;
|
|
27
29
|
this.shownInvalid = false;
|
|
28
30
|
this.pause = new DelayController(this, { renders: false });
|
|
29
31
|
this.touch = () => {
|
|
30
32
|
this.touched = true;
|
|
31
33
|
this.requestUpdate();
|
|
32
34
|
};
|
|
35
|
+
this.handleChange = () => {
|
|
36
|
+
if (this.edited) {
|
|
37
|
+
this.touch();
|
|
38
|
+
}
|
|
39
|
+
};
|
|
33
40
|
this.handleInput = (event) => {
|
|
34
41
|
event.stopPropagation();
|
|
35
42
|
this.value = event.target.value;
|
|
43
|
+
this.edited = true;
|
|
36
44
|
this.dispatchEvent(new Event('input'));
|
|
37
45
|
this.markTyping(this.value !== '');
|
|
38
46
|
};
|
|
@@ -87,16 +95,21 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
|
|
|
87
95
|
}
|
|
88
96
|
this.value = '';
|
|
89
97
|
this.touched = false;
|
|
98
|
+
this.edited = false;
|
|
90
99
|
this.markTyping(false);
|
|
91
100
|
return true;
|
|
92
101
|
}
|
|
93
102
|
get declared() {
|
|
94
103
|
return this.getAttribute('value') ?? '';
|
|
95
104
|
}
|
|
105
|
+
get validityFeedback() {
|
|
106
|
+
return this.feedback;
|
|
107
|
+
}
|
|
96
108
|
formResetCallback() {
|
|
97
109
|
super.formResetCallback();
|
|
98
110
|
this.value = this.declared;
|
|
99
111
|
this.touched = false;
|
|
112
|
+
this.edited = false;
|
|
100
113
|
this.requestUpdate();
|
|
101
114
|
}
|
|
102
115
|
formStateRestoreCallback(state) {
|
|
@@ -117,7 +130,7 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
|
|
|
117
130
|
if (field?.validity) {
|
|
118
131
|
this.internals.setValidity(field.validity, field.validationMessage, field);
|
|
119
132
|
}
|
|
120
|
-
const invalid = this.touched && !this.internals.validity.valid;
|
|
133
|
+
const invalid = this.feedback !== '' || (this.touched && !this.internals.validity.valid);
|
|
121
134
|
this.toggleState('invalid', invalid);
|
|
122
135
|
this.internals.ariaInvalid = invalid ? 'true' : 'false';
|
|
123
136
|
if (invalid !== this.shownInvalid) {
|
|
@@ -166,8 +179,9 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
|
|
|
166
179
|
minlength=${this.minLength ?? nothing}
|
|
167
180
|
maxlength=${this.maxLength ?? nothing}
|
|
168
181
|
aria-invalid=${this.shownInvalid ? 'true' : nothing}
|
|
182
|
+
aria-describedby=${this.feedback === '' ? nothing : 'feedback'}
|
|
169
183
|
?disabled=${this.isDisabled}
|
|
170
|
-
@change=${this.
|
|
184
|
+
@change=${this.handleChange}
|
|
171
185
|
@input=${this.handleInput}
|
|
172
186
|
@keydown=${this.handleKey}
|
|
173
187
|
></textarea>
|
|
@@ -176,9 +190,14 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
|
|
|
176
190
|
<span part="end"><slot name="end"></slot></span>
|
|
177
191
|
${this.renderSend()}
|
|
178
192
|
</div>
|
|
179
|
-
</div
|
|
193
|
+
</div>${this.feedback === ''
|
|
194
|
+
? nothing
|
|
195
|
+
: html `<p part="feedback" id="feedback" role="alert">${this.feedback}</p>`}`;
|
|
180
196
|
}
|
|
181
197
|
};
|
|
198
|
+
__decorate([
|
|
199
|
+
property()
|
|
200
|
+
], HelixHeadlessChatComposer.prototype, "feedback", void 0);
|
|
182
201
|
__decorate([
|
|
183
202
|
words,
|
|
184
203
|
property()
|
|
@@ -37,6 +37,13 @@ declare const HelixHeadlessChat_base: (new (...args: any[]) => import("@fusion.d
|
|
|
37
37
|
* `loading` while you fetch, then prepend the older turns. The turn the user
|
|
38
38
|
* was reading stays in place on screen.
|
|
39
39
|
*
|
|
40
|
+
* The transcript is a `log`, the role for content added in order where the
|
|
41
|
+
* newest matters most, and it answers the APG feed's keys. The `feed` role
|
|
42
|
+
* reads its articles as a list from the top, with no newest end to stay
|
|
43
|
+
* pinned to, so the log keeps the role a conversation has. While `loading` is
|
|
44
|
+
* set or older turns load, the log carries `aria-busy`, as the APG asks of a
|
|
45
|
+
* feed.
|
|
46
|
+
*
|
|
40
47
|
* The log is `aria-live="off"`. A separate status region announces each new
|
|
41
48
|
* turn once, when it has finished arriving. It skips the user's own turns
|
|
42
49
|
* unless they failed to send, turns loaded as history, and the turns already
|
|
@@ -49,6 +56,12 @@ declare const HelixHeadlessChat_base: (new (...args: any[]) => import("@fusion.d
|
|
|
49
56
|
* conversations. `turns` draws a long
|
|
50
57
|
* transcript one window at a time, so the log needs a block size of its own.
|
|
51
58
|
*
|
|
59
|
+
* The log scrolls on this tier. It carries `data-scroller`, and `core` ships
|
|
60
|
+
* `[data-scroller] { overflow: auto }` into every shadow root, so pinning,
|
|
61
|
+
* history and the window work with no stylesheet. You still need to give the
|
|
62
|
+
* log a block size, such as `helix-headless-chat::part(log) { block-size:
|
|
63
|
+
* 20rem }`.
|
|
64
|
+
*
|
|
52
65
|
* **A conversation is built from three elements.** The chat holds the
|
|
53
66
|
* transcript, a `Chat.Message` stands for each turn, and a `Chat.Composer` sits
|
|
54
67
|
* in the `composer` slot. When the user sends, `helix-send` reaches the chat:
|
|
@@ -65,7 +78,9 @@ declare const HelixHeadlessChat_base: (new (...args: any[]) => import("@fusion.d
|
|
|
65
78
|
* turn says its place, such as "3 of 40", windowed or not. The transcript is
|
|
66
79
|
* a tab stop while it holds a turn, and the turn a reader moved to is one
|
|
67
80
|
* too. Moving to an older turn unpins a transcript that scrolls and scrolls
|
|
68
|
-
* the turn into view, and moving to the newest pins it again.
|
|
81
|
+
* the turn into view, and moving to the newest pins it again. When the turn
|
|
82
|
+
* holding focus is removed, focus moves to the turn that took its place, or to
|
|
83
|
+
* the one before it where it was the last.
|
|
69
84
|
*
|
|
70
85
|
* | Key | What it does |
|
|
71
86
|
* |---|---|
|
|
@@ -120,7 +135,7 @@ declare const HelixHeadlessChat_base: (new (...args: any[]) => import("@fusion.d
|
|
|
120
135
|
* @csspart indicator - The three dots before the words that say who is typing, which the styled tier animates as a message's are. Hidden from screen readers.
|
|
121
136
|
* @csspart live - The region where screen readers announce each turn once it has arrived.
|
|
122
137
|
* @csspart loading - Shown while the history loads.
|
|
123
|
-
* @csspart log - The transcript, which scrolls.
|
|
138
|
+
* @csspart log - The transcript, which scrolls. On this tier, it needs only a block size.
|
|
124
139
|
* @csspart message - The message drawn for a turn from `turns`, forwarded whole.
|
|
125
140
|
* @csspart message-actions - The controls on a turn from `turns`, beside its time.
|
|
126
141
|
* @csspart message-avatar - The avatar box of a turn from `turns`.
|
|
@@ -518,6 +533,27 @@ export declare class HelixHeadlessChat extends HelixHeadlessChat_base {
|
|
|
518
533
|
*/
|
|
519
534
|
private follow;
|
|
520
535
|
protected updated(changed: PropertyValues): void;
|
|
536
|
+
/** The turn focus is in, by its place and its identity, once a reader is in one. */
|
|
537
|
+
private held;
|
|
538
|
+
/** Notes the turn focus went into, or that it went somewhere else. */
|
|
539
|
+
private readonly handleFocusIn;
|
|
540
|
+
/**
|
|
541
|
+
* Forgets the turn once focus moves to something outside the transcript.
|
|
542
|
+
* A move to nothing is kept, since removing the turn that held focus is
|
|
543
|
+
* one.
|
|
544
|
+
*/
|
|
545
|
+
private readonly handleFocusOut;
|
|
546
|
+
/**
|
|
547
|
+
* Moves focus to a neighbor where removing the turn that held it dropped
|
|
548
|
+
* focus to the body: the turn that took its place, or the one before it
|
|
549
|
+
* where it was the last.
|
|
550
|
+
*
|
|
551
|
+
* **Focus went to the body**, and Page Down did nothing until the reader
|
|
552
|
+
* tabbed back into the transcript. List and Virtual List move focus to a
|
|
553
|
+
* neighbor the same way, as React Aria's GridList does. Focus anywhere but
|
|
554
|
+
* the body is where you put it, and is left there.
|
|
555
|
+
*/
|
|
556
|
+
private followRemoved;
|
|
521
557
|
/**
|
|
522
558
|
* Hands each turn written as a child the words it is named with, once it
|
|
523
559
|
* has hydrated, so its first render is the server's.
|
|
@@ -5,7 +5,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
7
|
var HelixHeadlessChat_1;
|
|
8
|
-
import { ChildrenController, customElement, drawIcon, focusableElements, formatWords, HelixBeforePinEvent, HelixElement, HelixHistoryEvent, HelixPinEvent, hostAria, MixinCollection, onUnlessFalse, reportRuntime, requestChange, UpgradeController, VirtualizeController, whenHydrated, words, wordsConverter, } from '@fusion.dev/helix-core';
|
|
8
|
+
import { ChildrenController, customElement, deepActiveElement, drawIcon, focusableElements, formatWords, HelixBeforePinEvent, HelixElement, HelixHistoryEvent, HelixPinEvent, hostAria, MixinCollection, onUnlessFalse, reportRuntime, requestChange, UpgradeController, VirtualizeController, whenHydrated, words, wordsConverter, } from '@fusion.dev/helix-core';
|
|
9
9
|
import { arrowDown } from '@fusion.dev/helix-icons';
|
|
10
10
|
import { nothing } from 'lit';
|
|
11
11
|
import { property, state } from 'lit/decorators.js';
|
|
@@ -98,6 +98,21 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
|
|
|
98
98
|
this.resume();
|
|
99
99
|
};
|
|
100
100
|
this.warnedMixed = false;
|
|
101
|
+
this.handleFocusIn = (event) => {
|
|
102
|
+
const at = this.turnFrom(event.composedPath());
|
|
103
|
+
if (at === undefined || at < 0) {
|
|
104
|
+
this.held = undefined;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const id = this.fed ? this.identity(this.turns[at]) : this.messages[at];
|
|
108
|
+
this.held = { at, id };
|
|
109
|
+
};
|
|
110
|
+
this.handleFocusOut = (event) => {
|
|
111
|
+
const to = event.relatedTarget;
|
|
112
|
+
if (to instanceof Node && !this.contains(to) && !this.renderRoot?.contains?.(to)) {
|
|
113
|
+
this.held = undefined;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
101
116
|
this.handleKeydown = (event) => {
|
|
102
117
|
if (event.defaultPrevented || event.altKey || event.metaKey || event.shiftKey) {
|
|
103
118
|
return;
|
|
@@ -423,6 +438,8 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
|
|
|
423
438
|
connectedCallback() {
|
|
424
439
|
super.connectedCallback();
|
|
425
440
|
this.addEventListener('keydown', this.handleKeydown);
|
|
441
|
+
this.addEventListener('focusin', this.handleFocusIn);
|
|
442
|
+
this.addEventListener('focusout', this.handleFocusOut);
|
|
426
443
|
this.settling = new MutationObserver(this.handleSettle);
|
|
427
444
|
this.settling.observe(this, { attributeFilter: ['state'], subtree: true });
|
|
428
445
|
this.opening = true;
|
|
@@ -436,6 +453,8 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
|
|
|
436
453
|
disconnectedCallback() {
|
|
437
454
|
super.disconnectedCallback();
|
|
438
455
|
this.removeEventListener('keydown', this.handleKeydown);
|
|
456
|
+
this.removeEventListener('focusin', this.handleFocusIn);
|
|
457
|
+
this.removeEventListener('focusout', this.handleFocusOut);
|
|
439
458
|
this.settling?.disconnect();
|
|
440
459
|
this.settling = undefined;
|
|
441
460
|
this.grower?.disconnect();
|
|
@@ -506,6 +525,23 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
|
|
|
506
525
|
this.remember();
|
|
507
526
|
this.reachHistory();
|
|
508
527
|
this.announce();
|
|
528
|
+
this.followRemoved();
|
|
529
|
+
}
|
|
530
|
+
followRemoved() {
|
|
531
|
+
const held = this.held;
|
|
532
|
+
if (held === undefined) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const ids = this.fed ? this.turns.map((turn) => this.identity(turn)) : this.messages;
|
|
536
|
+
if (ids.includes(held.id)) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
this.held = undefined;
|
|
540
|
+
const active = deepActiveElement();
|
|
541
|
+
if (ids.length === 0 || (active !== null && active !== document.body)) {
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
void this.focusTurn(Math.min(held.at, ids.length - 1));
|
|
509
545
|
}
|
|
510
546
|
tellTurns() {
|
|
511
547
|
const messages = this.messages;
|
|
@@ -670,6 +706,7 @@ let HelixHeadlessChat = class HelixHeadlessChat extends MixinCollection(HelixEle
|
|
|
670
706
|
part="log"
|
|
671
707
|
role="log"
|
|
672
708
|
aria-live="off"
|
|
709
|
+
data-scroller
|
|
673
710
|
aria-label=${this.label || this.fallbackLabel}
|
|
674
711
|
aria-busy=${busy ? 'true' : nothing}
|
|
675
712
|
?data-windowed=${fed}
|
|
@@ -31,7 +31,8 @@ declare const HelixHeadlessColorInput_base: (new (...args: any[]) => import("@fu
|
|
|
31
31
|
*
|
|
32
32
|
* The trigger and ArrowDown open the panel with focus in the square. Tab moves
|
|
33
33
|
* between the panel's controls without leaving it, and Escape closes it and
|
|
34
|
-
* returns focus to the
|
|
34
|
+
* returns focus to the control that opened it: the trigger after a press, and
|
|
35
|
+
* the field after ArrowDown.
|
|
35
36
|
*
|
|
36
37
|
* The component sets `--color-input-swatch`, so do not set it yourself. It
|
|
37
38
|
* holds the resolved color, so the styled tier can paint the swatch without
|
|
@@ -305,6 +306,19 @@ export declare class HelixHeadlessColorInput extends HelixHeadlessColorInput_bas
|
|
|
305
306
|
*/
|
|
306
307
|
protected sync(): void;
|
|
307
308
|
protected updated(changed: PropertyValues): void;
|
|
309
|
+
/** Whether the trigger's press opened the panel that is open now. */
|
|
310
|
+
private pressedOpen;
|
|
311
|
+
/**
|
|
312
|
+
* The trigger's press, which notes that it opened the panel, so Escape hands
|
|
313
|
+
* focus back to the trigger.
|
|
314
|
+
*/
|
|
315
|
+
private readonly handlePress;
|
|
316
|
+
/**
|
|
317
|
+
* Hands focus back to the control that opened the panel: the trigger after
|
|
318
|
+
* its press, which is the APG date picker dialog's rule, and the text after
|
|
319
|
+
* ArrowDown from it.
|
|
320
|
+
*/
|
|
321
|
+
protected returnFocus(): void;
|
|
308
322
|
/**
|
|
309
323
|
* The picker the field opens, behind a seam the styled tier can move.
|
|
310
324
|
*
|
|
@@ -55,6 +55,12 @@ let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinPickerF
|
|
|
55
55
|
this.handleSampleEnd = () => {
|
|
56
56
|
this.requestOpen(true);
|
|
57
57
|
};
|
|
58
|
+
this.pressedOpen = false;
|
|
59
|
+
this.handlePress = () => {
|
|
60
|
+
const opening = !this.open;
|
|
61
|
+
this.handleTriggerClick();
|
|
62
|
+
this.pressedOpen = opening && this.open;
|
|
63
|
+
};
|
|
58
64
|
}
|
|
59
65
|
static { this.words = 'helix-color-input'; }
|
|
60
66
|
requestValue(next) {
|
|
@@ -117,6 +123,18 @@ let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinPickerF
|
|
|
117
123
|
if (changed.has('value') && parseColor(this.value)) {
|
|
118
124
|
this.settled = this.value;
|
|
119
125
|
}
|
|
126
|
+
if (changed.has('open') && !this.open) {
|
|
127
|
+
this.pressedOpen = false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
returnFocus() {
|
|
131
|
+
const trigger = this.pressedOpen ? this.partNamed('trigger') : null;
|
|
132
|
+
this.pressedOpen = false;
|
|
133
|
+
if (trigger) {
|
|
134
|
+
trigger.focus();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
super.returnFocus();
|
|
120
138
|
}
|
|
121
139
|
renderPicker() {
|
|
122
140
|
return html `<${this.tag('color-picker')}
|
|
@@ -204,7 +222,7 @@ let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinPickerF
|
|
|
204
222
|
: 'swatch-value'
|
|
205
223
|
: nothing}
|
|
206
224
|
?disabled=${this.isDisabled}
|
|
207
|
-
@click=${this.
|
|
225
|
+
@click=${this.handlePress}
|
|
208
226
|
>
|
|
209
227
|
<span
|
|
210
228
|
part="preview"
|
|
@@ -445,7 +445,11 @@ export declare class HelixHeadlessColorPicker extends HelixHeadlessColorPicker_b
|
|
|
445
445
|
* field shows the color the reset put back.
|
|
446
446
|
*/
|
|
447
447
|
formResetCallback(): void;
|
|
448
|
-
/**
|
|
448
|
+
/**
|
|
449
|
+
* The line `said` holds, or nothing while it is empty. `role="alert"`, as
|
|
450
|
+
* every field's feedback line has, so the message is announced when it
|
|
451
|
+
* appears.
|
|
452
|
+
*/
|
|
449
453
|
protected renderPickerFeedback(): TemplateResult | typeof nothing;
|
|
450
454
|
/** Whether a pointer is dragging the square's handle, said as `:state(dragging)`. */
|
|
451
455
|
private dragging;
|
|
@@ -309,7 +309,7 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
|
|
|
309
309
|
}
|
|
310
310
|
renderPickerFeedback() {
|
|
311
311
|
const said = this.said;
|
|
312
|
-
return said === '' ? nothing : html `<p part="feedback" id="feedback">${said}</p>`;
|
|
312
|
+
return said === '' ? nothing : html `<p part="feedback" id="feedback" role="alert">${said}</p>`;
|
|
313
313
|
}
|
|
314
314
|
settle() {
|
|
315
315
|
if (this.unsettled) {
|