@swiftwc/ui 0.0.0-dev.97 → 0.0.0-dev.99

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.
@@ -12,7 +12,7 @@
12
12
  * @slot icon — Use the `slot="icon"` attribute to place childen in the icon block.
13
13
  *
14
14
  * @cssprop --label-gap — The gap between the icon and the title.
15
- * @cssprop --label-image-size — The size of the icon.
15
+ * @cssprop {number} --label-image-size — The size of the icon.
16
16
  * @cssprop --label-padding-inline — The padding inline of the label.
17
17
  *
18
18
  */
@@ -15,7 +15,7 @@ import { html } from '../tpl';
15
15
  * @slot icon — Use the `slot="icon"` attribute to place childen in the icon block.
16
16
  *
17
17
  * @cssprop --label-gap — The gap between the icon and the title.
18
- * @cssprop --label-image-size — The size of the icon.
18
+ * @cssprop {number} --label-image-size — The size of the icon.
19
19
  * @cssprop --label-padding-inline — The padding inline of the label.
20
20
  *
21
21
  */
@@ -23,7 +23,7 @@ export type Dictionary = DictEntry[];
23
23
  * @slot — The default slot.
24
24
  * @slot label
25
25
  * @slot list
26
- * @slot validity-options
26
+ * @slot {HTMLOptionElement[]} validity-options
27
27
  */
