@fusion.dev/helix-headless 0.172.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.
Files changed (41) hide show
  1. package/custom-elements.json +912 -60
  2. package/dist/button-group/root/button-group-root.d.ts +30 -0
  3. package/dist/button-group/root/button-group-root.js +50 -4
  4. package/dist/canvas/root/canvas-root.d.ts +7 -0
  5. package/dist/canvas/root/canvas-root.js +7 -0
  6. package/dist/copy-button/root/copy-button-root.d.ts +43 -3
  7. package/dist/copy-button/root/copy-button-root.js +49 -3
  8. package/dist/element-events.generated.js +2 -0
  9. package/dist/graph/root/graph-root.d.ts +22 -0
  10. package/dist/graph/root/graph-root.js +21 -5
  11. package/dist/list/root/list-root.d.ts +6 -1
  12. package/dist/list/root/list-root.js +3 -2
  13. package/dist/outline/root/outline-root.d.ts +57 -0
  14. package/dist/outline/root/outline-root.js +98 -29
  15. package/dist/speed-dial/root/speed-dial-root.d.ts +14 -0
  16. package/dist/speed-dial/root/speed-dial-root.js +34 -2
  17. package/dist/splitter/root/splitter-root.d.ts +25 -2
  18. package/dist/splitter/root/splitter-root.js +19 -4
  19. package/dist/tag/group/tag-group.d.ts +8 -1
  20. package/dist/tag/group/tag-group.js +8 -0
  21. package/dist/tag/root/tag-root.d.ts +12 -0
  22. package/dist/tag/root/tag-root.js +12 -0
  23. package/dist/time-picker/root/time-picker-root.d.ts +14 -0
  24. package/dist/time-picker/root/time-picker-root.js +8 -1
  25. package/dist/timeline/item/timeline-item.d.ts +5 -1
  26. package/dist/timeline/item/timeline-item.js +9 -0
  27. package/dist/timeline/root/timeline-root.d.ts +23 -0
  28. package/dist/timeline/root/timeline-root.js +17 -3
  29. package/dist/tooltip/root/tooltip-root.d.ts +10 -0
  30. package/dist/tooltip/root/tooltip-root.js +14 -2
  31. package/dist/virtual-list/reorder/index.d.ts +1 -0
  32. package/dist/virtual-list/reorder/index.js +4 -0
  33. package/dist/virtual-list/reorder/virtual-list-reorder.d.ts +86 -0
  34. package/dist/virtual-list/reorder/virtual-list-reorder.js +193 -0
  35. package/dist/virtual-list/root/virtual-list-root.d.ts +49 -11
  36. package/dist/virtual-list/root/virtual-list-root.js +10 -1
  37. package/lint.json +1 -1
  38. package/package.json +5 -5
  39. package/types/jsx.d.ts +568 -18
  40. package/types/vue.d.ts +115 -11
  41. package/types/words.d.ts +14 -0
@@ -40,6 +40,22 @@ import { type Branch } from '../levels.js';
40
40
  * headings are far apart, none is in the band and no section is marked, and
41
41
  * when they are close together, several are in the band at once.
42
42
  *
43
+ * **Several sections at once.** `marks="visible"` marks the link of every
44
+ * section with part of it on screen, from the reading line to the bottom of
45
+ * what the sections scroll in, as Zag's table of contents marks its active
46
+ * ids. The mark then runs from the first of those links to the last, and
47
+ * `marked` lists their ids in page order.
48
+ *
49
+ * **Going to a section from code.** `scrollToSection(id)` scrolls to the
50
+ * section as a press on its link does, with `behavior`, and marks its link at
51
+ * once.
52
+ *
53
+ * **Styling a link by its state.** Each link the outline finds carries
54
+ * `data-depth`, its depth in the outline from `1`, nested lists and
55
+ * `data-level` alike, and `aria-current="location"` while its section is
56
+ * marked, so `a[aria-current]` and `a[data-depth="2"]` are the hooks for a
57
+ * rule of yours.
58
+ *
43
59
  * @since 0.1.0
44
60
  * @status stable
45
61
  * @category navigation
