@fusion.dev/helix-headless 0.24.0 → 0.25.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.
@@ -72,6 +72,7 @@ export declare class HelixHeadlessAvatarGroup extends HelixElement {
72
72
  /** Said once per element, because a re-render is not a second mistake. */
73
73
  private warned;
74
74
  protected willUpdate(changed: PropertyValues): void;
75
+ protected firstUpdated(changed: PropertyValues): void;
75
76
  protected updated(changed: PropertyValues): void;
76
77
  protected render(): TemplateResult;
77
78
  }
@@ -15,7 +15,7 @@ let HelixHeadlessAvatarGroup = class HelixHeadlessAvatarGroup extends HelixEleme
15
15
  this.warned = false;
16
16
  }
17
17
  get avatars() {
18
- return [...(this.children ?? [])].filter((child) => child instanceof HTMLElement);
18
+ return this.knownChildren.filter((child) => child instanceof HTMLElement);
19
19
  }
20
20
  get overflow() {
21
21
  const limit = this.max ?? 0;
@@ -26,6 +26,10 @@ let HelixHeadlessAvatarGroup = class HelixHeadlessAvatarGroup extends HelixEleme
26
26
  super.willUpdate(changed);
27
27
  hostAria(this.internals, { label: this.label, role: 'group' });
28
28
  }