28
28
  export declare class PickerView extends FormAssociatedBase {
29
29
  #private;
@@ -129,7 +129,7 @@ const collectLeafValues = (node) => (node.children.length ? node.children.flatMa
129
129
  * @slot — The default slot.
130
130
  * @slot label
131
131
  * @slot list
132
- * @slot validity-options
132
+ * @slot {HTMLOptionElement[]} validity-options
133
133
  */
134
134
  export class PickerView extends FormAssociatedBase {
135
135
  static get ATTR() {
@@ -8,7 +8,6 @@ export declare class ProgressView extends HTMLElement {
8
8
  #private;
9
9
  static get observedAttributes(): string[];
10
10
  get progressViewStyle(): "circular" | "linear";
11
- get template(): DocumentFragment;
12
11
  constructor();
13
12
  attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
14
13
  disconnectedCallback(): void;
@@ -25,7 +25,7 @@ export class ProgressView extends HTMLElement {
25
25
  get progressViewStyle() {
26
26
  return this.#progressViewStyle ?? 'circular';
27
27
  }
28
- get template() {
28
+ get #template() {
29
29
  if (!_a.#templates.has(this.progressViewStyle))
30
30
  switch (this.progressViewStyle) {
31
31
  case 'linear':
@@ -109,7 +109,7 @@ export class ProgressView extends HTMLElement {
109
109
  if (this.#lastRenderedStyle === this.#progressViewStyle)
110
110
  return; // skip if already applied
111
111
  this.#lastRenderedStyle = this.#progressViewStyle;
112
- this.#shadowRoot.replaceChildren(document.importNode(this.template, true)); // clear shadow DOM
112
+ this.#shadowRoot.replaceChildren(document.importNode(this.#template, true)); // clear shadow DOM
113
113
  CleanupRegistry.unregister(this, 'slots');
114
114
  for (const slot of this.#shadowRoot.querySelectorAll('slot'))
115
115
  this.#slots?.set(slot.name, slot);
@@ -7,7 +7,7 @@ export type KeyboardType = (typeof keyboardTypes)[number];
7
7
  * @fires {{ detail: { text: string } }} commit — Triggered when user interacts with the input area
8
8
  *
9
9
  * @slot label
10
- * @slot validity-options
10
+ * @slot {HTMLOptionElement[]} validity-options
11
11
  */
12
12
  export declare class TextField extends FormAssociatedBase {
13
13
  #private;
@@ -13,7 +13,7 @@ const keyboardTypes = ['decimal-pad', 'number-pad', 'default'];
13
13
  * @fires {{ detail: { text: string } }} commit — Triggered when user interacts with the input area
14
14
  *
15
15
  * @slot label
16
- * @slot validity-options
16
+ * @slot {HTMLOptionElement[]} validity-options
17
17
  */
18
18
  export class TextField extends FormAssociatedBase {
19
19
  static get observedAttributes() {
@@ -7,7 +7,7 @@ export type ToggleStyle = (typeof toggleStyles)[number];
7
7
  * @fires toggle:change — User toggled the control
8
8
  *
9
9
  * @slot label
10
- * @slot validity-options
10
+ * @slot {HTMLOptionElement[]} validity-options
11
11
  */
12
12
  export declare class ToggleView extends FormAssociatedBase {
13
13
  #private;
@@ -2,7 +2,7 @@ var _a;
2
2
  import { CleanupRegistry } from '../internal/class/cleanup-registry';
3
3
  import { FormAssociatedBase, getInternals } from '../internal/class/form-associated-base';
4
4
  import { MutationObserverSet } from '../internal/class/mutation-observer-set';
5
- import { $, devFlags, kebabCase, onoff } from '../internal/utils';
5
+ import { $, devFlags, kebabCase, onoff, touchGlass } from '../internal/utils';
6
6
  import { queryMorph } from '../morphdom';
7
7
  import { html } from '../tpl';
8
8
  const toggleStyles = ['switch', 'button'];
@@ -12,7 +12,7 @@ const toggleStyles = ['switch', 'button'];
12
12
  * @fires toggle:change — User toggled the control
13
13
  *
14
14
  * @slot label
15
- * @slot validity-options
15
+ * @slot {HTMLOptionElement[]} validity-options
16
16
  */
17
17
  export class ToggleView extends FormAssociatedBase {
18
18
  static get observedAttributes() {
@@ -24,7 +24,6 @@ export class ToggleView extends FormAssociatedBase {
24
24
  * @type {boolean}
25
25
  */
26
26
  'is-on',
27
- 'keyboard-type',
28
27
  /**
29
28
  * @type {boolean}
30
29
  */
@@ -42,8 +41,7 @@ export class ToggleView extends FormAssociatedBase {
42
41
  <slot name="label"></slot>
43
42
  </div>
44
43
  <div part="root toggle-input-stack">
45
- <!--<input type="checkbox" part="root input toggle-form-input">-->
46
- <div part="root toggle-form-input" tabindex="0"></div>
44
+ <div part="root toggle-track" tabindex="0"></div>
47
45
  </div>
48
46
  <slot name="validity-options" hidden></slot>
49
47
  </label>`));
@@ -57,10 +55,30 @@ export class ToggleView extends FormAssociatedBase {
57
55
  #customValidity = '';
58
56
  #slots = new Map();
59
57
  #validityObservers = new MutationObserverSet(this.#renderValidityMsgs);
60
- #input;
58
+ #track;
61
59
  #value = 'on';
62
60
  #isOn = false;
63
- #connected = false;
61
+ #didDrag = false;
62
+ #trackWidth = 0;
63
+ #dotSize = 0;
64
+ get #trackMinX() {
65
+ return 0;
66
+ }
67
+ get #trackMaxX() {
68
+ return this.#trackWidth - this.#dotSize;
69
+ }
70
+ #handleMeasure = ([{ target, borderBoxSize }]) => {
71
+ if (devFlags.debug)
72
+ console.debug(`${_a.name} ⚡️ measure`);
73
+ if (!(target instanceof HTMLElement))
74
+ return;
75
+ this.#trackWidth = borderBoxSize.at(0)?.inlineSize ?? 0;
76
+ this.#dotSize = borderBoxSize.at(0)?.blockSize ?? 0;
77
+ };
78
+ #resizeObserver = new ResizeObserver(this.#handleMeasure);
79
+ get #isDragging() {
80
+ return '0ms' === this.#track?.style.getPropertyValue('--toggle--dot-transition-duration');
81
+ }
64
82
  get #internals() {
65
83
  return getInternals(this);
66
84
  }
@@ -75,14 +93,18 @@ export class ToggleView extends FormAssociatedBase {
75
93
  });
76
94
  CleanupRegistry.unregister(this, 'validities');
77
95
  CleanupRegistry.register(this, onoff('slotchange', this.#handleValiditiesSlotchange, this.#slots?.get('validity-options')).on(), 'validities');
78
- this.#input = this.#shadowRoot.querySelector('[part*=toggle-form-input]') ?? undefined;
79
- this.#input?.addEventListener('click', () => {
80
- this.isOn = !this.isOn;
81
- this.#sendValueToForm(this.#connected);
82
- });
96
+ this.#track = this.#shadowRoot.querySelector('[part*=toggle-track]') ?? undefined;
83
97
  CleanupRegistry.register(this, onoff([{ types: 'focusin', listener: this.#handleFocusin }], this).on());
84
- CleanupRegistry.register(this, onoff([{ types: 'keydown', listener: this.#handleInputKeydown }], this.#input).on());
85
- // CleanupRegistry.register(this, onoff([{ types: 'input', listener: this.#handleInputInput }], this.#input).on())
98
+ CleanupRegistry.register(this, onoff([
99
+ { types: 'keydown', listener: this.#handleTrackKeydown },
100
+ { types: 'click', listener: this.#handleTrackClick },
101
+ { types: 'pointerdown', listener: this.#handleTrackPointerdown },
102
+ { types: 'pointerup', listener: this.#handleTrackPointerup },
103
+ { types: 'pointercancel', listener: this.#handleTrackPointercancel },
104
+ { types: 'pointermove', listener: this.#handleTrackPointermove },
105
+ { types: 'lostpointercapture', listener: this.#handleTrackPointercancel },
106
+ ], this.#track).on());
107
+ CleanupRegistry.register(this, onoff([{ types: 'blur', listener: this.#handleWindowBlur }], self).on());
86
108
  }
87
109
  attributeChangedCallback(name, oldValue, newValue) {
88
110
  if (devFlags.debug)
@@ -90,18 +112,18 @@ export class ToggleView extends FormAssociatedBase {
90
112
  switch (name) {
91
113
  case 'value':
92
114
  this.value = String(newValue); // this.#input?.setAttribute('checked', newValue ?? '')
93
- this.#sendValueToForm(this.#connected);
115
+ this.#sendValueToForm(false);
94
116
  break;
95
117
  case 'is-on':
96
118
  this.isOn = newValue !== null; // this.#input?.setAttribute('checked', newValue ?? '')
97
- this.#sendValueToForm(this.#connected);
119
+ this.#sendValueToForm(false);
98
120
  break;
99
121
  case 'required':
100
122
  // this.#input?.setAttribute(name, newValue ?? '')
101
- this.#sendValueToForm(this.#connected);
123
+ this.#sendValueToForm(false);
102
124
  break;
103
125
  case 'name':
104
- this.#sendValueToForm(this.#connected);
126
+ this.#sendValueToForm(false);
105
127
  break;
106
128
  case 'label':
107
129
  queryMorph('[slot=label]', html `<label-view slot="label">${newValue ? html `<span>${newValue}</span>` : null}</label-view>`, this);
@@ -114,16 +136,18 @@ export class ToggleView extends FormAssociatedBase {
114
136
  }
115
137
  disconnectedCallback() {
116
138
  super.disconnectedCallback();
139
+ if (this.#track)
140
+ this.#resizeObserver.unobserve(this.#track);
117
141
  this.#validityObservers.unobserveAll();
118
142
  }
119
143
  connectedCallback() {
120
144
  super.connectedCallback();
121
- // finally
122
- // if (this.hasAttribute('value')) this.value = this.getAttribute('value') ?? ''
123
145
  if (this.hasAttribute('is-on'))
124
146
  this.isOn = this.hasAttribute('is-on');
125
147
  this.#sendValueToForm(false);
126
- this.#connected = true;
148
+ if (this.#track)
149
+ this.#resizeObserver.observe(this.#track);
150
+ CleanupRegistry.register(this, onoff(touchGlass(this, (t) => t, ({ clientX, clientY }) => this.#measurePointerIsOverDot(clientX, clientY) ?? false), this).on());
127
151
  }
128
152
  get toggleStyle() {
129
153
  return toggleStyles.includes(this.getAttribute('toggle-style') ?? '') ? this.getAttribute('toggle-style') : 'switch';
@@ -149,7 +173,116 @@ export class ToggleView extends FormAssociatedBase {
149
173
  if (devFlags.debug)
150
174
  console.debug(`${_a.name} ⚡️ ${type}`);
151
175
  if (target === this)
152
- this.#input?.focus();
176
+ this.#track?.focus();
177
+ };
178
+ #handleTrackClick = ({ type }) => {
179
+ if (devFlags.debug)
180
+ console.debug(`${_a.name} ⚡️ ${type}`);
181
+ if (this.#didDrag)
182
+ return (this.#didDrag = false);
183
+ this.#applyIsOnWithFlip(!this.isOn);
184
+ this.#sendValueToForm(true);
185
+ };
186
+ #measurePointerIsOverDot = (clientX, clientY) => {
187
+ if (!this.#track)
188
+ return;
189
+ const { left, top, height } = this.#track.getBoundingClientRect();
190
+ const dotLeft = left + parseFloat(self.getComputedStyle(this.#track, '::before').left);
191
+ const dotCenterX = dotLeft + this.#dotSize / 2;
192
+ const dotCenterY = top + height / 2;
193
+ return Math.hypot(clientX - dotCenterX, clientY - dotCenterY) <= this.#dotSize / 2;
194
+ };
195
+ #dragOffsetX = 0;
196
+ get #baseX() {
197
+ return this.isOn ? this.#trackMaxX : this.#trackMinX;
198
+ }
199
+ #handleTrackPointerdown = ({ type, pointerId, clientX, clientY }) => {
200
+ if (devFlags.debug)
201
+ console.debug(`${_a.name} ⚡️ ${type}`);
202
+ if (!this.#track)
203
+ return;
204
+ if (!this.#measurePointerIsOverDot(clientX, clientY))
205
+ return; // ignore taps elsewhere on the track
206
+ this.#didDrag = true; // dot was engaged — settle() will own the outcome, click must be swallowed
207
+ const currentDelta = parseFloat(self.getComputedStyle(this.#track).getPropertyValue('--toggle--dot-x')) || 0;
208
+ const currentAbsoluteX = this.#baseX + currentDelta;
209
+ this.#dragOffsetX = clientX - this.#track.getBoundingClientRect().left - currentAbsoluteX;
210
+ this.#track.style.setProperty('--toggle--dot-transition-duration', '0ms');
211
+ this.#track.setPointerCapture(pointerId);
212
+ // this.#updateFromEvent(clientX)
213
+ };
214
+ #handleTrackPointermove = ({ type, clientX }) => {
215
+ if (devFlags.debug)
216
+ console.debug(`${_a.name} ⚡️ ${type}`);
217
+ if (!this.#isDragging)
218
+ return;
219
+ this.#updateFromEvent(clientX);
220
+ };
221
+ #handleTrackPointerup = ({ type, pointerId }) => {
222
+ if (devFlags.debug)
223
+ console.debug(`${_a.name} ⚡️ ${type}`);
224
+ if (!this.#isDragging)
225
+ return; // pointerdown never engaged (missed the dot) — plain click, nothing to settle
226
+ this.#track?.style.removeProperty('--toggle--dot-transition-duration');
227
+ this.#track?.releasePointerCapture(pointerId);
228
+ this.#settle();
229
+ };
230
+ #handleTrackPointercancel = ({ type }) => {
231
+ if (devFlags.debug)
232
+ console.debug(`${_a.name} ⚡️ ${type}`);
233
+ if (!this.#isDragging)
234
+ return; // already settled via pointerup, or nothing to settle
235
+ this.#track?.style.removeProperty('--toggle--dot-transition-duration');
236
+ this.#settle();
237
+ };
238
+ #handleWindowBlur = () => {
239
+ if (!this.#isDragging)
240
+ return;
241
+ this.#track?.style.removeProperty('--toggle--dot-transition-duration');
242
+ this.#settle();
243
+ };
244
+ #updateFromEvent = (clientX) => {
245
+ if (!this.#track)
246
+ return;
247
+ const { left } = this.#track.getBoundingClientRect();
248
+ const absoluteX = Math.min(Math.max(clientX - left - this.#dragOffsetX, this.#trackMinX), this.#trackMaxX);
249
+ this.#track.style.setProperty('--toggle--dot-x', `${Math.round(absoluteX - this.#baseX)}px`);
250
+ };
251
+ #settle = () => {
252
+ if (!this.#track)
253
+ return;
254
+ const deltaX = parseFloat(self.getComputedStyle(this.#track).getPropertyValue('--toggle--dot-x')) || 0;
255
+ const absoluteX = this.#baseX + deltaX;
256
+ const nextIsOn = absoluteX > this.#trackMaxX / 2;
257
+ if (nextIsOn === this.isOn)
258
+ return this.#track.style.removeProperty('--toggle--dot-x'); // pure transform animation back to rest, inset untouched
259
+ this.#applyIsOnWithFlip(nextIsOn);
260
+ this.#sendValueToForm(true);
261
+ };
262
+ #handleTrackKeydown = (evt) => {
263
+ if (devFlags.debug)
264
+ console.debug(`${_a.name} ⚡️ ${evt?.type}`);
265
+ if (' ' !== evt.key)
266
+ return;
267
+ evt.preventDefault();
268
+ this.#applyIsOnWithFlip(!this.isOn);
269
+ this.#sendValueToForm(true);
270
+ };
271
+ #applyIsOnWithFlip = (nextIsOn) => {
272
+ if (!this.#track || nextIsOn === this.isOn)
273
+ return;
274
+ // capture current visual position under the OLD base, before flipping state
275
+ const deltaX = parseFloat(self.getComputedStyle(this.#track).getPropertyValue('--toggle--dot-x')) || 0, oldAbsoluteX = this.#baseX + deltaX;
276
+ this.isOn = nextIsOn; // changes --toggle--dot-inset-inline-start/-end via aria-checked
277
+ const compensationX = oldAbsoluteX - this.#baseX; // baseX now reflects the NEW state
278
+ // make inset + transform jump together, instantly, so nothing is visibly out of sync
279
+ this.#track.style.setProperty('--toggle--dot-transition-duration', '0ms');
280
+ this.#track.style.setProperty('--toggle--dot-x', `${Math.round(compensationX)}px`);
281
+ void this.#track.offsetWidth; // force reflow so the instant jump is committed before re-enabling transitions
282
+ this.#track.style.removeProperty('--toggle--dot-transition-duration');
283
+ self.requestAnimationFrame(() => {
284
+ this.#track?.style.removeProperty('--toggle--dot-x'); // only transform animates from here — single source of motion
285
+ });
153
286
  };
154
287
  #sendValueToForm = (dispatchEvent = true) => {
155
288
  // input.value has already been updated/synced !!
@@ -158,9 +291,8 @@ export class ToggleView extends FormAssociatedBase {
158
291
  if (this.hasAttribute('required') && !this.isOn) {
159
292
  this.setValidity({ valueMissing: true });
160
293
  }
161
- else {
294
+ else
162
295
  this.setValidity({});
163
- }
164
296
  const entries = new FormData();
165
297
  if (this.value)
166
298
  entries.append(this.name, this.value);
@@ -168,17 +300,6 @@ export class ToggleView extends FormAssociatedBase {
168
300
  if (dispatchEvent)
169
301
  this.dispatchEvent(new CustomEvent('toggle:change', { detail: { value: this.value, isOn: this.isOn }, bubbles: true, composed: true }));
170
302
  };
171
- #handleInputKeydown = (evt) => {
172
- if (devFlags.debug)
173
- console.debug(`${_a.name} ⚡️ ${evt?.type}`);
174
- this.isOn = !this.isOn;
175
- this.#sendValueToForm();
176
- };
177
- #handleInputInput = (evt) => {
178
- if (devFlags.debug)
179
- console.debug(`${_a.name} ⚡️ ${evt?.type}`);
180
- this.#sendValueToForm();
181
- };
182
303
  /**
183
304
  * Form participation property
184
305
  */
@@ -222,7 +343,7 @@ export class ToggleView extends FormAssociatedBase {
222
343
  }
223
344
  if (devFlags.debug)
224
345
  console.debug(`${_a.name} ⚡️ validity-change`);
225
- return this.#internals.setValidity(flags, this.#customValidity || message, anchor ?? this.#input);
346
+ return this.#internals.setValidity(flags, this.#customValidity || message, anchor ?? this.#track);
226
347
  };
227
348
  setCustomValidity = (message) => {
228
349
  this.#customValidity = message;
@@ -31,11 +31,11 @@ let ToolBarItem = ToolBarItem_1 = class ToolBarItem extends HTMLElement {
31
31
  const role = this.getAttribute('data-previous-slot') ?? this.getAttribute('slot') ?? '', tKey = this.getAttribute('title-key');
32
32
  if (['confirmation-action', 'cancellation-action'].includes(role))
33
33
  Snapshot.waitReady.then(() => {
34
- ensurePlaceholder(this.querySelector(':scope>button'), role, tKey, Snapshot.config);
34
+ ensurePlaceholder(this.querySelector(':scope>button:not(:has(> :not([slot])))'), role, tKey, Snapshot.config);
35
35
  this.#mutationObserver = new MutationObserver((mutations) => {
36
36
  for (const { target } of mutations)
37
37
  if (target instanceof HTMLElement)
38
- ensurePlaceholder(this.querySelector(':scope>button'), role, tKey, Snapshot.config);
38
+ ensurePlaceholder(this.querySelector(':scope>button:not(:has(> :not([slot])))'), role, tKey, Snapshot.config);
39
39
  });
40
40
  this.#mutationObserver.observe(this, {
41
41
  childList: true,
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @summary A view that arranges its children one on top of the other.
3
3
  *
4
+ * @attr {@FrameInlineLength} readable — Shortcut for `placement="leading fill" frame:alignment="center" frame:width="infinity" frame:max-width="{length}"`
4
5
  * @attr {leading|"leading fill"|center|trailing|fill|space-between} distribution — The distribution of cols
5
6
  *
6
7
  * @attr {@Template} template — The main-axis grid template
@@ -2,6 +2,7 @@ import { devFlags } from '../internal/utils';
2
2
  /**
3
3
  * @summary A view that arranges its children one on top of the other.
4
4
  *
5
+ * @attr {@FrameInlineLength} readable — Shortcut for `placement="leading fill" frame:alignment="center" frame:width="infinity" frame:max-width="{length}"`
5
6
  * @attr {leading|"leading fill"|center|trailing|fill|space-between} distribution — The distribution of cols
6
7
  *
7
8
  * @attr {@Template} template — The main-axis grid template