@ixfx/components 0.14.7 → 0.15.1
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/bundle/index.d.ts +102 -3
- package/bundle/index.js +574 -247
- package/bundle/style.css +2 -82
- package/dist/{button-BYuf72aR.d.ts → button-N92DKoWb.d.ts} +7 -0
- package/dist/{button-B7mgqtxR.js → button-ikByJJPN.js} +29 -8
- package/dist/button.d.ts +1 -1
- package/dist/button.js +1 -1
- package/dist/{colour-picker-BCMU7W7R.js → colour-picker-DwAa6RDh.js} +73 -9
- package/dist/colour-picker.d.ts +1 -1
- package/dist/colour-picker.js +1 -1
- package/dist/form-BGZK3jZd.d.ts +54 -0
- package/dist/form-CY0nibZ5.js +418 -0
- package/dist/form.d.ts +1 -13
- package/dist/form.js +1 -1
- package/dist/{index-tq6f8007.d.ts → index-DN2BWctP.d.ts} +3 -0
- package/dist/index.d.ts +54 -6
- package/dist/index.js +138 -8
- package/dist/style.css +2 -82
- package/dist/{tab-list-CexFFg6O.d.ts → tab-list-Df371rqw.d.ts} +1 -0
- package/dist/tab-list-R_bGbDvO.js +766 -0
- package/dist/tabs.d.ts +1 -1
- package/dist/tabs.js +1 -673
- package/docs-user/button.md +1 -0
- package/docs-user/colour-picker.md +19 -0
- package/docs-user/form.md +97 -4
- package/docs-user/index.json +2 -2
- package/docs-user/multicol-list.md +1 -1
- package/docs-user/tabs.md +30 -0
- package/package.json +2 -1
- package/dist/form-CsXJX15p.js +0 -62
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
import { t as themeFallbacks } from "./fallbacks-DQ_5OsmR.js";
|
|
2
|
+
import { n as safeCustomElement, t as __decorate } from "./decorate-VRK8rslU.js";
|
|
3
|
+
import { t as scrollFadeStyles } from "./scroll-fade-Xx28D7mH.js";
|
|
4
|
+
import { t as createDragGhost } from "./drag-ghost-DtSDvjri.js";
|
|
5
|
+
import { LitElement, css, html, nothing } from "lit";
|
|
6
|
+
import { property } from "lit/decorators.js";
|
|
7
|
+
//#region src/tabs/indicator-transition.ts
|
|
8
|
+
/**
|
|
9
|
+
* Smoothstep, 3t² − 2t³, expressed exactly as a cubic Bézier: control points
|
|
10
|
+
* at x = 1/3 and 2/3 make x linear in the curve parameter, leaving y as the
|
|
11
|
+
* smoothstep polynomial.
|
|
12
|
+
*/
|
|
13
|
+
const SMOOTHSTEP = `cubic-bezier(0.3333, 0, 0.6667, 1)`;
|
|
14
|
+
let counter = 0;
|
|
15
|
+
/** A document-unique `view-transition-name` for one tab list's indicator. */
|
|
16
|
+
function nextIndicatorName() {
|
|
17
|
+
counter += 1;
|
|
18
|
+
return `ixfx-tab-indicator-${counter}`;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Read the transition settings from an element's computed style. Returns
|
|
22
|
+
* undefined when transitions are off (`--ixfx-tab-transition-duration` unset
|
|
23
|
+
* or zero), unsupported by the browser, or the user prefers reduced motion.
|
|
24
|
+
*/
|
|
25
|
+
function readTiming(el) {
|
|
26
|
+
if (!(`startViewTransition` in document)) return void 0;
|
|
27
|
+
if (globalThis.matchMedia?.(`(prefers-reduced-motion: reduce)`).matches) return void 0;
|
|
28
|
+
const style = getComputedStyle(el);
|
|
29
|
+
const duration = style.getPropertyValue(`--ixfx-tab-transition-duration`).trim();
|
|
30
|
+
if (!(Number.parseFloat(duration) > 0)) return void 0;
|
|
31
|
+
return {
|
|
32
|
+
duration,
|
|
33
|
+
easing: style.getPropertyValue(`--ixfx-tab-transition-easing`).trim() || SMOOTHSTEP
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const baseRule = `ixfx-tab-list-item[selected]::after { view-transition-name: var(--ixfx-tab-indicator-name, none); }`;
|
|
37
|
+
const rules = /* @__PURE__ */ new Map();
|
|
38
|
+
let sheet;
|
|
39
|
+
function writeSheet() {
|
|
40
|
+
if (sheet === void 0) {
|
|
41
|
+
sheet = new CSSStyleSheet();
|
|
42
|
+
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
|
|
43
|
+
}
|
|
44
|
+
sheet.replaceSync([baseRule, ...rules.values()].join(`\n`));
|
|
45
|
+
}
|
|
46
|
+
/** Install the document-level indicator rule. Safe to call repeatedly. */
|
|
47
|
+
function installIndicatorStyles() {
|
|
48
|
+
if (sheet === void 0) writeSheet();
|
|
49
|
+
}
|
|
50
|
+
function applyTiming(name, timing) {
|
|
51
|
+
rules.set(name, `::view-transition-group(${name}) { animation-duration: ${timing.duration}; animation-timing-function: ${timing.easing}; }`);
|
|
52
|
+
writeSheet();
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Run `update` (which must change the DOM synchronously) inside a view
|
|
56
|
+
* transition. The update itself is deferred by the browser until the old
|
|
57
|
+
* state has been captured.
|
|
58
|
+
*/
|
|
59
|
+
function runTransition(name, timing, update) {
|
|
60
|
+
applyTiming(name, timing);
|
|
61
|
+
return document.startViewTransition(update);
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/tabs/tab-list.ts
|
|
65
|
+
/** Pointer movement threshold (px) before a press becomes a drag. */
|
|
66
|
+
const DRAG_THRESHOLD_PX = 5;
|
|
67
|
+
let TabListElement = class TabListElement extends LitElement {
|
|
68
|
+
constructor(..._args) {
|
|
69
|
+
super(..._args);
|
|
70
|
+
this.orientation = `horizontal`;
|
|
71
|
+
this.flip = false;
|
|
72
|
+
this.scrollable = false;
|
|
73
|
+
this.hideLabels = false;
|
|
74
|
+
this.labelNogrow = false;
|
|
75
|
+
this.shrink = false;
|
|
76
|
+
this.dragMode = `off`;
|
|
77
|
+
this.#items = [];
|
|
78
|
+
this.#indicatorName = nextIndicatorName();
|
|
79
|
+
this.#dragState = null;
|
|
80
|
+
this.#suppressNextClick = false;
|
|
81
|
+
this.#onSlotChange = () => {
|
|
82
|
+
this.updateComplete.then(() => {
|
|
83
|
+
this.#refreshItems();
|
|
84
|
+
this.#syncIndicatorColors();
|
|
85
|
+
this.#syncHideLabels();
|
|
86
|
+
this.#syncLabelNogrow();
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
this.#onToolbarSlotChange = () => {
|
|
90
|
+
const assigned = (this.shadowRoot?.querySelector(`slot[name="toolbar"]`))?.assignedElements() ?? [];
|
|
91
|
+
const has = assigned.length > 0;
|
|
92
|
+
this.toggleAttribute(`has-toolbar`, has);
|
|
93
|
+
const toolbarDiv = this.shadowRoot?.querySelector(`.list-toolbar`);
|
|
94
|
+
if (toolbarDiv) toolbarDiv.hidden = !has;
|
|
95
|
+
if (has) {
|
|
96
|
+
const toolbar = assigned[0];
|
|
97
|
+
if (toolbar?.tagName === `IXFX-TOOLBAR`) requestAnimationFrame(() => {
|
|
98
|
+
toolbar?._scheduleMeasure?.();
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
this.#onKeyUp = (event) => {
|
|
103
|
+
const key = event.key;
|
|
104
|
+
if (this.#isVertical) {
|
|
105
|
+
if (key === `ArrowUp`) this.#selectPrevious();
|
|
106
|
+
else if (key === `ArrowDown`) this.#selectNext();
|
|
107
|
+
} else if (key === `ArrowRight`) this.#selectNext();
|
|
108
|
+
else if (key === `ArrowLeft`) this.#selectPrevious();
|
|
109
|
+
};
|
|
110
|
+
this.#onKeyDown = (event) => {
|
|
111
|
+
const state = this.#dragState;
|
|
112
|
+
if (event.key !== `Escape` || !state?.active) return;
|
|
113
|
+
event.preventDefault();
|
|
114
|
+
const pointerId = state.pointerId;
|
|
115
|
+
this.#abortDrag();
|
|
116
|
+
this.#endDrag(pointerId);
|
|
117
|
+
};
|
|
118
|
+
this.#onWheel = (event) => {
|
|
119
|
+
if (!this.scrollable) return;
|
|
120
|
+
const el = this.#containerEl;
|
|
121
|
+
if (!el) return;
|
|
122
|
+
if (!this.#isVertical) {
|
|
123
|
+
if (event.deltaY === 0 || el.scrollWidth <= el.clientWidth) return;
|
|
124
|
+
el.scrollLeft = el.scrollLeft + event.deltaY;
|
|
125
|
+
event.preventDefault();
|
|
126
|
+
} else {
|
|
127
|
+
if (event.deltaX === 0 || el.scrollHeight <= el.clientHeight) return;
|
|
128
|
+
el.scrollTop = el.scrollTop + event.deltaX;
|
|
129
|
+
event.preventDefault();
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
this.#onSlotClick = (event) => {
|
|
133
|
+
if (this.#suppressNextClick) {
|
|
134
|
+
this.#suppressNextClick = false;
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
event.stopPropagation();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
this.focus();
|
|
140
|
+
const t = event.target.closest(`ixfx-tab-list-item`);
|
|
141
|
+
if (!t) return;
|
|
142
|
+
const previous = this.getSelectedElement();
|
|
143
|
+
const forId = t.getAttribute(`for`);
|
|
144
|
+
const previousId = previous?.id;
|
|
145
|
+
const apply = () => {
|
|
146
|
+
this.selectElement(t);
|
|
147
|
+
this.dispatchEvent(new CustomEvent(`change`, {
|
|
148
|
+
bubbles: true,
|
|
149
|
+
detail: {
|
|
150
|
+
previous: previousId,
|
|
151
|
+
selected: t.id,
|
|
152
|
+
for: forId ?? ``
|
|
153
|
+
}
|
|
154
|
+
}));
|
|
155
|
+
};
|
|
156
|
+
if (previous === t) apply();
|
|
157
|
+
else this.#transition(apply);
|
|
158
|
+
};
|
|
159
|
+
this.#onPointerDown = (event) => {
|
|
160
|
+
if (this.dragMode !== `reorder`) return;
|
|
161
|
+
if (event.button !== 0) return;
|
|
162
|
+
const target = this.#findDragTarget(event);
|
|
163
|
+
if (!target) return;
|
|
164
|
+
const fromIndex = this.#items.indexOf(target);
|
|
165
|
+
if (fromIndex < 0) return;
|
|
166
|
+
this.#dragState = {
|
|
167
|
+
pointerId: event.pointerId,
|
|
168
|
+
source: target,
|
|
169
|
+
fromIndex,
|
|
170
|
+
startX: event.clientX,
|
|
171
|
+
startY: event.clientY,
|
|
172
|
+
active: false,
|
|
173
|
+
ghost: null,
|
|
174
|
+
gapIndex: void 0
|
|
175
|
+
};
|
|
176
|
+
window.addEventListener(`pointermove`, this.#onPointerMove);
|
|
177
|
+
window.addEventListener(`pointerup`, this.#onPointerUp);
|
|
178
|
+
window.addEventListener(`pointercancel`, this.#onPointerCancel);
|
|
179
|
+
};
|
|
180
|
+
this.#pendingMove = null;
|
|
181
|
+
this.#moveRaf = null;
|
|
182
|
+
this.#onPointerMove = (event) => {
|
|
183
|
+
const state = this.#dragState;
|
|
184
|
+
if (!state || event.pointerId !== state.pointerId) return;
|
|
185
|
+
this.#pendingMove = event;
|
|
186
|
+
this.#moveRaf ??= requestAnimationFrame(this.#flushMove);
|
|
187
|
+
};
|
|
188
|
+
this.#flushMove = () => {
|
|
189
|
+
const event = this.#pendingMove;
|
|
190
|
+
this.#pendingMove = null;
|
|
191
|
+
this.#moveRaf = null;
|
|
192
|
+
const state = this.#dragState;
|
|
193
|
+
if (!state || !event) return;
|
|
194
|
+
const dx = event.clientX - state.startX;
|
|
195
|
+
const dy = event.clientY - state.startY;
|
|
196
|
+
const distance = Math.hypot(dx, dy);
|
|
197
|
+
if (!state.active) {
|
|
198
|
+
if (distance < DRAG_THRESHOLD_PX) return;
|
|
199
|
+
this.#beginDrag(event);
|
|
200
|
+
}
|
|
201
|
+
state.ghost?.move(event.clientX, event.clientY);
|
|
202
|
+
const gap = this.#computeGapIndex(event.clientX, event.clientY);
|
|
203
|
+
if (gap !== state.gapIndex) {
|
|
204
|
+
state.gapIndex = gap;
|
|
205
|
+
this.requestUpdate();
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
this.#onPointerUp = (event) => {
|
|
209
|
+
const state = this.#dragState;
|
|
210
|
+
if (!state || event.pointerId !== state.pointerId) return;
|
|
211
|
+
if (state.active) {
|
|
212
|
+
const gap = this.#computeGapIndex(event.clientX, event.clientY);
|
|
213
|
+
this.#commitDrag(state.source, state.fromIndex, gap);
|
|
214
|
+
}
|
|
215
|
+
this.#endDrag(event.pointerId);
|
|
216
|
+
};
|
|
217
|
+
this.#onPointerCancel = (event) => {
|
|
218
|
+
const state = this.#dragState;
|
|
219
|
+
if (!state || event.pointerId !== state.pointerId) return;
|
|
220
|
+
this.#abortDrag();
|
|
221
|
+
this.#endDrag(event.pointerId);
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
#items;
|
|
225
|
+
#indicatorName;
|
|
226
|
+
#dragState;
|
|
227
|
+
#suppressNextClick;
|
|
228
|
+
get #isVertical() {
|
|
229
|
+
return this.orientation === `vertical-stack` || this.orientation === `vertical-side`;
|
|
230
|
+
}
|
|
231
|
+
get #containerClass() {
|
|
232
|
+
return [
|
|
233
|
+
this.scrollable ? `${this.orientation} scrollable` : this.orientation,
|
|
234
|
+
this.scrollable ? this.#isVertical ? `scroll-fade-y` : `scroll-fade-x` : ``,
|
|
235
|
+
this.dragMode === `reorder` ? `reorder` : ``
|
|
236
|
+
].filter(Boolean).join(` `);
|
|
237
|
+
}
|
|
238
|
+
render() {
|
|
239
|
+
const caretStyle = this.#computeCaretStyle();
|
|
240
|
+
return html`
|
|
241
|
+
<div class="outer ${this.orientation}">
|
|
242
|
+
<div
|
|
243
|
+
tabindex=0
|
|
244
|
+
@keyup=${this.#onKeyUp}
|
|
245
|
+
@click=${this.#onSlotClick}
|
|
246
|
+
@pointerdown=${this.#onPointerDown}
|
|
247
|
+
@keydown=${this.#onKeyDown}
|
|
248
|
+
@wheel=${this.#onWheel}
|
|
249
|
+
id="container"
|
|
250
|
+
class=${this.#containerClass}
|
|
251
|
+
>
|
|
252
|
+
<slot @slotchange=${this.#onSlotChange}></slot>
|
|
253
|
+
${caretStyle ? html`<div class="drop-caret" style=${caretStyle}></div>` : nothing}
|
|
254
|
+
</div>
|
|
255
|
+
<div class="list-toolbar ${this.orientation}" part="list-toolbar">
|
|
256
|
+
<slot name="toolbar" @slotchange=${this.#onToolbarSlotChange}></slot>
|
|
257
|
+
</div>
|
|
258
|
+
</div>`;
|
|
259
|
+
}
|
|
260
|
+
#onSlotChange;
|
|
261
|
+
/**
|
|
262
|
+
* Re-read the assigned tab items. Some environments do not fire slotchange
|
|
263
|
+
* for a slot's initial assignment, so firstUpdated also calls this before
|
|
264
|
+
* syncing item attributes.
|
|
265
|
+
*/
|
|
266
|
+
#refreshItems() {
|
|
267
|
+
this.#items = (this.shadowRoot?.querySelector(`slot:not([name])`))?.assignedElements({ flatten: true }) ?? [];
|
|
268
|
+
}
|
|
269
|
+
#onToolbarSlotChange;
|
|
270
|
+
firstUpdated() {
|
|
271
|
+
this.updateComplete.then(() => {
|
|
272
|
+
this.#refreshItems();
|
|
273
|
+
this.#syncIndicatorColors();
|
|
274
|
+
this.#syncHideLabels();
|
|
275
|
+
this.#syncLabelNogrow();
|
|
276
|
+
this.#onToolbarSlotChange();
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
updated(changedProperties) {
|
|
280
|
+
super.updated(changedProperties);
|
|
281
|
+
this.classList.toggle(`scrollable`, this.scrollable);
|
|
282
|
+
if (changedProperties.has(`dragMode`)) this.toggleAttribute(`drag-mode`, this.dragMode !== `off`);
|
|
283
|
+
if (changedProperties.has(`hideLabels`)) this.#syncHideLabels();
|
|
284
|
+
if (changedProperties.has(`labelNogrow`)) this.#syncLabelNogrow();
|
|
285
|
+
if (changedProperties.has(`orientation`) || changedProperties.has(`flip`)) for (const item of this.#items) item.setAttribute(`indicator-edge`, this.#indicatorEdge);
|
|
286
|
+
}
|
|
287
|
+
connectedCallback() {
|
|
288
|
+
super.connectedCallback();
|
|
289
|
+
installIndicatorStyles();
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Run a selection change inside a view transition when the page opted in
|
|
293
|
+
* via `--ixfx-tab-transition-duration`. The browser defers `update`, so only
|
|
294
|
+
* user-initiated changes (click, arrow keys) use this; the public
|
|
295
|
+
* `selectElement` stays synchronous.
|
|
296
|
+
*/
|
|
297
|
+
#transition(update) {
|
|
298
|
+
const timing = readTiming(this);
|
|
299
|
+
if (timing === void 0) {
|
|
300
|
+
update();
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
this.style.setProperty(`--ixfx-tab-indicator-name`, this.#indicatorName);
|
|
304
|
+
const transition = runTransition(this.#indicatorName, timing, update);
|
|
305
|
+
const clear = () => this.style.removeProperty(`--ixfx-tab-indicator-name`);
|
|
306
|
+
transition.finished.then(clear, clear);
|
|
307
|
+
}
|
|
308
|
+
#syncHideLabels() {
|
|
309
|
+
for (const item of this.#items) if (this.hideLabels) item.setAttribute(`hide-labels`, ``);
|
|
310
|
+
else item.removeAttribute(`hide-labels`);
|
|
311
|
+
}
|
|
312
|
+
#syncLabelNogrow() {
|
|
313
|
+
for (const item of this.#items) if (this.labelNogrow) item.setAttribute(`label-nogrow`, ``);
|
|
314
|
+
else item.removeAttribute(`label-nogrow`);
|
|
315
|
+
}
|
|
316
|
+
get #indicatorEdge() {
|
|
317
|
+
if (this.#isVertical) return this.flip ? `left` : `right`;
|
|
318
|
+
return this.flip ? `top` : `bottom`;
|
|
319
|
+
}
|
|
320
|
+
#syncIndicatorColors() {
|
|
321
|
+
for (const item of this.#items) {
|
|
322
|
+
item.setAttribute(`indicator-edge`, this.#indicatorEdge);
|
|
323
|
+
item.style.setProperty(`--indicator-color-bottom`, `transparent`);
|
|
324
|
+
item.style.setProperty(`--indicator-color-top`, `transparent`);
|
|
325
|
+
item.style.setProperty(`--indicator-color-left`, `transparent`);
|
|
326
|
+
item.style.setProperty(`--indicator-color-right`, `transparent`);
|
|
327
|
+
if (item.hasAttribute(`selected`)) {
|
|
328
|
+
if (this.orientation === `horizontal` && !this.flip) item.style.setProperty(`--indicator-color-bottom`, `var(--accent)`);
|
|
329
|
+
else if (this.orientation === `horizontal` && this.flip) item.style.setProperty(`--indicator-color-top`, `var(--accent)`);
|
|
330
|
+
else if (this.#isVertical && !this.flip) item.style.setProperty(`--indicator-color-right`, `var(--accent)`);
|
|
331
|
+
else if (this.#isVertical && this.flip) item.style.setProperty(`--indicator-color-left`, `var(--accent)`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
#onKeyUp;
|
|
336
|
+
#onKeyDown;
|
|
337
|
+
/**
|
|
338
|
+
* Translate vertical wheel deltas into horizontal scroll on a scrollable
|
|
339
|
+
* horizontal list (and vice-versa for vertical lists). Without this, the
|
|
340
|
+
* default browser behaviour only scrolls horizontally with Shift+wheel or a
|
|
341
|
+
* physical horizontal wheel — neither of which most users reach for on a
|
|
342
|
+
* VS Code-style tab strip.
|
|
343
|
+
*
|
|
344
|
+
* Only kicks in when the list is actually scrollable (overflow content
|
|
345
|
+
* present), and is a no-op when nothing is scrollable in that direction.
|
|
346
|
+
*/
|
|
347
|
+
#onWheel;
|
|
348
|
+
getSelectedElement() {
|
|
349
|
+
for (const el of this.#items) if (this.#isSelected(el)) return el;
|
|
350
|
+
const slot = this.shadowRoot?.querySelector(`slot`);
|
|
351
|
+
if (slot) {
|
|
352
|
+
const els = slot.assignedElements({ flatten: true });
|
|
353
|
+
for (const el of els) if (this.#isSelected(el)) return el;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
#isSelected(el) {
|
|
357
|
+
if (el.getAttribute(`selected`) === null) return false;
|
|
358
|
+
return el.hasAttribute(`selected`);
|
|
359
|
+
}
|
|
360
|
+
selectElement(el) {
|
|
361
|
+
const existing = this.getSelectedElement();
|
|
362
|
+
if (existing) {
|
|
363
|
+
existing.removeAttribute(`selected`);
|
|
364
|
+
existing.style.setProperty(`--indicator-color-bottom`, `transparent`);
|
|
365
|
+
existing.style.setProperty(`--indicator-color-top`, `transparent`);
|
|
366
|
+
existing.style.setProperty(`--indicator-color-left`, `transparent`);
|
|
367
|
+
existing.style.setProperty(`--indicator-color-right`, `transparent`);
|
|
368
|
+
}
|
|
369
|
+
el.setAttribute(`selected`, ``);
|
|
370
|
+
if (this.orientation === `horizontal` && !this.flip) el.style.setProperty(`--indicator-color-bottom`, `var(--accent)`);
|
|
371
|
+
else if (this.orientation === `horizontal` && this.flip) el.style.setProperty(`--indicator-color-top`, `var(--accent)`);
|
|
372
|
+
else if (this.#isVertical && !this.flip) el.style.setProperty(`--indicator-color-right`, `var(--accent)`);
|
|
373
|
+
else if (this.#isVertical && this.flip) el.style.setProperty(`--indicator-color-left`, `var(--accent)`);
|
|
374
|
+
}
|
|
375
|
+
#selectNext() {
|
|
376
|
+
let found = false;
|
|
377
|
+
for (const el of this.#items) if (this.#isSelected(el) && !found) found = true;
|
|
378
|
+
else if (found) {
|
|
379
|
+
this.#transition(() => this.selectElement(el));
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
#selectPrevious() {
|
|
384
|
+
let prev;
|
|
385
|
+
for (const el of this.#items) if (this.#isSelected(el) && prev) {
|
|
386
|
+
const target = prev;
|
|
387
|
+
this.#transition(() => this.selectElement(target));
|
|
388
|
+
} else prev = el;
|
|
389
|
+
}
|
|
390
|
+
#onSlotClick;
|
|
391
|
+
/**
|
|
392
|
+
* Walk composedPath looking for the nearest ancestor tab-list-item, but bail
|
|
393
|
+
* if the path goes through a `.close-btn` or `.toolbar` (so close/toolbar
|
|
394
|
+
* buttons always win — same delegation pattern used by tooltip-controller).
|
|
395
|
+
*/
|
|
396
|
+
#findDragTarget(event) {
|
|
397
|
+
if (this.dragMode === `off`) return null;
|
|
398
|
+
const path = event.composedPath();
|
|
399
|
+
let item = null;
|
|
400
|
+
for (const node of path) {
|
|
401
|
+
if (!(node instanceof HTMLElement)) continue;
|
|
402
|
+
if (node.classList.contains(`close-btn`) || node.classList.contains(`toolbar`)) return null;
|
|
403
|
+
if (!item && node.tagName === `IXFX-TAB-LIST-ITEM`) item = node;
|
|
404
|
+
}
|
|
405
|
+
return item;
|
|
406
|
+
}
|
|
407
|
+
#onPointerDown;
|
|
408
|
+
#pendingMove;
|
|
409
|
+
#moveRaf;
|
|
410
|
+
#onPointerMove;
|
|
411
|
+
#cancelPendingMove() {
|
|
412
|
+
if (this.#moveRaf !== null) {
|
|
413
|
+
cancelAnimationFrame(this.#moveRaf);
|
|
414
|
+
this.#moveRaf = null;
|
|
415
|
+
}
|
|
416
|
+
this.#pendingMove = null;
|
|
417
|
+
}
|
|
418
|
+
#flushMove;
|
|
419
|
+
#onPointerUp;
|
|
420
|
+
#onPointerCancel;
|
|
421
|
+
#beginDrag(event) {
|
|
422
|
+
const state = this.#dragState;
|
|
423
|
+
if (!state || state.active) return;
|
|
424
|
+
try {
|
|
425
|
+
this.#containerEl?.setPointerCapture(event.pointerId);
|
|
426
|
+
} catch {}
|
|
427
|
+
state.active = true;
|
|
428
|
+
state.source.setAttribute(`drag-source`, ``);
|
|
429
|
+
state.ghost = this.#createGhost(state.source);
|
|
430
|
+
if (state.ghost) state.ghost.move(event.clientX, event.clientY);
|
|
431
|
+
}
|
|
432
|
+
#abortDrag() {
|
|
433
|
+
const state = this.#dragState;
|
|
434
|
+
if (!state) return;
|
|
435
|
+
state.source.removeAttribute(`drag-source`);
|
|
436
|
+
if (state.gapIndex !== void 0) {
|
|
437
|
+
state.gapIndex = void 0;
|
|
438
|
+
this.requestUpdate();
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
#endDrag(pointerId) {
|
|
442
|
+
const state = this.#dragState;
|
|
443
|
+
if (!state) return;
|
|
444
|
+
this.#cancelPendingMove();
|
|
445
|
+
state.ghost?.destroy();
|
|
446
|
+
state.ghost = null;
|
|
447
|
+
this.#dragState = null;
|
|
448
|
+
window.removeEventListener(`pointermove`, this.#onPointerMove);
|
|
449
|
+
window.removeEventListener(`pointerup`, this.#onPointerUp);
|
|
450
|
+
window.removeEventListener(`pointercancel`, this.#onPointerCancel);
|
|
451
|
+
try {
|
|
452
|
+
this.#containerEl?.releasePointerCapture(pointerId);
|
|
453
|
+
} catch {}
|
|
454
|
+
this.requestUpdate();
|
|
455
|
+
}
|
|
456
|
+
#commitDrag(source, fromIndex, gapIndex) {
|
|
457
|
+
source.removeAttribute(`drag-source`);
|
|
458
|
+
const siblings = this.#items.filter((el) => el !== source);
|
|
459
|
+
const target = gapIndex === void 0 ? siblings.length : Math.max(0, Math.min(gapIndex, siblings.length));
|
|
460
|
+
const parent = source.parentNode;
|
|
461
|
+
if (!parent) return;
|
|
462
|
+
const ref = siblings[target] ?? null;
|
|
463
|
+
parent.insertBefore(source, ref);
|
|
464
|
+
const toIndex = Array.prototype.indexOf.call(parent.children, source);
|
|
465
|
+
if (toIndex === fromIndex) return;
|
|
466
|
+
this.#items = (this.shadowRoot?.querySelector(`slot`))?.assignedElements({ flatten: true }) ?? [];
|
|
467
|
+
this.#syncIndicatorColors();
|
|
468
|
+
this.#suppressNextClick = true;
|
|
469
|
+
this.dispatchEvent(new CustomEvent(`reorder`, {
|
|
470
|
+
bubbles: true,
|
|
471
|
+
detail: {
|
|
472
|
+
id: source.id,
|
|
473
|
+
fromIndex,
|
|
474
|
+
toIndex
|
|
475
|
+
}
|
|
476
|
+
}));
|
|
477
|
+
}
|
|
478
|
+
#createGhost(source) {
|
|
479
|
+
const content = document.createElement(`span`);
|
|
480
|
+
content.style.display = `inline-flex`;
|
|
481
|
+
content.style.alignItems = `center`;
|
|
482
|
+
content.style.gap = `var(--space-xs, 4px)`;
|
|
483
|
+
content.style.minWidth = `0`;
|
|
484
|
+
const iconName = source.iconName;
|
|
485
|
+
if (iconName) {
|
|
486
|
+
const icon = document.createElement(`ixfx-icon`);
|
|
487
|
+
icon.name = iconName;
|
|
488
|
+
content.appendChild(icon);
|
|
489
|
+
}
|
|
490
|
+
const label = document.createElement(`span`);
|
|
491
|
+
label.textContent = (source.textContent ?? ``).trim();
|
|
492
|
+
label.style.overflow = `hidden`;
|
|
493
|
+
label.style.textOverflow = `ellipsis`;
|
|
494
|
+
label.style.whiteSpace = `nowrap`;
|
|
495
|
+
content.appendChild(label);
|
|
496
|
+
try {
|
|
497
|
+
return createDragGhost({
|
|
498
|
+
anchor: source,
|
|
499
|
+
content
|
|
500
|
+
});
|
|
501
|
+
} catch {
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Compute the index of the gap nearest the pointer, ignoring the source item.
|
|
507
|
+
* Returns 0..#items.length-1; undefined if no siblings.
|
|
508
|
+
*
|
|
509
|
+
* Per the plan: drag-mode="reorder" has "no sense of breaking out", so the
|
|
510
|
+
* perpendicular axis is ignored — the pointer always resolves to some gap.
|
|
511
|
+
*/
|
|
512
|
+
#computeGapIndex(clientX, clientY) {
|
|
513
|
+
const state = this.#dragState;
|
|
514
|
+
if (!state) return void 0;
|
|
515
|
+
const siblings = this.#items.filter((el) => el !== state.source);
|
|
516
|
+
if (siblings.length === 0) return 0;
|
|
517
|
+
const useX = !this.#isVertical;
|
|
518
|
+
let bestGap = 0;
|
|
519
|
+
let bestDist = Infinity;
|
|
520
|
+
for (let i = 0; i < siblings.length; i++) {
|
|
521
|
+
const rect = siblings[i].getBoundingClientRect();
|
|
522
|
+
const start = useX ? rect.left : rect.top;
|
|
523
|
+
const end = useX ? rect.right : rect.bottom;
|
|
524
|
+
const mid = (start + end) / 2;
|
|
525
|
+
const pos = useX ? clientX : clientY;
|
|
526
|
+
const dStart = Math.abs(pos - start);
|
|
527
|
+
Math.abs(pos - mid);
|
|
528
|
+
const dEnd = Math.abs(pos - end);
|
|
529
|
+
if (pos < mid) {
|
|
530
|
+
if (dStart < bestDist) {
|
|
531
|
+
bestDist = dStart;
|
|
532
|
+
bestGap = i;
|
|
533
|
+
}
|
|
534
|
+
} else if (dEnd < bestDist) {
|
|
535
|
+
bestDist = dEnd;
|
|
536
|
+
bestGap = i + 1;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return bestGap;
|
|
540
|
+
}
|
|
541
|
+
#computeCaretStyle() {
|
|
542
|
+
const state = this.#dragState;
|
|
543
|
+
if (!state?.active || state.gapIndex === void 0) return void 0;
|
|
544
|
+
const siblings = this.#items.filter((el) => el !== state.source);
|
|
545
|
+
if (siblings.length === 0) return void 0;
|
|
546
|
+
const useX = !this.#isVertical;
|
|
547
|
+
const containerRect = this.#containerEl?.getBoundingClientRect();
|
|
548
|
+
if (!containerRect) return void 0;
|
|
549
|
+
const gap = Math.max(0, Math.min(state.gapIndex, siblings.length));
|
|
550
|
+
let rect = null;
|
|
551
|
+
if (gap < siblings.length) rect = siblings[gap].getBoundingClientRect();
|
|
552
|
+
else rect = siblings[siblings.length - 1].getBoundingClientRect();
|
|
553
|
+
if (useX) {
|
|
554
|
+
const x = (gap < siblings.length ? rect.left : rect.right) - containerRect.left;
|
|
555
|
+
const y = 0;
|
|
556
|
+
const h = containerRect.height;
|
|
557
|
+
const w = 2;
|
|
558
|
+
return `transform: translate(${x - w / 2}px, ${y}px); width: ${w}px; height: ${h}px;`;
|
|
559
|
+
} else {
|
|
560
|
+
const y = (gap < siblings.length ? rect.top : rect.bottom) - containerRect.top;
|
|
561
|
+
const x = 0;
|
|
562
|
+
const w = containerRect.width;
|
|
563
|
+
const h = 2;
|
|
564
|
+
return `transform: translate(${x}px, ${y - h / 2}px); width: ${w}px; height: ${h}px;`;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
get #containerEl() {
|
|
568
|
+
return this.shadowRoot?.getElementById(`container`) ?? null;
|
|
569
|
+
}
|
|
570
|
+
static {
|
|
571
|
+
this.styles = [
|
|
572
|
+
themeFallbacks,
|
|
573
|
+
scrollFadeStyles(`x`),
|
|
574
|
+
scrollFadeStyles(`y`),
|
|
575
|
+
css`
|
|
576
|
+
:host {
|
|
577
|
+
display: inline-block;
|
|
578
|
+
--activated-colour: var(--accent);
|
|
579
|
+
--colour: var(--surface-3-text);
|
|
580
|
+
--scroll-max-height: 200px;
|
|
581
|
+
--allow-overflow: hidden;
|
|
582
|
+
color: var(--colour);
|
|
583
|
+
border: var(--border, none);
|
|
584
|
+
border-color: var(--surface-5);
|
|
585
|
+
border-radius: var(--radius-l, 8px) var(--radius-l, 8px) 0 0;
|
|
586
|
+
|
|
587
|
+
}
|
|
588
|
+
:host(.scrollable) {
|
|
589
|
+
max-height: var(--scroll-max-height);
|
|
590
|
+
}
|
|
591
|
+
:host([hide-labels]) {
|
|
592
|
+
display: inline-flex;
|
|
593
|
+
width: auto;
|
|
594
|
+
}
|
|
595
|
+
text {
|
|
596
|
+
user-select:none;
|
|
597
|
+
}
|
|
598
|
+
.outer {
|
|
599
|
+
display: flex;
|
|
600
|
+
width: 100%;
|
|
601
|
+
height: 100%;
|
|
602
|
+
gap: var(--space-s);
|
|
603
|
+
align-items: stretch;
|
|
604
|
+
box-sizing: border-box;
|
|
605
|
+
}
|
|
606
|
+
.outer.horizontal {
|
|
607
|
+
flex-direction: row;
|
|
608
|
+
align-items: center;
|
|
609
|
+
}
|
|
610
|
+
.outer.vertical-stack,
|
|
611
|
+
.outer.vertical-side {
|
|
612
|
+
flex-direction: column;
|
|
613
|
+
}
|
|
614
|
+
.list-toolbar {
|
|
615
|
+
display: flex;
|
|
616
|
+
align-items: center;
|
|
617
|
+
gap: var(--space-xs);
|
|
618
|
+
flex: 0 0 auto;
|
|
619
|
+
width: max-content;
|
|
620
|
+
max-width: 50%;
|
|
621
|
+
overflow: visible;
|
|
622
|
+
}
|
|
623
|
+
.list-toolbar.horizontal ::slotted(*:not(ixfx-toolbar)) {
|
|
624
|
+
margin-left: auto;
|
|
625
|
+
}
|
|
626
|
+
.list-toolbar[hidden] {
|
|
627
|
+
display: none !important;
|
|
628
|
+
}
|
|
629
|
+
.list-toolbar[hidden] {
|
|
630
|
+
display: none !important;
|
|
631
|
+
}
|
|
632
|
+
::slotted(ixfx-toolbar) {
|
|
633
|
+
--toolbar-bg: transparent;
|
|
634
|
+
--toolbar-border: none;
|
|
635
|
+
--toolbar-padding: 0;
|
|
636
|
+
width: max-content;
|
|
637
|
+
max-width: 100%;
|
|
638
|
+
min-width: 150px;
|
|
639
|
+
flex-shrink: 0;
|
|
640
|
+
}
|
|
641
|
+
#container {
|
|
642
|
+
display:flex;
|
|
643
|
+
position: relative;
|
|
644
|
+
flex: 1 1 0%;
|
|
645
|
+
min-width: 0;
|
|
646
|
+
min-height: 0;
|
|
647
|
+
}
|
|
648
|
+
.outer.vertical-stack #container,
|
|
649
|
+
.outer.vertical-side #container {
|
|
650
|
+
flex: 1 1 auto;
|
|
651
|
+
height: 100%;
|
|
652
|
+
}
|
|
653
|
+
#container.horizontal {
|
|
654
|
+
flex-direction: row;
|
|
655
|
+
overflow: hidden;
|
|
656
|
+
}
|
|
657
|
+
#container.horizontal.scrollable {
|
|
658
|
+
overflow-x: scroll;
|
|
659
|
+
--allow-overflow: visible;
|
|
660
|
+
scrollbar-width: none;
|
|
661
|
+
}
|
|
662
|
+
#container.horizontal.scrollable::-webkit-scrollbar {
|
|
663
|
+
display: none;
|
|
664
|
+
}
|
|
665
|
+
#container.horizontal.scrollable ::slotted(ixfx-tab-list-item) {
|
|
666
|
+
flex-shrink: 0;
|
|
667
|
+
}
|
|
668
|
+
#container.vertical-stack {
|
|
669
|
+
flex-direction: column;
|
|
670
|
+
height: 100%;
|
|
671
|
+
}
|
|
672
|
+
#container.vertical-stack.flip {
|
|
673
|
+
flex-direction: column-reverse;
|
|
674
|
+
}
|
|
675
|
+
#container.vertical-stack.scrollable {
|
|
676
|
+
overflow-y: scroll;
|
|
677
|
+
max-height: var(--scroll-max-height);
|
|
678
|
+
--allow-overflow: visible;
|
|
679
|
+
scrollbar-width: none;
|
|
680
|
+
}
|
|
681
|
+
#container.vertical-stack.scrollable::-webkit-scrollbar {
|
|
682
|
+
display: none;
|
|
683
|
+
}
|
|
684
|
+
#container.vertical-side {
|
|
685
|
+
flex-direction: column;
|
|
686
|
+
height: 100%;
|
|
687
|
+
}
|
|
688
|
+
#container.vertical-side.flip {
|
|
689
|
+
flex-direction: column-reverse;
|
|
690
|
+
}
|
|
691
|
+
#container.vertical-side.scrollable {
|
|
692
|
+
overflow-y: scroll;
|
|
693
|
+
max-height: var(--scroll-max-height);
|
|
694
|
+
--allow-overflow: visible;
|
|
695
|
+
scrollbar-width: none;
|
|
696
|
+
}
|
|
697
|
+
#container.vertical-side.scrollable::-webkit-scrollbar {
|
|
698
|
+
display: none;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
::slotted(ixfx-tab-list-item) {
|
|
702
|
+
flex: 1 1 auto;
|
|
703
|
+
min-width: 0;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
:host([shrink]) ::slotted(ixfx-tab-list-item) {
|
|
707
|
+
flex: 0 0 auto;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
#container.vertical-side ::slotted(ixfx-tab-list-item) {
|
|
711
|
+
writing-mode: vertical-lr;
|
|
712
|
+
text-orientation: mixed;
|
|
713
|
+
}
|
|
714
|
+
#container.vertical-side.flip ::slotted(ixfx-tab-list-item) {
|
|
715
|
+
writing-mode: vertical-rl;
|
|
716
|
+
text-orientation: mixed;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/* Drag affordances — opt-in via drag-mode="reorder" on the host (list). */
|
|
720
|
+
#container.reorder ::slotted(ixfx-tab-list-item) {
|
|
721
|
+
cursor: grab;
|
|
722
|
+
}
|
|
723
|
+
:host([drag-mode]) ::slotted(ixfx-tab-list-item[drag-source]) {
|
|
724
|
+
cursor: grabbing;
|
|
725
|
+
opacity: 0.4;
|
|
726
|
+
transition: opacity var(--transition, 0.15s ease-out);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/* Insertion caret shown during an active drag. */
|
|
730
|
+
.drop-caret {
|
|
731
|
+
position: absolute;
|
|
732
|
+
background: var(--accent);
|
|
733
|
+
border-radius: 1px;
|
|
734
|
+
pointer-events: none;
|
|
735
|
+
z-index: 1;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
::slotted(.activated) {
|
|
739
|
+
--colour: var(--activated-colour);
|
|
740
|
+
}
|
|
741
|
+
`
|
|
742
|
+
];
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
__decorate([property({ type: String })], TabListElement.prototype, "orientation", void 0);
|
|
746
|
+
__decorate([property({ type: Boolean })], TabListElement.prototype, "flip", void 0);
|
|
747
|
+
__decorate([property({ type: Boolean })], TabListElement.prototype, "scrollable", void 0);
|
|
748
|
+
__decorate([property({
|
|
749
|
+
type: Boolean,
|
|
750
|
+
attribute: `hide-labels`
|
|
751
|
+
})], TabListElement.prototype, "hideLabels", void 0);
|
|
752
|
+
__decorate([property({
|
|
753
|
+
type: Boolean,
|
|
754
|
+
attribute: `label-nogrow`
|
|
755
|
+
})], TabListElement.prototype, "labelNogrow", void 0);
|
|
756
|
+
__decorate([property({
|
|
757
|
+
type: Boolean,
|
|
758
|
+
reflect: true
|
|
759
|
+
})], TabListElement.prototype, "shrink", void 0);
|
|
760
|
+
__decorate([property({
|
|
761
|
+
type: String,
|
|
762
|
+
attribute: `drag-mode`
|
|
763
|
+
})], TabListElement.prototype, "dragMode", void 0);
|
|
764
|
+
TabListElement = __decorate([safeCustomElement(`ixfx-tab-list`)], TabListElement);
|
|
765
|
+
//#endregion
|
|
766
|
+
export { TabListElement as t };
|