29
+ firstUpdated(changed) {
30
+ super.firstUpdated(changed);
31
+ this.finishHydration();
32
+ }
29
33
  updated(changed) {
30
34
  super.updated(changed);
31
35
  const limit = this.max ?? 0;
@@ -53,6 +53,7 @@ export declare class HelixHeadlessAvatar extends HelixElement {
53
53
  private readonly handleError;
54
54
  private readonly handleLoad;
55
55
  protected willUpdate(changed: PropertyValues): void;
56
+ protected firstUpdated(changed: PropertyValues): void;
56
57
  protected render(): TemplateResult;
57
58
  /**
58
59
  * The mark shown where no image loaded, behind a seam a tier can move.
@@ -41,6 +41,17 @@ let HelixHeadlessAvatar = class HelixHeadlessAvatar extends HelixElement {
41
41
  role: this.name === '' ? '' : 'img',
42
42
  });
43
43
  }
44
+ firstUpdated(changed) {
45
+ super.firstUpdated(changed);
46
+ const image = this.serverRendered ? this.renderRoot.querySelector('img') : null;
47
+ if (image?.complete) {
48
+ const broken = image.naturalWidth === 0;
49
+ if (broken) {
50
+ this.failed = true;
51
+ }
52
+ setTimeout(() => this.relayNativeEvent(new Event(broken ? 'error' : 'load')));
53
+ }
54
+ }
44
55
  render() {
45
56
  if (this.src !== '' && !this.failed) {
46
57
  return html `<img
@@ -112,6 +112,8 @@ export declare class HelixHeadlessCarousel extends HelixElement {
112
112
  interval: number;
113
113
  /** The slide in view, whoever set it, and `undefined` until somebody has. */
114
114
  private indexState;
115
+ /** How many panes were drawn at the last update, to scroll when the first are. */
116
+ private scrolledFor;
115
117
  /**
116
118
  * Accessible name for the carousel.
117
119
  */
@@ -16,6 +16,7 @@ let HelixHeadlessCarousel = class HelixHeadlessCarousel extends HelixElement {
16
16
  this.defaultIndex = 0;
17
17
  this.interval = 5000;
18
18
  this.indexState = undefined;
19
+ this.scrolledFor = 0;
19
20
  this.label = 'Carousel';
20
21
  this.loop = false;
21
22
  this.rotating = false;
@@ -207,11 +208,13 @@ let HelixHeadlessCarousel = class HelixHeadlessCarousel extends HelixElement {
207
208
  }
208
209
  updated(changed) {
209
210
  super.updated(changed);
210
- if (changed.has('index')) {
211
+ const count = this.slides.length;
212
+ const drawn = this.renderRoot.querySelectorAll('[part="slide"]').length;
213
+ if (changed.has('index') || (this.scrolledFor === 0 && drawn > 0)) {
211
214
  this.scrollToIndex();
212
215
  }
216
+ this.scrolledFor = drawn;
213
217
  this.runTimer();
214
- const count = this.slides.length;
215
218
  if (count > 0 && this.index > count - 1) {
216
219
  this.goTo(count - 1);
217
220
  }
@@ -298,6 +298,7 @@ export declare class HelixHeadlessCombobox extends HelixHeadlessCombobox_base {
298
298
  * component: a form that received "jav" got the filter, not an option.
299
299
  */
300
300
  private sync;
301
+ protected firstUpdated(changed: PropertyValues): void;
301
302
  protected willUpdate(changed: PropertyValues): void;
302
303
  protected updated(changed: PropertyValues): void;
303
304
  /**
@@ -10,6 +10,7 @@ import { nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
11
11
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
12
12
  import { html } from 'lit/static-html.js';
13
+ import { optionsAmong } from '../../option/root/option-root.js';
13
14
  import '../../listbox/index.js';
14
15
  import '../../tag/index.js';
15
16
  const isInsertion = (event) => !event.inputType?.startsWith('delete');
@@ -138,7 +139,7 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
138
139
  return this.value === '' ? [] : this.value.split(',');
139
140
  }
140
141
  get chosen() {
141
- const options = this.listbox?.options ?? [];
142
+ const options = optionsAmong(this.knownChildren);
142
143
  const known = options.length > 0;
143
144
  const words = new Map(options.map((option) => [option.value, option.text]));
144
145
  return this.values
@@ -214,6 +215,10 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
214
215
  this.internals.setFormValue(this.value === '' ? null : this.value);
215
216
  this.reportRequired(this.value === '', 'Choose one to continue.', 'control');
216
217
  }
218
+ firstUpdated(changed) {
219
+ super.firstUpdated(changed);
220
+ this.finishHydration();
221
+ }
217
222
  willUpdate(changed) {
218
223
  super.willUpdate(changed);
219
224
  this.internals.ariaExpanded = this.open ? 'true' : 'false';
@@ -86,6 +86,11 @@ export declare class HelixHeadlessCommand extends HelixHeadlessCommand_base {
86
86
  * child element, which is the slot, so `:not(:has(*))` on it is never true
87
87
  * and an empty footer would draw its rule and its padding over nothing. Steps
88
88
  * was caught by the same thing.
89
+ *
90
+ * Read from the children before each render, and heard from the slot when
91
+ * they change. The slot alone left the first render without its footer and a
92
+ * second to follow, and a hydrated palette without one for good: a
93
+ * declarative shadow root reports its slots before anything listens.
89
94
  */
90
95
  private footed;
91
96
  private get listbox();
@@ -144,6 +149,8 @@ export declare class HelixHeadlessCommand extends HelixHeadlessCommand_base {
144
149
  private readonly swallowOptionSelect;
145
150
  connectedCallback(): void;
146
151
  disconnectedCallback(): void;
152
+ protected willUpdate(changed: PropertyValues): void;
153
+ protected firstUpdated(changed: PropertyValues): void;
147
154
  protected updated(changed: PropertyValues): void;
148
155
  /**
149
156
  * The surface the palette opens in, behind a seam the styled tier can move.
@@ -95,6 +95,16 @@ let HelixHeadlessCommand = class HelixHeadlessCommand extends MixinControllerOpe
95
95
  this.removeEventListener('click', this.handleAlternate, { capture: true });
96
96
  document.removeEventListener('keydown', this.handleHotkey);
97
97
  }
98
+ willUpdate(changed) {
99
+ super.willUpdate(changed);
100
+ if (this.childrenKnown) {
101
+ this.footed = this.knownChildren.some((child) => child.slot === 'footer');
102
+ }
103
+ }
104
+ firstUpdated(changed) {
105
+ super.firstUpdated(changed);
106
+ this.finishHydration();
107
+ }
98
108
  updated(changed) {
99
109
  super.updated?.(changed);
100
110
  if (!changed.has('open')) {
@@ -73,11 +73,14 @@ let HelixHeadlessFormatDate = class HelixHeadlessFormatDate extends HelixElement
73
73
  }
74
74
  firstUpdated(changed) {
75
75
  super.firstUpdated(changed);
76
- this.read();
76
+ this.finishHydration();
77
77
  this.watch();
78
78
  }
79
79
  willUpdate(changed) {
80
80
  super.willUpdate(changed);
81
+ if (this.childrenKnown) {
82
+ this.read();
83
+ }
81
84
  if (changed.has('date') || changed.has('elapsed') || changed.has('locale')) {
82
85
  this.tick();
83
86
  }
@@ -150,6 +150,15 @@ export declare class HelixHeadlessImage extends HelixElement {
150
150
  * image it has, so deciding `error` here calls every picture broken the moment
151
151
  * it renders. Failure has an event of its own and that event is reliable, so
152
152
  * this is only here to catch the success that happened too early to hear.
153
+ *
154
+ * **Except for a picture a server sent**, which a real browser's parser has
155
+ * already fetched: finished with no pixels there is a failure, and its `error`
156
+ * fired before anything could listen, so the placeholder stayed up over a
157
+ * picture that was never coming.
158
+ *
159
+ * **Once per picture.** The first update asks from `firstUpdated` and from
160
+ * `updated`, and a picture already finished answered both, so a page heard
161
+ * every cached `load`, and every `error` a server's page had, twice.
153
162
  */
154
163
  private settle;
155
164
  protected willUpdate(changed: PropertyValues): void;
@@ -34,14 +34,27 @@ let HelixHeadlessImage = class HelixHeadlessImage extends HelixElement {
34
34
  this.relayNativeEvent(event);
35
35
  return;
36
36
  }
37
- this.dispatchEvent(new Event(name, { bubbles: false, cancelable: false, composed: false }));
37
+ const send = () => {
38
+ this.dispatchEvent(new Event(name, { bubbles: false, cancelable: false, composed: false }));
39
+ };
40
+ if (this.serverRendered) {
41
+ setTimeout(send);
42
+ }
43
+ else {
44
+ send();
45
+ }
38
46
  }
39
47
  settle() {
40
48
  const image = this.renderRoot?.querySelector('img');
41
- if (this.src === '' || !image?.complete || image.naturalWidth === 0) {
49
+ if (this.src === '' || this.status !== 'loading' || !image?.complete) {
42
50
  return;
43
51
  }
44
- this.announce('loaded');
52
+ if (image.naturalWidth > 0) {
53
+ this.announce('loaded');
54
+ }
55
+ else if (this.serverRendered) {
56
+ this.announce('error');
57
+ }
45
58
  }
46
59
  willUpdate(changed) {
47
60
  super.willUpdate(changed);
@@ -1,5 +1,5 @@
1
1
  import { HelixElement } from '@fusion.dev/helix-core';
2
- import { type TemplateResult } from 'lit';
2
+ import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  /**
4
4
  * @summary A keyboard key or chord, set inline with text.
5
5
  *
@@ -57,7 +57,13 @@ export declare class HelixHeadlessKbd extends HelixElement {
57
57
  connectedCallback(): void;
58
58
  disconnectedCallback(): void;
59
59
  protected updated(changed: Map<string, unknown>): void;
60
- /** The chord as this reader's keyboard labels it. */
60
+ /**
61
+ * The chord as this reader's keyboard labels it.
62
+ *
63
+ * As a server labels it while hydrating what one sent: it has no keyboard to
64
+ * ask and serves the words, and a render that differs is a text Lit records
65
+ * and never writes, so a Mac kept Ctrl. The update after hydration asks.
66
+ */
61
67
  private get label();
62
68
  private listen;
63
69
  private deafen;
@@ -72,6 +78,7 @@ export declare class HelixHeadlessKbd extends HelixElement {
72
78
  private get printable();
73
79
  /** Whether a press is this chord, modifiers and all. */
74
80
  private matches_;
81
+ protected firstUpdated(changed: PropertyValues): void;
75
82
  protected render(): TemplateResult;
76
83
  }
77
84
  declare global {
@@ -105,13 +105,14 @@ let HelixHeadlessKbd = class HelixHeadlessKbd extends HelixElement {
105
105
  }
106
106
  }
107
107
  get label() {
108
- const names = onApple() ? APPLE : ELSEWHERE;
108
+ const apple = !this.hydrating && onApple();
109
+ const names = apple ? APPLE : ELSEWHERE;
109
110
  const parts = this.keys
110
111
  .split('+')
111
112
  .map((part) => part.trim())
112
113
  .filter((part) => part !== '')
113
114
  .map((part) => names[part.toLowerCase()] ?? (part.length === 1 ? part.toUpperCase() : part));
114
- return parts.join(onApple() ? '' : '+');
115
+ return parts.join(apple ? '' : '+');
115
116
  }
116
117
  listen() {
117
118
  if (!this.within || this.keys === '') {
@@ -157,6 +158,10 @@ let HelixHeadlessKbd = class HelixHeadlessKbd extends HelixElement {
157
158
  ? true
158
159
  : !press.metaKey));
159
160
  }
161
+ firstUpdated(changed) {
162
+ super.firstUpdated(changed);
163
+ this.finishHydration();
164
+ }
160
165
  render() {
161
166
  return html `<kbd part="kbd">${this.keys === '' ? html `<slot></slot>` : this.label}</kbd>`;
162
167
  }
@@ -1,5 +1,22 @@
1
1
  import { HelixElement } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
+ /**
4
+ * The options among some elements, reaching one level into a group.
5
+ *
6
+ * What a control holding options reads its words from, off its own children
7
+ * rather than off the listbox they are slotted into: the slot exists only once a
8
+ * render has made it, so the listbox has none on a first render, and on a page a
9
+ * server rendered the `slotchange` that would correct it fires before anything
10
+ * listens.
11
+ *
12
+ * **An option by its shape, not by `instanceof`.** A tag is defined once, by
13
+ * whichever copy of this class registered it first, and a second copy in the
14
+ * same document tests against a class no element on the page has: every option
15
+ * failed it, and a select said "Choose" over its value. Two copies are a
16
+ * consumer with a duplicated dependency, or the browser specs, where each
17
+ * family's bundle carries its own and they share one page.
18
+ */
19
+ export declare const optionsAmong: (elements: readonly Element[]) => HelixHeadlessOption[];
3
20
  /**
4
21
  * @summary One choice in a listbox.
5
22
  *
@@ -7,6 +7,10 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { customElement, HelixElement, HelixSelectEvent } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
+ export const optionsAmong = (elements) => {
11
+ const isOption = (element) => 'text' in element && 'value' in element && 'selected' in element;
12
+ return elements.flatMap((element) => isOption(element) ? [element] : [...element.children].filter(isOption));
13
+ };
10
14
  let HelixHeadlessOption = class HelixHeadlessOption extends HelixElement {
11
15
  constructor() {
12
16
  super(...arguments);
@@ -1,5 +1,5 @@
1
1
  import { HelixElement } from '@fusion.dev/helix-core';
2
- import { type TemplateResult } from 'lit';
2
+ import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  /** How much of the code can be lost and still read. */
4
4
  export type QrCorrection = 'H' | 'L' | 'M' | 'Q';
5
5
  /** How the modules are drawn. */
@@ -81,6 +81,8 @@ export declare class HelixHeadlessQrCode extends HelixElement {
81
81
  value: string;
82
82
  /** Whether anything is slotted into the middle. */
83
83
  private logo;
84
+ protected willUpdate(changed: PropertyValues): void;
85
+ protected firstUpdated(changed: PropertyValues): void;
84
86
  /** The level actually used, which the slot can raise. */
85
87
  private get level();
86
88
  private renderCode;
@@ -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
  import { customElement, HelixElement } from '@fusion.dev/helix-core';
8
- import { html, nothing, svg } from 'lit';
8
+ import { html, nothing, svg, } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { encode } from 'uqr';
11
11
  const isFinder = (x, y, size) => {
@@ -67,6 +67,16 @@ let HelixHeadlessQrCode = class HelixHeadlessQrCode extends HelixElement {
67
67
  this.value = '';
68
68
  this.logo = false;
69
69
  }
70
+ willUpdate(changed) {
71
+ super.willUpdate(changed);
72
+ if (this.childrenKnown) {
73
+ this.logo = this.knownChildren.some((child) => child.slot === 'logo');
74
+ }
75
+ }
76
+ firstUpdated(changed) {
77
+ super.firstUpdated(changed);
78
+ this.finishHydration();
79
+ }
70
80
  get level() {
71
81
  return this.correction ?? (this.logo ? 'H' : 'M');
72
82
  }
@@ -111,6 +111,8 @@ export declare class HelixHeadlessRating extends HelixHeadlessRating_base {
111
111
  * ignored.
112
112
  */
113
113
  private markSource;
114
+ /** Whether `markSource` has had its first answer from the children. */
115
+ private markRead;
114
116
  /**
115
117
  * What the pointer is over, which is shown instead of the score while it is.
116
118
  */
@@ -194,6 +196,7 @@ export declare class HelixHeadlessRating extends HelixHeadlessRating_base {
194
196
  connectedCallback(): void;
195
197
  disconnectedCallback(): void;
196
198
  protected updated(changed: PropertyValues): void;
199
+ protected willUpdate(changed: PropertyValues): void;
197
200
  protected firstUpdated(changed: PropertyValues): void;
198
201
  protected render(): TemplateResult;
199
202
  /**
@@ -18,6 +18,7 @@ let HelixHeadlessRating = class HelixHeadlessRating extends MixinControllerValue
18
18
  this.step = 1;
19
19
  this.valueText = '';
20
20
  this.markSource = undefined;
21
+ this.markRead = false;
21
22
  this.previewing = undefined;
22
23
  this.keys = new KeyboardNavigationController(this, {
23
24
  items: () => this.options,
@@ -157,8 +158,16 @@ let HelixHeadlessRating = class HelixHeadlessRating extends MixinControllerValue
157
158
  super.updated?.(changed);
158
159
  this.sync();
159
160
  }
161
+ willUpdate(changed) {
162
+ super.willUpdate(changed);
163
+ if (!this.markRead && this.childrenKnown) {
164
+ this.markRead = true;
165
+ this.markSource = this.knownChildren.find((child) => child.slot === 'mark');
166
+ }
167
+ }
160
168
  firstUpdated(changed) {
161
169
  super.firstUpdated?.(changed);
170
+ this.finishHydration();
162
171
  this.sync();
163
172
  }
164
173
  render() {
@@ -124,6 +124,13 @@ export declare class HelixHeadlessSelect extends HelixHeadlessSelect_base {
124
124
  * Read off the options rather than stored, because the words are the
125
125
  * consumer's content and storing a copy is storing something that goes stale
126
126
  * the moment they change it.
127
+ *
128
+ * **Off this element's own children, not the listbox's**, which reach the
129
+ * listbox through a slot and so only after a render has made it: read there,
130
+ * the first render showed the placeholder over a chosen value and waited on a
131
+ * `slotchange`, which a declarative shadow root fires before anything listens,
132
+ * so a hydrated select kept "Choose" for good. Nothing on a server or while
133
+ * hydrating what one sent, which drew the placeholder too.
127
134
  */
128
135
  private get display();
129
136
  /** Closes the list and puts focus back where it came from. */
@@ -133,6 +140,7 @@ export declare class HelixHeadlessSelect extends HelixHeadlessSelect_base {
133
140
  /** Reports the answer and the verdict on it. */
134
141
  private sync;
135
142
  protected willUpdate(changed: PropertyValues): void;
143
+ protected firstUpdated(changed: PropertyValues): void;
136
144
  protected updated(changed: PropertyValues): void;
137
145
  /**
138
146
  * The list the trigger opens, behind a seam the styled tier can move.
@@ -10,6 +10,7 @@ import { nothing } from 'lit';
10
10
  import { property } from 'lit/decorators.js';
11
11
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
12
12
  import { html } from 'lit/static-html.js';
13
+ import { optionsAmong } from '../../option/root/option-root.js';
13
14
  import '../../listbox/index.js';
14
15
  let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(MixinControllerValue(MixinField(HelixFormElement))) {
15
16
  constructor() {
@@ -69,7 +70,7 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
69
70
  }
70
71
  get display() {
71
72
  const chosen = new Set(this.value === '' ? [] : this.value.split(','));
72
- const words = (this.listbox?.options ?? [])
73
+ const words = optionsAmong(this.knownChildren)
73
74
  .filter((option) => chosen.has(option.value))
74
75
  .map((option) => option.text);
75
76
  return words.length > 0 ? words.join(', ') : this.placeholder;
@@ -89,6 +90,10 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
89
90
  super.willUpdate(changed);
90
91
  hostAria(this.internals, { label: this.label });
91
92
  }
93
+ firstUpdated(changed) {
94
+ super.firstUpdated(changed);
95
+ this.finishHydration();
96
+ }
92
97
  updated(changed) {
93
98
  super.updated(changed);
94
99
  this.sync();
@@ -122,11 +127,8 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
122
127
  @change=${this.stopInner}
123
128
  @input=${this.stopInner}
124
129
  >
125
- <!-- The trigger's words are read off the options, and on the first
126
- render there are none: the slot has not been assigned yet, so a
127
- select with a value came up showing its placeholder. Assignment
128
- changes no property, so nothing else would schedule the update
129
- that fixes it. -->
130
+ <!-- An option arriving or leaving changes no property, so this is what
131
+ schedules the update that rereads the trigger's words. -->
130
132
  <slot @slotchange=${() => this.requestUpdate()}></slot>
131
133
  </${this.tag('listbox')}>`;
132
134
  }
@@ -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 { customElement, HelixElement, hostAria, MixinControllerActive, } from '@fusion.dev/helix-core';
7
+ import { customElement, HelixElement, hostAria, MixinControllerActive, whenHydrated, } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(HelixElement) {
@@ -28,7 +28,10 @@ let HelixHeadlessSteps = class HelixHeadlessSteps extends MixinControllerActive(
28
28
  const steps = this.steps;
29
29
  const value = this.resolveActive(steps)?.value ?? '';
30
30
  for (const step of steps) {
31
- step.current = step.value === value;
31
+ const current = step.value === value;
32
+ whenHydrated(step, () => {
33
+ step.current = current;
34
+ });
32
35
  }
33
36
  }
34
37
  connectedCallback() {
@@ -90,6 +90,15 @@ export declare class HelixHeadlessTour extends HelixElement {
90
90
  * Where the light falls, read from the current target.
91
91
  */
92
92
  private spot;
93
+ /**
94
+ * Watches the lit target's own box, which moves the light without this
95
+ * element updating or the window changing: a label whose font arrives late,
96
+ * words a page rewrites. Measured on a server's page, the spotlight sat two
97
+ * pixels inside each side of a button that widened once its font loaded.
98
+ */
99
+ private sizer;
100
+ /** What `sizer` is watching, so a render that changed nothing re-observes nothing. */
101
+ private sized;
93
102
  /** The stops, in the order they were written. */
94
103
  get stops(): HelixHeadlessTourStop[];
95
104
  /** How many stops there are. */
@@ -124,8 +133,12 @@ export declare class HelixHeadlessTour extends HelixElement {
124
133
  * localised the tour.
125
134
  */
126
135
  private settle;
136
+ /** The current stop's target while the run is open, which is what the light falls on. */
137
+ private get lit();
127
138
  /** Reads the current target's box, which is where the light falls. */
128
139
  private light;
140
+ /** Keeps `sizer` on the lit target, and on nothing once there is none. */
141
+ private watch;
129
142
  protected render(): TemplateResult;
130
143
  }
131
144
  declare global {
@@ -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 { customElement, HelixBeforeActivateEvent, HelixBeforeCloseEvent, HelixCompleteEvent, HelixElement, hostAria, requestChange, } from '@fusion.dev/helix-core';
7
+ import { customElement, HelixBeforeActivateEvent, HelixBeforeCloseEvent, HelixCompleteEvent, HelixElement, hostAria, requestChange, whenHydrated, } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property, queryAssignedElements, state } from 'lit/decorators.js';
10
10
  let HelixHeadlessTour = class HelixHeadlessTour extends HelixElement {
@@ -19,6 +19,8 @@ let HelixHeadlessTour = class HelixHeadlessTour extends HelixElement {
19
19
  this.open = false;
20
20
  this.skipLabel = '';
21
21
  this.spotlightPadding = 8;
22
+ this.sizer = undefined;
23
+ this.sized = undefined;
22
24
  this.handleAsk = (event) => {
23
25
  const what = event.detail?.what;
24
26
  if (what === 'next') {
@@ -96,6 +98,9 @@ let HelixHeadlessTour = class HelixHeadlessTour extends HelixElement {
96
98
  this.ownerDocument.addEventListener('keydown', this.handleKeydown);
97
99
  globalThis.addEventListener('resize', this.handleViewport, { passive: true });
98
100
  globalThis.addEventListener('scroll', this.handleViewport, { capture: true, passive: true });
101
+ if (this.hasUpdated) {
102
+ this.watch(this.lit);
103
+ }
99
104
  }
100
105
  disconnectedCallback() {
101
106
  super.disconnectedCallback();
@@ -103,6 +108,8 @@ let HelixHeadlessTour = class HelixHeadlessTour extends HelixElement {
103
108
  this.ownerDocument.removeEventListener('keydown', this.handleKeydown);
104
109
  globalThis.removeEventListener('resize', this.handleViewport);
105
110
  globalThis.removeEventListener('scroll', this.handleViewport, { capture: true });
111
+ this.sizer?.disconnect();
112
+ this.sized = undefined;
106
113
  }
107
114
  willUpdate(changed) {
108
115
  super.willUpdate(changed);
@@ -117,28 +124,34 @@ let HelixHeadlessTour = class HelixHeadlessTour extends HelixElement {
117
124
  const stops = this.stops;
118
125
  const at = Math.max(0, Math.min(this.index, stops.length - 1));
119
126
  stops.forEach((stop, index) => {
120
- stop.current = this.open && index === at;
121
- stop.first = index === 0;
122
- stop.last = index === stops.length - 1;
123
- if (this.backLabel !== '') {
124
- stop.backLabel = this.backLabel;
125
- }
126
- if (this.nextLabel !== '') {
127
- stop.nextLabel = this.nextLabel;
128
- }
129
- if (this.doneLabel !== '') {
130
- stop.doneLabel = this.doneLabel;
131
- }
132
- if (this.skipLabel !== '') {
133
- stop.skipLabel = this.skipLabel;
134
- }
127
+ whenHydrated(stop, () => {
128
+ stop.current = this.open && index === at;
129
+ stop.first = index === 0;
130
+ stop.last = index === stops.length - 1;
131
+ if (this.backLabel !== '') {
132
+ stop.backLabel = this.backLabel;
133
+ }
134
+ if (this.nextLabel !== '') {
135
+ stop.nextLabel = this.nextLabel;
136
+ }
137
+ if (this.doneLabel !== '') {
138
+ stop.doneLabel = this.doneLabel;
139
+ }
140
+ if (this.skipLabel !== '') {
141
+ stop.skipLabel = this.skipLabel;
142
+ }
143
+ });
135
144
  });
136
145
  }
137
- light() {
146
+ get lit() {
138
147
  const stops = this.stops;
139
- const target = this.open
148
+ return this.open
140
149
  ? stops[Math.max(0, Math.min(this.index, stops.length - 1))]?.target
141
150
  : undefined;
151
+ }
152
+ light() {
153
+ const target = this.lit;
154
+ this.watch(target);
142
155
  if (!target || typeof target.getBoundingClientRect !== 'function') {
143
156
  this.spot = undefined;
144
157
  return;
@@ -160,6 +173,18 @@ let HelixHeadlessTour = class HelixHeadlessTour extends HelixElement {
160
173
  this.spot = next;
161
174
  }
162
175
  }
176
+ watch(target) {
177
+ const next = target ?? undefined;
178
+ if (next === this.sized || typeof ResizeObserver === 'undefined') {
179
+ return;
180
+ }
181
+ this.sizer ??= new ResizeObserver(() => this.light());
182
+ this.sizer.disconnect();
183
+ if (next) {
184
+ this.sizer.observe(next);
185
+ }
186
+ this.sized = next;
187
+ }
163
188
  render() {
164
189
  const spot = this.spot;
165
190
  return html `${spot === undefined