@@ -109,6 +125,12 @@ export declare class HelixHeadlessOutline extends HelixElement {
109
125
  * slot names it in place of this.
110
126
  */
111
127
  label: string;
128
+ /**
129
+ * Which links are marked. `reading` marks the one section being read, the
130
+ * last whose heading has passed the reading line. `visible` marks every
131
+ * section with part of it on screen, and the mark runs across their links.
132
+ */
133
+ marks: 'reading' | 'visible';
112
134
  /**
113
135
  * How far below the top of the scroll container a heading counts as reached,
114
136
  * in pixels. The scroll container is the window or the panel the sections
@@ -130,6 +152,14 @@ export declare class HelixHeadlessOutline extends HelixElement {
130
152
  * than saying it in a sentence nobody reads.
131
153
  */
132
154
  get current(): string;
155
+ /**
156
+ * The ids of the sections whose links are marked, in page order: the one
157
+ * `current` names, or under `marks="visible"` every section on screen.
158
+ * Read-only, for the reason `current` gives.
159
+ */
160
+ get marked(): string[];
161
+ /** What `marked` reports, which only `markTo` writes. */
162
+ private markedIds;
133
163
  /**
134
164
  * What `current` reports, which only `mark` writes.
135
165
  *
@@ -183,6 +213,12 @@ export declare class HelixHeadlessOutline extends HelixElement {
183
213
  * wherever the page says the top of the readable area is.
184
214
  */
185
215
  private mark;
216
+ /**
217
+ * The sections with part of them on screen, under `marks="visible"`: each
218
+ * runs from its own target to the next one's, and the last to the end of the
219
+ * page.
220
+ */
221
+ private onScreen;
186
222
  /** Where the destination is set, whether measured or asked for. */
187
223
  private markTo;
188
224
  /**
@@ -293,6 +329,18 @@ export declare class HelixHeadlessOutline extends HelixElement {
293
329
  * handled is not ours, and `auto` is the whole point of `auto`.
294
330
  */
295
331
  private readonly handleClick;
332
+ /**
333
+ * Scrolls to the section with this id, as a press on its link does, with
334
+ * `behavior`, and marks its link at once. The address and focus stay where
335
+ * they are. Returns whether the outline has a link to that section.
336
+ */
337
+ scrollToSection(id: string): boolean;
338
+ /**
339
+ * Marks the section a reader asked for, and holds the mark there until the
340
+ * page stops moving. Scrolls to it where `scroll` says this element does
341
+ * the scrolling, and returns whether it did.
342
+ */
343
+ private travel;
296
344
  /**
297
345
  * Watches every section, and re-marks whenever any of them crosses anything.
298
346
  *
@@ -312,6 +360,15 @@ export declare class HelixHeadlessOutline extends HelixElement {
312
360
  * life of the page.
313
361
  */
314
362
  private readonly handleSlotChange;
363
+ /**
364
+ * Writes each link's depth in the outline, from 1, as `data-depth`: the
365
+ * list items around it inside the outline, or for a flat run, its place in
366
+ * the lists `data-level` makes of it.
367
+ *
368
+ * On the consumer's links, as `aria-current` is, and only after the first
369
+ * render, where a framework has hydrated them.
370
+ */
371
+ private writeDepths;
315
372
  disconnectedCallback(): void;
316
373
  /**
317
374
  * Stops waiting for a scroll to land, both halves of the wait.
@@ -17,11 +17,14 @@ let HelixHeadlessOutline = class HelixHeadlessOutline extends HelixElement {
17
17
  active: () => this.links.find((link) => link.hasAttribute('aria-current')),
18
18
  indicator: () => this.partNamed('indicator'),
19
19
  surface: () => this.partNamed('nav'),
20
+ through: () => this.links.findLast((link) => link.hasAttribute('aria-current')),
20
21
  });
21
22
  this.behavior = 'auto';
22
23
  this.follow = false;
23
24
  this.label = 'On this page';
25
+ this.marks = 'reading';
24
26
  this.offset = 0;
27
+ this.markedIds = [];
25
28
  this.reading = '';
26
29
  this.frame = 0;
27
30
  this.handleScroll = () => {
@@ -42,34 +45,18 @@ let HelixHeadlessOutline = class HelixHeadlessOutline extends HelixElement {
42
45
  if (!section) {
43
46
  return;
44
47
  }
45
- this.landing = section;
46
- this.markTo(section);
47
- this.unland();
48
- const arrive = () => {
49
- this.unland();
50
- this.landing = undefined;
51
- this.mark();
52
- };
53
- this.arrive = arrive;
54
- document.addEventListener('scrollend', arrive, { capture: true, once: true });
55
- this.landed = setTimeout(arrive, 1200);
56
- if (this.behavior === 'auto') {
57
- return;
58
- }
59
- event.preventDefault();
60
- const reduced = globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches === true;
61
- section.target.scrollIntoView({
62
- behavior: reduced && this.behavior === 'smooth' ? 'instant' : this.behavior,
63
- block: 'start',
64
- });
65
- globalThis.history?.pushState(null, '', link?.getAttribute('href') ?? '');
66
- const target = section.target;
67
- if (!target.hasAttribute('tabindex')) {
68
- target.setAttribute('tabindex', '-1');
48
+ if (this.travel(section, this.behavior !== 'auto')) {
49
+ event.preventDefault();
50
+ globalThis.history?.pushState(null, '', link?.getAttribute('href') ?? '');
51
+ const target = section.target;
52
+ if (!target.hasAttribute('tabindex')) {
53
+ target.setAttribute('tabindex', '-1');
54
+ }
55
+ target.focus({ preventScroll: true });
69
56
  }
70
- target.focus({ preventScroll: true });
71
57
  };
72
58
  this.handleSlotChange = () => {
59
+ this.writeDepths();
73
60
  this.watch();
74
61
  this.indicator.place();
75
62
  };
@@ -87,6 +74,9 @@ let HelixHeadlessOutline = class HelixHeadlessOutline extends HelixElement {
87
74
  get current() {
88
75
  return this.reading;
89
76
  }
77
+ get marked() {
78
+ return [...this.markedIds];
79
+ }
90
80
  get links() {
91
81
  const slots = [
92
82
  ...(this.renderRoot?.querySelectorAll?.('slot[data-link], nav > slot:not([role])') ?? []),
@@ -127,20 +117,34 @@ let HelixHeadlessOutline = class HelixHeadlessOutline extends HelixElement {
127
117
  const origin = page ? 0 : scroller.getBoundingClientRect().top;
128
118
  const passed = sections.filter(({ target }) => target.getBoundingClientRect().top - origin - this.offset <= 1);
129
119
  const active = this.atEnd ? sections.at(-1) : (passed.at(-1) ?? sections[0]);
130
- this.markTo(active);
120
+ const shown = this.marks === 'visible' ? this.onScreen(sections, origin, scroller) : [];
121
+ this.markTo(shown.length > 0 ? shown : active ? [active] : []);
131
122
  }
132
- markTo(active) {
123
+ onScreen(sections, origin, scroller) {
124
+ const line = origin + this.offset;
125
+ const bottom = origin + (scroller === this.page ? this.page.clientHeight : scroller.clientHeight);
126
+ const tops = sections.map(({ target }) => target.getBoundingClientRect().top);
127
+ return sections.filter((_, at) => {
128
+ const end = tops[at + 1] ?? Number.POSITIVE_INFINITY;
129
+ return (tops[at] ?? 0) < bottom - 1 && end > line + 1;
130
+ });
131
+ }
132
+ markTo(marked) {
133
+ const on = new Set(marked.map(({ target }) => target));
133
134
  for (const { link, target } of this.sections) {
134
- if (target === active?.target) {
135
+ if (on.has(target)) {
135
136
  link.setAttribute('aria-current', 'location');
136
137
  }
137
138
  else {
138
139
  link.removeAttribute('aria-current');
139
140
  }
140
141
  }
142
+ const active = marked[0];
141
143
  const next = active?.target.id ?? '';
142
- if (next !== this.reading) {
144
+ const ids = marked.map(({ target }) => target.id);
145
+ if (next !== this.reading || ids.join(' ') !== this.markedIds.join(' ')) {
143
146
  this.reading = next;
147
+ this.markedIds = ids;
144
148
  this.indicator.place();
145
149
  if (this.follow && active) {
146
150
  this.reveal(active.link);
@@ -173,6 +177,36 @@ let HelixHeadlessOutline = class HelixHeadlessOutline extends HelixElement {
173
177
  element.style.scrollMarginBlockStart = this.offset > 0 ? `${this.offset}px` : '';
174
178
  }
175
179
  }
180
+ scrollToSection(id) {
181
+ const section = this.sections.find(({ target }) => target.id === id);
182
+ if (!section) {
183
+ return false;
184
+ }
185
+ this.travel(section, true);
186
+ return true;
187
+ }
188
+ travel(section, scroll) {
189
+ this.landing = section;
190
+ this.markTo([section]);
191
+ this.unland();
192
+ const arrive = () => {
193
+ this.unland();
194
+ this.landing = undefined;
195
+ this.mark();
196
+ };
197
+ this.arrive = arrive;
198
+ document.addEventListener('scrollend', arrive, { capture: true, once: true });
199
+ this.landed = setTimeout(arrive, 1200);
200
+ if (!scroll) {
201
+ return false;
202
+ }
203
+ const reduced = globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches === true;
204
+ section.target.scrollIntoView({
205
+ behavior: reduced && this.behavior === 'smooth' ? 'instant' : this.behavior,
206
+ block: 'start',
207
+ });
208
+ return true;
209
+ }
176
210
  watch() {
177
211
  this.observer?.disconnect();
178
212
  if (this.sections.length === 0) {
@@ -188,6 +222,35 @@ let HelixHeadlessOutline = class HelixHeadlessOutline extends HelixElement {
188
222
  this.reach();
189
223
  this.mark();
190
224
  }
225
+ writeDepths() {
226
+ const links = this.links;
227
+ const items = new Map();
228
+ for (const link of links) {
229
+ let count = 0;
230
+ for (let node = link.parentElement; node && node !== this; node = node.parentElement) {
231
+ count += node.localName === 'li' ? 1 : 0;
232
+ }
233
+ items.set(link, count);
234
+ }
235
+ const flat = links.filter((link) => items.get(link) === 0);
236
+ const depth = new Map();
237
+ const walk = (branches, at) => {
238
+ for (const branch of branches) {
239
+ const link = flat[branch.index];
240
+ if (link) {
241
+ depth.set(link, at);
242
+ }
243
+ walk(branch.children, at + 1);
244
+ }
245
+ };
246
+ walk(nest(flat.map((link) => levelOf(link.getAttribute('data-level')))), 1);
247
+ for (const link of links) {
248
+ const at = String(depth.get(link) ?? items.get(link) ?? 1);
249
+ if (link.getAttribute('data-depth') !== at) {
250
+ link.setAttribute('data-depth', at);
251
+ }
252
+ }
253
+ }
191
254
  disconnectedCallback() {
192
255
  super.disconnectedCallback();
193
256
  this.childWatch?.disconnect();
@@ -258,9 +321,12 @@ let HelixHeadlessOutline = class HelixHeadlessOutline extends HelixElement {
258
321
  updated(changed) {
259
322
  super.updated(changed);
260
323
  this.assign();
324
+ this.writeDepths();
261
325
  this.nameByHeading();
262
326
  if (changed.has('offset')) {
263
327
  this.reach();
328
+ }
329
+ if (changed.has('offset') || changed.has('marks')) {
264
330
  this.mark();
265
331
  }
266
332
  if (!this.observer) {
@@ -295,6 +361,9 @@ __decorate([
295
361
  words,
296
362
  property()
297
363
  ], HelixHeadlessOutline.prototype, "label", void 0);
364
+ __decorate([
365
+ property()
366
+ ], HelixHeadlessOutline.prototype, "marks", void 0);
298
367
  __decorate([
299
368
  property({ type: Number })
300
369
  ], HelixHeadlessOutline.prototype, "offset", void 0);
@@ -200,8 +200,22 @@ export declare class HelixHeadlessSpeedDial extends HelixHeadlessSpeedDial_base
200
200
  * attribute the consumer's server never sent is a mismatch whether or not it
201
201
  * moves afterwards, measured in both load orders, and a trigger that says it
202
202
  * is expanded is already saying it has something to expand.
203
+ *
204
+ * **A plain trigger once a reader has reached it.** A `<button>` of yours has
205
+ * no shadow root, so it takes `aria-haspopup="menu"` and `aria-expanded` as
206
+ * attributes, written only once focus or a pointer has reached it. That is
207
+ * after hydration, which is the only time React compares them. A trigger
208
+ * that carries either attribute itself is yours to keep up to date.
203
209
  */
204
210
  private announce;
211
+ /** The plain trigger this dial has written its state on, so it can be taken back. */
212
+ private toldPlain;
213
+ /** Whether a reader has reached the trigger, by focus or a pointer, which is after hydration. */
214
+ private reached;
215
+ /** Takes the state off a plain trigger that is no longer this dial's. */
216
+ private untellPlain;
217
+ /** A reader reaching the trigger, which lets a plain one be told its state. */
218
+ private readonly handleReach;
205
219
  /** The control the dial has described, so the description can be taken back. */
206
220
  private described;
207
221
  /**
@@ -139,6 +139,13 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
139
139
  this.close();
140
140
  event.preventDefault();
141
141
  };
142
+ this.reached = false;
143
+ this.handleReach = (event) => {
144
+ if (!this.reached && event.composedPath().includes(this.trigger)) {
145
+ this.reached = true;
146
+ this.announce();
147
+ }
148
+ };
142
149
  this.handleTriggerFocusIn = () => {
143
150
  if (this.pressHeld) {
144
151
  this.describeTrigger(true);
@@ -186,6 +193,8 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
186
193
  this.addEventListener('click', this.handleHeldClick, true);
187
194
  this.addEventListener('pointerenter', this.handlePointerEnter);
188
195
  this.addEventListener('pointerleave', this.handlePointerLeave);
196
+ this.addEventListener('focusin', this.handleReach);
197
+ this.addEventListener('pointerover', this.handleReach);
189
198
  }
190
199
  disconnectedCallback() {
191
200
  super.disconnectedCallback();
@@ -199,14 +208,37 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
199
208
  this.removeEventListener('click', this.handleHeldClick, true);
200
209
  this.removeEventListener('pointerenter', this.handlePointerEnter);
201
210
  this.removeEventListener('pointerleave', this.handlePointerLeave);
211
+ this.removeEventListener('focusin', this.handleReach);
212
+ this.removeEventListener('pointerover', this.handleReach);
202
213
  this.stay();
203
214
  }
204
215
  announce() {
205
216
  const trigger = this.trigger;
206
- if (trigger === this.partNamed('trigger')) {
217
+ if (trigger === this.partNamed('trigger') || trigger === this) {
218
+ return;
219
+ }
220
+ if (trigger !== this.toldPlain) {
221
+ this.untellPlain();
222
+ }
223
+ if (trigger.localName.includes('-')) {
224
+ tellDisclosure(trigger, this.open, 'menu');
207
225
  return;
208
226
  }
209
- tellDisclosure(trigger, this.open, 'menu');
227
+ if (trigger !== this.toldPlain) {
228
+ if (!this.reached ||
229
+ trigger.hasAttribute('aria-expanded') ||
230
+ trigger.hasAttribute('aria-haspopup')) {
231
+ return;
232
+ }
233
+ this.toldPlain = trigger;
234
+ }
235
+ trigger.setAttribute('aria-haspopup', 'menu');
236
+ trigger.setAttribute('aria-expanded', String(this.open));
237
+ }
238
+ untellPlain() {
239
+ this.toldPlain?.removeAttribute('aria-haspopup');
240
+ this.toldPlain?.removeAttribute('aria-expanded');
241
+ this.toldPlain = undefined;
210
242
  }
211
243
  describeTrigger(light = false) {
212
244
  const trigger = this.trigger;
@@ -28,7 +28,11 @@ import { type PropertyValues, type TemplateResult } from 'lit';
28
28
  * back to the position where the collapsing gesture began. A drag records that position
29
29
  * on press, and every other way of collapsing records it in `settle`.
30
30
  * `collapsed-size` keeps a sliver of the pane visible, such as a tab strip.
31
- * Collapsing overrides `min` and `max`.
31
+ * Collapsing overrides `min` and `max`. `collapsible` says which panes can be
32
+ * collapsed at all, as Zag's panels each say.
33
+ *
34
+ * `min` is the least the start pane keeps and `end-min` the least the end pane
35
+ * keeps, each a percentage or a CSS length such as `16rem`.
32
36
  *
33
37
  * Set `primary` to the pane that keeps its size when the splitter's own size
34
38
  * changes, such as a sidebar beside a window being resized. The other pane
@@ -49,7 +53,7 @@ import { type PropertyValues, type TemplateResult } from 'lit';
49
53
  * | Left, Right | Moves a side-by-side divider by `step`. On a right-to-left page, Left moves it toward the end. |
50
54
  * | Up, Down | Moves a stacked divider by `step`. |
51
55
  * | Home, End | Moves the divider to its bounds. |
52
- * | Enter | Collapses the start pane, and opens it again. |
56
+ * | Enter | Collapses the start pane, or the end pane where `collapsible="end"`, and opens it again. |
53
57
  * | F6 | Moves focus from one pane to the first control in the other, and from the divider to the start pane. |
54
58
  *
55
59
  * @since 0.1.0
@@ -119,6 +123,15 @@ export declare class HelixHeadlessSplitter extends HelixElement {
119
123
  * consumer's to set.
120
124
  */
121
125
  collapsedSize: number;
126
+ /**
127
+ * Which panes can be collapsed, as Zag's panels each take `collapsible`:
128
+ * `start`, `end`, `both`, or `none`. A pane that cannot be collapsed stops
129
+ * at the edge without shutting. `Enter` collapses the end pane where only
130
+ * the end pane can be collapsed, and the start pane otherwise.
131
+ *
132
+ * Unset is `both`, and the attribute stays absent until you write it.
133
+ */
134
+ collapsible?: 'both' | 'end' | 'none' | 'start';
122
135
  /**
123
136
  * The name of the handle where two dividers cross, read by screen readers.
124
137
  */
@@ -141,6 +154,14 @@ export declare class HelixHeadlessSplitter extends HelixElement {
141
154
  * Where the divider starts, when `position` is not set.
142
155
  */
143
156
  defaultPosition: number;
157
+ /**
158
+ * The least the end pane keeps, in the units `min` takes: a percentage, or
159
+ * a CSS length such as `16rem` resolved against the splitter's box. The
160
+ * divider stops that far from the end, whatever `max` allows, so `min` and
161
+ * `end-min` hold a minimum for each pane, as Zag's panels each take
162
+ * `minSize`.
163
+ */
164
+ endMin: number | string;
144
165
  /**
145
166
  * The divider's name, read by screen readers.
146
167
  *
@@ -403,6 +424,8 @@ export declare class HelixHeadlessSplitter extends HelixElement {
403
424
  * latches on any drag that runs off the end.
404
425
  */
405
426
  private shutAt;
427
+ /** Whether `collapsible` lets this pane be collapsed. */
428
+ private canCollapse;
406
429
  /** Pulled between the bounds, which are the ends unless a consumer moved them. */
407
430
  private clamp;
408
431
  /**
@@ -55,7 +55,7 @@ let HelixHeadlessSplitter = class HelixHeadlessSplitter extends HelixElement {
55
55
  return this.toPercent(this.min, 0);
56
56
  }
57
57
  get maxPercent() {
58
- return this.toPercent(this.max, 100);
58
+ return Math.min(this.toPercent(this.max, 100), 100 - this.toPercent(this.endMin, 0));
59
59
  }
60
60
  get position() {
61
61
  const shut = this.collapsed;
@@ -141,10 +141,14 @@ let HelixHeadlessSplitter = class HelixHeadlessSplitter extends HelixElement {
141
141
  }
142
142
  shutAt(position) {
143
143
  const size = this.sliver;
144
- if (position <= size) {
144
+ if (position <= size && this.canCollapse('start')) {
145
145
  return 'start';
146
146
  }
147
- return position >= 100 - size ? 'end' : undefined;
147
+ return position >= 100 - size && this.canCollapse('end') ? 'end' : undefined;
148
+ }
149
+ canCollapse(pane) {
150
+ const collapsible = this.collapsible ?? 'both';
151
+ return collapsible === 'both' || collapsible === pane;
148
152
  }
149
153
  clamp(next) {
150
154
  const min = Math.max(0, Math.min(100, this.minPercent));
@@ -250,6 +254,10 @@ let HelixHeadlessSplitter = class HelixHeadlessSplitter extends HelixElement {
250
254
  }
251
255
  toggleCollapse() {
252
256
  const shut = this.collapsed;
257
+ const pane = this.collapsible === 'end' ? 'end' : 'start';
258
+ if (!shut && !this.canCollapse(pane)) {
259
+ return;
260
+ }
253
261
  const opened = shut ? undefined : this.position;
254
262
  requestChange({
255
263
  apply: (value) => {
@@ -266,7 +274,7 @@ let HelixHeadlessSplitter = class HelixHeadlessSplitter extends HelixElement {
266
274
  },
267
275
  event: (value) => new HelixBeforeCollapseEvent({ value }),
268
276
  host: this,
269
- next: shut ? '' : 'start',
277
+ next: shut ? '' : pane,
270
278
  });
271
279
  }
272
280
  reset() {
@@ -280,6 +288,7 @@ let HelixHeadlessSplitter = class HelixHeadlessSplitter extends HelixElement {
280
288
  this.cornerLabel = 'Resize both';
281
289
  this.cornerValueLabel = '{across} across, {down} down';
282
290
  this.defaultPosition = 50;
291
+ this.endMin = 0;
283
292
  this.label = 'Resize panes';
284
293
  this.aria = new AriaForwardController(this, {
285
294
  label: true,
@@ -665,6 +674,9 @@ __decorate([
665
674
  __decorate([
666
675
  property({ attribute: 'collapsed-size', type: Number })
667
676
  ], HelixHeadlessSplitter.prototype, "collapsedSize", void 0);
677
+ __decorate([
678
+ property()
679
+ ], HelixHeadlessSplitter.prototype, "collapsible", void 0);
668
680
  __decorate([
669
681
  words,
670
682
  property({ attribute: 'corner-label' })
@@ -679,6 +691,9 @@ __decorate([
679
691
  __decorate([
680
692
  property({ attribute: 'default-position', type: Number })
681
693
  ], HelixHeadlessSplitter.prototype, "defaultPosition", void 0);
694
+ __decorate([
695
+ property({ attribute: 'end-min' })
696
+ ], HelixHeadlessSplitter.prototype, "endMin", void 0);
682
697
  __decorate([
683
698
  words,
684
699
  property()
@@ -22,7 +22,8 @@ export type TagSelectionMode = SelectionMode;
22
22
  * not disabled. `selection-behavior="replace"` selects the tag a reader
23
23
  * presses or moves to alone, as a file list does, and Control or Command
24
24
  * with a press or Space adds or takes out one tag. `disallow-empty-selection`
25
- * keeps one tag selected once one is. `value` holds the
25
+ * keeps one tag selected once one is, and Escape lets go of the selection
26
+ * otherwise. `value` holds the
26
27
  * selected tags' values, separated by commas, and a change asks with
27
28
  * `helix-before-change` and tells with `change`. Enter on a tag with an
28
29
  * `href` follows its link. A `disabled` tag is passed over. A selected tag
@@ -40,6 +41,12 @@ export type TagSelectionMode = SelectionMode;
40
41
  * Where only disabled tags are left, the first of them holds the tab stop and
41
42
  * takes focus, so the reader stays in the set.
42
43
  *
44
+ * **Typing new tags.** A field where people type tags and press Enter to add
45
+ * each one is Combobox with `multiple` and `allows-custom-value`, or
46
+ * `creatable` to ask first with `helix-create`. It draws each value as a tag,
47
+ * Backspace on the empty field removes the last one, and the arrow keys move
48
+ * between them.
49
+ *
43
50
  * @since 0.1.0
44
51
  * @status stable
45
52
  * @aliases Tag list, Chip group, Token list
@@ -46,6 +46,14 @@ let HelixHeadlessTagGroup = class HelixHeadlessTagGroup extends HelixElement {
46
46
  if (event.ctrlKey || event.metaKey) {
47
47
  return;
48
48
  }
49
+ if (event.key === 'Escape') {
50
+ if (this.selectionMode && this.written !== '' && !this.disallowEmptySelection) {
51
+ event.preventDefault();
52
+ event.stopPropagation();
53
+ this.ask('');
54
+ }
55
+ return;
56
+ }
49
57
  if (MOVES.has(event.key)) {
50
58
  const to = this.last;
51
59
  if (multiple && event.shiftKey && to && to !== tag) {
@@ -28,6 +28,12 @@ declare const HelixHeadlessTag_base: (new (...args: any[]) => import("@fusion.de
28
28
  * selected. Its remove button leaves the tab order there, Delete and Backspace
29
29
  * doing its job from the row.
30
30
  *
31
+ * **Styling a tag by what is happening to it.** `:state(selected)` while it is
32
+ * selected, `:state(focus-visible)` while its cell shows keyboard focus,
33
+ * `[disabled]` while it is out of reach, and the host's own `:hover` and
34
+ * `:active` under the pointer, as React Aria's `data-hovered` and
35
+ * `data-pressed` are.
36
+ *
31
37
  * @since 0.1.0
32
38
  * @status stable
33
39
  * @category markers
@@ -47,6 +53,7 @@ declare const HelixHeadlessTag_base: (new (...args: any[]) => import("@fusion.de
47
53
  * @csspart link - The link around the label, under `href`.
48
54
  * @csspart start - The category before the label.
49
55
  *
56
+ * @cssstate focus-visible - The tag's cell has keyboard focus that the browser shows, in a `Tag.Group`.
50
57
  * @cssstate selected - The tag is selected in a `Tag.Group` that selects.
51
58
  */
52
59
  export declare class HelixHeadlessTag extends HelixHeadlessTag_base {
@@ -131,6 +138,11 @@ export declare class HelixHeadlessTag extends HelixHeadlessTag_base {
131
138
  */
132
139
  private get removeName();
133
140
  protected firstUpdated(changed: PropertyValues): void;
141
+ /**
142
+ * Says whether the cell shows keyboard focus, as `:focus-visible` does, so a
143
+ * sheet outside can draw the ring on the tag by the host.
144
+ */
145
+ private readonly handleCellFocus;
134
146
  protected render(): TemplateResult;
135
147
  /** The label, inside a link under `href`, behind a seam a tier can move. */
136
148
  protected renderLabel(): TemplateResult;
@@ -15,6 +15,16 @@ let HelixHeadlessTag = class HelixHeadlessTag extends MixinDismissible(HelixElem
15
15
  this.dismissTabStop = true;
16
16
  this.href = '';
17
17
  this.value = '';
18
+ this.handleCellFocus = (event) => {
19
+ const cell = event.currentTarget;
20
+ let shown = false;
21
+ try {
22
+ shown = event.type === 'focus' && cell.matches(':focus-visible');
23
+ }
24
+ catch {
25
+ }
26
+ this.toggleState('focus-visible', shown);
27
+ };
18
28
  }
19
29
  static { this.words = 'helix-tag'; }
20
30
  get words() {
@@ -66,6 +76,8 @@ let HelixHeadlessTag = class HelixHeadlessTag extends MixinDismissible(HelixElem
66
76
  part="cell"
67
77
  role="gridcell"
68
78
  tabindex=${this.focusable ? '0' : '-1'}
79
+ @focus=${this.handleCellFocus}
80
+ @blur=${this.handleCellFocus}
69
81
  >${content}</span
70
82
  >`
71
83
  : content;
@@ -469,6 +469,20 @@ export declare class HelixHeadlessTimePicker extends HelixHeadlessTimePicker_bas
469
469
  * The validation message for a time that `step` skips.
470
470
  */
471
471
  stepMismatchMessage: string;
472
+ /**
473
+ * What each time in the list shows: a function given the time as `HH:mm`,
474
+ * or `HH:mm:ss` with `seconds`, and the time as the row writes it, that
475
+ * returns text, an element or a Lit template.
476
+ *
477
+ * For the seats left at a time, or a caption under it, as React Aria's
478
+ * `ListBoxItem` takes children. Content with `slot="start"`, `slot="end"`
479
+ * or `slot="description"` goes to that slot of the row, and a description
480
+ * is read after the time. The row is still named by the written time, and
481
+ * typeahead still finds it by that. Return the written time with your
482
+ * content where it should still show. A property only, there being no way
483
+ * to write a function in markup. The wheels draw their own figures.
484
+ */
485
+ timeContent?: ((time: string, written: string) => unknown) | undefined;
472
486
  /**
473
487
  * The validation message for a time that `disable-times` bars.
474
488
  */
@@ -480,6 +480,8 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerF
480
480
  ${this.times.map((seconds) => {
481
481
  const at = this.valueFor(seconds);
482
482
  const between = this.between(at);
483
+ const written = writeClock(at, this.clock, this.seconds);
484
+ const drawn = this.timeContent;
483
485
  return html `<${this.tag('option')}
484
486
  part="option${between ? ' option-between' : ''}"
485
487
  exportparts="description: option-description, item: option-item,
@@ -489,7 +491,9 @@ let HelixHeadlessTimePicker = class HelixHeadlessTimePicker extends MixinPickerF
489
491
  ?disabled=${this.barred(at)}
490
492
  ?selected=${this.held.includes(at)}
491
493
  data-between=${between ? '' : nothing}
492
- >${writeClock(at, this.clock, this.seconds)}</${this.tag('option')}
494
+ aria-label=${drawn ? written : nothing}
495
+ text-value=${drawn ? written : nothing}
496
+ >${drawn ? drawn(at, written) : written}</${this.tag('option')}
493
497
  >`;
494
498
  })}
495
499
  <slot name="empty" slot="empty">${this.emptyText}</slot>
@@ -600,6 +604,9 @@ __decorate([
600
604
  words,
601
605
  property({ attribute: 'step-mismatch-message' })
602
606
  ], HelixHeadlessTimePicker.prototype, "stepMismatchMessage", void 0);
607
+ __decorate([
608
+ property({ attribute: false })
609
+ ], HelixHeadlessTimePicker.prototype, "timeContent", void 0);
603
610
  __decorate([
604
611
  words,
605
612
  property({ attribute: 'unavailable-message' })