@sveltia/ui 0.54.0 → 0.55.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/dist/components/resizable-pane/resizable-handle.svelte +12 -4
- package/dist/components/slider/slider.svelte +26 -13
- package/dist/components/tree/tree-item.svelte +243 -0
- package/dist/components/tree/tree-item.svelte.d.ts +158 -0
- package/dist/components/tree/tree.svelte +108 -0
- package/dist/components/tree/tree.svelte.d.ts +126 -0
- package/dist/components/util/app-shell.svelte +7 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/services/tree.svelte.d.ts +235 -0
- package/dist/services/tree.svelte.js +722 -0
- package/dist/styles/variables.scss +8 -0
- package/package.json +4 -3
|
@@ -136,16 +136,20 @@
|
|
|
136
136
|
* @param {PointerEvent} event `pointerup` or `pointercancel` event.
|
|
137
137
|
*/
|
|
138
138
|
const onPointerUp = (event) => {
|
|
139
|
-
|
|
139
|
+
const { pointerId } = event;
|
|
140
140
|
|
|
141
|
+
if (!dragging || pointerId !== targetPointerId) return;
|
|
142
|
+
|
|
143
|
+
element?.releasePointerCapture(pointerId);
|
|
141
144
|
dragging = false;
|
|
142
145
|
startScreenPos = 0;
|
|
143
146
|
targetPointerId = 0;
|
|
144
147
|
|
|
148
|
+
onResizeEnd?.();
|
|
149
|
+
|
|
145
150
|
document.removeEventListener('pointermove', onPointerMove);
|
|
146
151
|
document.removeEventListener('pointerup', onPointerUp);
|
|
147
152
|
document.removeEventListener('pointercancel', onPointerUp);
|
|
148
|
-
onResizeEnd?.();
|
|
149
153
|
};
|
|
150
154
|
|
|
151
155
|
/**
|
|
@@ -158,9 +162,13 @@
|
|
|
158
162
|
event.preventDefault();
|
|
159
163
|
event.stopPropagation();
|
|
160
164
|
|
|
165
|
+
const { screenX, screenY, pointerId } = event;
|
|
166
|
+
|
|
161
167
|
dragging = true;
|
|
162
|
-
startScreenPos = isHorizontal ?
|
|
163
|
-
targetPointerId =
|
|
168
|
+
startScreenPos = isHorizontal ? screenX : screenY;
|
|
169
|
+
targetPointerId = pointerId;
|
|
170
|
+
element?.setPointerCapture(pointerId);
|
|
171
|
+
|
|
164
172
|
onResizeStart?.();
|
|
165
173
|
|
|
166
174
|
document.addEventListener('pointermove', onPointerMove);
|
|
@@ -88,17 +88,24 @@
|
|
|
88
88
|
// Convert physical position to logical position (always LTR)
|
|
89
89
|
// In RTL, left side (physicalX=0) maps to max value (logicalX=barWidth)
|
|
90
90
|
// In LTR, left side (physicalX=0) maps to min value (logicalX=0)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
|
|
91
|
+
// Clamp to the track bounds so a fast drag that overshoots the edge still resolves to the
|
|
92
|
+
// nearest valid position instead of dropping the update.
|
|
93
|
+
const logicalX = Math.min(barWidth, Math.max(0, isRTL() ? barWidth - physicalX : physicalX));
|
|
97
94
|
const fromIndex = positionList.findLastIndex((s) => s <= logicalX);
|
|
98
95
|
const toIndex = positionList.findIndex((s) => logicalX <= s);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
96
|
+
/** @type {number} */
|
|
97
|
+
let index;
|
|
98
|
+
|
|
99
|
+
if (fromIndex === -1) {
|
|
100
|
+
index = toIndex;
|
|
101
|
+
} else if (toIndex === -1) {
|
|
102
|
+
index = fromIndex;
|
|
103
|
+
} else {
|
|
104
|
+
const fromDiff = Math.abs(positionList[fromIndex] - logicalX);
|
|
105
|
+
const toDiff = Math.abs(positionList[toIndex] - logicalX);
|
|
106
|
+
|
|
107
|
+
index = fromDiff < toDiff ? fromIndex : toIndex;
|
|
108
|
+
}
|
|
102
109
|
|
|
103
110
|
if (
|
|
104
111
|
sliderPositions[targetValueIndex] === positionList[index] ||
|
|
@@ -184,6 +191,7 @@
|
|
|
184
191
|
return;
|
|
185
192
|
}
|
|
186
193
|
|
|
194
|
+
event.preventDefault();
|
|
187
195
|
event.stopPropagation();
|
|
188
196
|
|
|
189
197
|
const screenDiff = screenX - startScreenX;
|
|
@@ -198,7 +206,7 @@
|
|
|
198
206
|
* @param {PointerEvent} event `pointerup` or `pointercancel` event.
|
|
199
207
|
*/
|
|
200
208
|
const onPointerUp = (event) => {
|
|
201
|
-
const { pointerId } = event;
|
|
209
|
+
const { pointerId, target } = event;
|
|
202
210
|
|
|
203
211
|
if (disabled || readonly || !dragging || pointerId !== targetPointerId) {
|
|
204
212
|
return;
|
|
@@ -206,8 +214,10 @@
|
|
|
206
214
|
|
|
207
215
|
event.stopPropagation();
|
|
208
216
|
|
|
217
|
+
const slider = /** @type {HTMLElement} */ (target);
|
|
218
|
+
|
|
209
219
|
// Handle a click on the bars
|
|
210
|
-
if (
|
|
220
|
+
if (slider.matches('.base-bar, .slider-bar')) {
|
|
211
221
|
const rect = /** @type {HTMLElement} */ (base).getBoundingClientRect();
|
|
212
222
|
// Get physical X position from left edge
|
|
213
223
|
const physicalX = /** @type {any} */ (event).clientX - rect.left;
|
|
@@ -216,6 +226,7 @@
|
|
|
216
226
|
}
|
|
217
227
|
|
|
218
228
|
// Reset everything
|
|
229
|
+
slider.releasePointerCapture(pointerId);
|
|
219
230
|
dragging = false;
|
|
220
231
|
startX = 0;
|
|
221
232
|
startScreenX = 0;
|
|
@@ -233,16 +244,17 @@
|
|
|
233
244
|
* @param {number} [valueIndex] Index in the {@link values} array to be used to get/set the value.
|
|
234
245
|
*/
|
|
235
246
|
const onPointerDown = (event, valueIndex = 0) => {
|
|
236
|
-
const { clientX, screenX, pointerId } = event;
|
|
237
|
-
|
|
238
247
|
if (disabled || readonly) {
|
|
239
248
|
return;
|
|
240
249
|
}
|
|
241
250
|
|
|
251
|
+
event.preventDefault();
|
|
242
252
|
event.stopPropagation();
|
|
243
253
|
|
|
244
254
|
dragging = true;
|
|
245
255
|
|
|
256
|
+
const { clientX, screenX, pointerId, target } = event;
|
|
257
|
+
const slider = /** @type {HTMLElement} */ (target);
|
|
246
258
|
const rect = /** @type {HTMLElement} */ (base).getBoundingClientRect();
|
|
247
259
|
|
|
248
260
|
// Store physical X position from left edge (same in LTR and RTL)
|
|
@@ -250,6 +262,7 @@
|
|
|
250
262
|
startScreenX = screenX;
|
|
251
263
|
targetPointerId = pointerId;
|
|
252
264
|
targetValueIndex = valueIndex;
|
|
265
|
+
slider.setPointerCapture(pointerId);
|
|
253
266
|
|
|
254
267
|
document.addEventListener('pointermove', onPointerMove);
|
|
255
268
|
document.addEventListener('pointerup', onPointerUp);
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
An item (node) within the `<Tree>` widget. An item that has the `items` slot content becomes a
|
|
4
|
+
parent node that can be expanded and collapsed.
|
|
5
|
+
@see https://w3c.github.io/aria/#treeitem
|
|
6
|
+
@see https://www.w3.org/WAI/ARIA/apg/patterns/treeview/
|
|
7
|
+
-->
|
|
8
|
+
<script>
|
|
9
|
+
import { isRTL } from '@sveltia/i18n';
|
|
10
|
+
import { getContext, setContext } from 'svelte';
|
|
11
|
+
import Icon from '../icon/icon.svelte';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @import { Snippet } from 'svelte';
|
|
15
|
+
* @import { Attachment } from 'svelte/attachments';
|
|
16
|
+
* @import { CommonEventHandlers } from '../../typedefs';
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {object} Props
|
|
21
|
+
* @property {string} [class] The `class` attribute on the wrapper element.
|
|
22
|
+
* @property {boolean} [hidden] Whether to hide the widget. An alias of the `aria-hidden`
|
|
23
|
+
* attribute.
|
|
24
|
+
* @property {boolean} [disabled] Whether to disable the widget. An alias of the `aria-disabled`
|
|
25
|
+
* attribute.
|
|
26
|
+
* @property {boolean} [selected] Whether to select the item. An alias of the `aria-selected`
|
|
27
|
+
* attribute.
|
|
28
|
+
* @property {boolean} [expanded] Whether to expand the item. An alias of the `aria-expanded`
|
|
29
|
+
* attribute. Ignored if the item has no `items` slot content.
|
|
30
|
+
* @property {string} [label] Text label displayed on the item.
|
|
31
|
+
* @property {any} [value] The `data-value` attribute on the item. Default: the `label`.
|
|
32
|
+
* @property {string} [valueType] Data type of the `value`. Typically `string`, `number` or
|
|
33
|
+
* `boolean`. Default: auto detect.
|
|
34
|
+
* @property {Snippet} [children] Primary slot content, used instead of the `label`.
|
|
35
|
+
* @property {Snippet} [items] Child items slot content, which makes the item a parent node.
|
|
36
|
+
* @property {Snippet} [startIcon] Start icon slot content.
|
|
37
|
+
* @property {Snippet} [endIcon] End icon slot content.
|
|
38
|
+
* @property {Snippet} [chevronIcon] Chevron icon slot content.
|
|
39
|
+
* @property {(event: CustomEvent) => void} [onChange] Custom `Change` event handler, called when
|
|
40
|
+
* the selection state is changed.
|
|
41
|
+
* @property {(event: CustomEvent) => void} [onSelect] Custom `Select` event handler.
|
|
42
|
+
* @property {(event: CustomEvent) => void} [onExpand] Custom `Expand` event handler, called when
|
|
43
|
+
* the item is expanded or collapsed.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @type {CommonEventHandlers & Props & Record<string, any>}
|
|
48
|
+
*/
|
|
49
|
+
let {
|
|
50
|
+
/* eslint-disable prefer-const */
|
|
51
|
+
selected = $bindable(false),
|
|
52
|
+
expanded = $bindable(false),
|
|
53
|
+
class: className,
|
|
54
|
+
hidden = false,
|
|
55
|
+
disabled = false,
|
|
56
|
+
label = '',
|
|
57
|
+
// svelte-ignore state_referenced_locally
|
|
58
|
+
value = label,
|
|
59
|
+
// svelte-ignore state_referenced_locally
|
|
60
|
+
valueType = typeof value,
|
|
61
|
+
children,
|
|
62
|
+
items,
|
|
63
|
+
startIcon,
|
|
64
|
+
endIcon,
|
|
65
|
+
chevronIcon,
|
|
66
|
+
onChange,
|
|
67
|
+
onSelect,
|
|
68
|
+
onExpand,
|
|
69
|
+
...restProps
|
|
70
|
+
/* eslint-enable prefer-const */
|
|
71
|
+
} = $props();
|
|
72
|
+
|
|
73
|
+
const id = $props.id();
|
|
74
|
+
/** Nesting level of the item, starting with 1 for the root level items. */
|
|
75
|
+
const level = /** @type {number} */ (getContext('sui:tree-item-level') ?? 0) + 1;
|
|
76
|
+
|
|
77
|
+
setContext('sui:tree-item-level', level);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Handle the `Change` event dispatched by the parent widget when the selection state is changed.
|
|
81
|
+
* @param {any} event `Change` event.
|
|
82
|
+
*/
|
|
83
|
+
const handleChange = (event) => {
|
|
84
|
+
selected = event.detail.selected;
|
|
85
|
+
onChange?.(event);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Handle the `Expand` event dispatched by the parent widget when the item is expanded or
|
|
90
|
+
* collapsed.
|
|
91
|
+
* @param {any} event `Expand` event.
|
|
92
|
+
*/
|
|
93
|
+
const handleExpand = (event) => {
|
|
94
|
+
expanded = event.detail.expanded;
|
|
95
|
+
onExpand?.(event);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Handle the `Select` event dispatched by the parent widget when the item is selected.
|
|
100
|
+
* @param {any} event `Select` event.
|
|
101
|
+
*/
|
|
102
|
+
const handleSelect = (event) => {
|
|
103
|
+
onSelect?.(event);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Listen to the custom events dispatched on the item element by the parent `<Tree>` widget, so
|
|
108
|
+
* the component state can be kept in sync with the DOM state.
|
|
109
|
+
* @type {Attachment}
|
|
110
|
+
*/
|
|
111
|
+
const handleEvents = (element) => {
|
|
112
|
+
/** @type {[string, (event: any) => void][]} */
|
|
113
|
+
const listeners = [
|
|
114
|
+
['Change', handleChange],
|
|
115
|
+
['Expand', handleExpand],
|
|
116
|
+
['Select', handleSelect],
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
listeners.forEach(([type, handler]) => {
|
|
120
|
+
element.addEventListener(type, handler);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return () => {
|
|
124
|
+
listeners.forEach(([type, handler]) => {
|
|
125
|
+
element.removeEventListener(type, handler);
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
</script>
|
|
130
|
+
|
|
131
|
+
<div
|
|
132
|
+
{...restProps}
|
|
133
|
+
{id}
|
|
134
|
+
role="treeitem"
|
|
135
|
+
class="sui treeitem {className}"
|
|
136
|
+
tabindex="-1"
|
|
137
|
+
{hidden}
|
|
138
|
+
aria-hidden={hidden}
|
|
139
|
+
aria-disabled={disabled}
|
|
140
|
+
aria-selected={selected}
|
|
141
|
+
aria-expanded={items ? expanded : undefined}
|
|
142
|
+
aria-level={level}
|
|
143
|
+
aria-labelledby="{id}-label"
|
|
144
|
+
data-label={label}
|
|
145
|
+
data-value={value}
|
|
146
|
+
data-type={valueType}
|
|
147
|
+
{@attach handleEvents}
|
|
148
|
+
>
|
|
149
|
+
<div role="none" class="row" style:--sui-tree-item-level={level}>
|
|
150
|
+
{#if items}
|
|
151
|
+
<span role="none" class="chevron" data-action="toggle">
|
|
152
|
+
{#if chevronIcon}
|
|
153
|
+
{@render chevronIcon()}
|
|
154
|
+
{:else}
|
|
155
|
+
<Icon name={isRTL() ? 'chevron_left' : 'chevron_right'} />
|
|
156
|
+
{/if}
|
|
157
|
+
</span>
|
|
158
|
+
{:else}
|
|
159
|
+
<span role="none" class="chevron placeholder"></span>
|
|
160
|
+
{/if}
|
|
161
|
+
{@render startIcon?.()}
|
|
162
|
+
<span role="none" class="label" id="{id}-label">
|
|
163
|
+
{#if label}
|
|
164
|
+
{label}
|
|
165
|
+
{:else}
|
|
166
|
+
{@render children?.()}
|
|
167
|
+
{/if}
|
|
168
|
+
</span>
|
|
169
|
+
{@render endIcon?.()}
|
|
170
|
+
</div>
|
|
171
|
+
{#if items}
|
|
172
|
+
<div role="group" class="group" hidden={!expanded}>
|
|
173
|
+
{@render items()}
|
|
174
|
+
</div>
|
|
175
|
+
{/if}
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<style>[role=treeitem] {
|
|
179
|
+
display: block;
|
|
180
|
+
}
|
|
181
|
+
[role=treeitem][hidden] {
|
|
182
|
+
display: none;
|
|
183
|
+
}
|
|
184
|
+
[role=treeitem]:focus-visible {
|
|
185
|
+
outline-color: transparent;
|
|
186
|
+
}
|
|
187
|
+
[role=treeitem]:focus-visible > .row {
|
|
188
|
+
outline-color: var(--sui-focus-ring-color);
|
|
189
|
+
outline-offset: calc(var(--sui-focus-ring-width) * -1);
|
|
190
|
+
}
|
|
191
|
+
[role=treeitem][aria-selected=true] > .row {
|
|
192
|
+
color: var(--sui-highlight-foreground-color);
|
|
193
|
+
background-color: var(--sui-selected-background-color);
|
|
194
|
+
}
|
|
195
|
+
[role=treeitem] > .row:hover {
|
|
196
|
+
color: var(--sui-highlight-foreground-color);
|
|
197
|
+
background-color: var(--sui-hover-background-color);
|
|
198
|
+
}
|
|
199
|
+
[role=treeitem] > .row:active {
|
|
200
|
+
background-color: var(--sui-active-background-color);
|
|
201
|
+
}
|
|
202
|
+
[role=treeitem][aria-expanded=true] > .row > .chevron:dir(ltr) {
|
|
203
|
+
transform: rotate(90deg);
|
|
204
|
+
}
|
|
205
|
+
[role=treeitem][aria-expanded=true] > .row > .chevron:dir(rtl) {
|
|
206
|
+
transform: rotate(-90deg);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.row {
|
|
210
|
+
display: flex;
|
|
211
|
+
align-items: center;
|
|
212
|
+
gap: 4px;
|
|
213
|
+
border-radius: var(--sui-tree-item-border-radius, var(--sui-option-border-radius, 4px));
|
|
214
|
+
padding: var(--sui-tree-item-padding, 0 8px 0 4px);
|
|
215
|
+
padding-inline-start: calc((var(--sui-tree-item-level, 1) - 1) * var(--sui-tree-item-indent, 16px) + 4px);
|
|
216
|
+
min-height: var(--sui-tree-item-height, var(--sui-option-height));
|
|
217
|
+
cursor: pointer;
|
|
218
|
+
transition: background-color 200ms;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.chevron {
|
|
222
|
+
flex: none;
|
|
223
|
+
display: flex;
|
|
224
|
+
align-items: center;
|
|
225
|
+
justify-content: center;
|
|
226
|
+
width: var(--sui-tree-item-chevron-size, 24px);
|
|
227
|
+
height: var(--sui-tree-item-chevron-size, 24px);
|
|
228
|
+
transition: transform 200ms;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.label {
|
|
232
|
+
flex: auto;
|
|
233
|
+
overflow: hidden;
|
|
234
|
+
white-space: nowrap;
|
|
235
|
+
text-overflow: ellipsis;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.group {
|
|
239
|
+
display: block;
|
|
240
|
+
}
|
|
241
|
+
.group[hidden] {
|
|
242
|
+
display: none;
|
|
243
|
+
}</style>
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
export default TreeItem;
|
|
2
|
+
type TreeItem = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<KeyboardEventHandlers & MouseEventHandlers & PointerEventHandlers & FocusEventHandlers & DragEventHandlers & Props & Record<string, any>>): void;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* An item (node) within the `<Tree>` widget. An item that has the `items` slot content becomes a
|
|
8
|
+
* parent node that can be expanded and collapsed.
|
|
9
|
+
* @see https://w3c.github.io/aria/#treeitem
|
|
10
|
+
* @see https://www.w3.org/WAI/ARIA/apg/patterns/treeview/
|
|
11
|
+
*/
|
|
12
|
+
declare const TreeItem: import("svelte").Component<import("../../typedefs").KeyboardEventHandlers & import("../../typedefs").MouseEventHandlers & import("../../typedefs").PointerEventHandlers & import("../../typedefs").FocusEventHandlers & import("../../typedefs").DragEventHandlers & {
|
|
13
|
+
/**
|
|
14
|
+
* The `class` attribute on the wrapper element.
|
|
15
|
+
*/
|
|
16
|
+
class?: string | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Whether to hide the widget. An alias of the `aria-hidden`
|
|
19
|
+
* attribute.
|
|
20
|
+
*/
|
|
21
|
+
hidden?: boolean | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Whether to disable the widget. An alias of the `aria-disabled`
|
|
24
|
+
* attribute.
|
|
25
|
+
*/
|
|
26
|
+
disabled?: boolean | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Whether to select the item. An alias of the `aria-selected`
|
|
29
|
+
* attribute.
|
|
30
|
+
*/
|
|
31
|
+
selected?: boolean | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Whether to expand the item. An alias of the `aria-expanded`
|
|
34
|
+
* attribute. Ignored if the item has no `items` slot content.
|
|
35
|
+
*/
|
|
36
|
+
expanded?: boolean | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Text label displayed on the item.
|
|
39
|
+
*/
|
|
40
|
+
label?: string | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* The `data-value` attribute on the item. Default: the `label`.
|
|
43
|
+
*/
|
|
44
|
+
value?: any;
|
|
45
|
+
/**
|
|
46
|
+
* Data type of the `value`. Typically `string`, `number` or
|
|
47
|
+
* `boolean`. Default: auto detect.
|
|
48
|
+
*/
|
|
49
|
+
valueType?: string | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* Primary slot content, used instead of the `label`.
|
|
52
|
+
*/
|
|
53
|
+
children?: Snippet<[]> | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* Child items slot content, which makes the item a parent node.
|
|
56
|
+
*/
|
|
57
|
+
items?: Snippet<[]> | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* Start icon slot content.
|
|
60
|
+
*/
|
|
61
|
+
startIcon?: Snippet<[]> | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* End icon slot content.
|
|
64
|
+
*/
|
|
65
|
+
endIcon?: Snippet<[]> | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* Chevron icon slot content.
|
|
68
|
+
*/
|
|
69
|
+
chevronIcon?: Snippet<[]> | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* Custom `Change` event handler, called when
|
|
72
|
+
* the selection state is changed.
|
|
73
|
+
*/
|
|
74
|
+
onChange?: ((event: CustomEvent) => void) | undefined;
|
|
75
|
+
/**
|
|
76
|
+
* Custom `Select` event handler.
|
|
77
|
+
*/
|
|
78
|
+
onSelect?: ((event: CustomEvent) => void) | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* Custom `Expand` event handler, called when
|
|
81
|
+
* the item is expanded or collapsed.
|
|
82
|
+
*/
|
|
83
|
+
onExpand?: ((event: CustomEvent) => void) | undefined;
|
|
84
|
+
} & Record<string, any>, {}, "selected" | "expanded">;
|
|
85
|
+
type Props = {
|
|
86
|
+
/**
|
|
87
|
+
* The `class` attribute on the wrapper element.
|
|
88
|
+
*/
|
|
89
|
+
class?: string | undefined;
|
|
90
|
+
/**
|
|
91
|
+
* Whether to hide the widget. An alias of the `aria-hidden`
|
|
92
|
+
* attribute.
|
|
93
|
+
*/
|
|
94
|
+
hidden?: boolean | undefined;
|
|
95
|
+
/**
|
|
96
|
+
* Whether to disable the widget. An alias of the `aria-disabled`
|
|
97
|
+
* attribute.
|
|
98
|
+
*/
|
|
99
|
+
disabled?: boolean | undefined;
|
|
100
|
+
/**
|
|
101
|
+
* Whether to select the item. An alias of the `aria-selected`
|
|
102
|
+
* attribute.
|
|
103
|
+
*/
|
|
104
|
+
selected?: boolean | undefined;
|
|
105
|
+
/**
|
|
106
|
+
* Whether to expand the item. An alias of the `aria-expanded`
|
|
107
|
+
* attribute. Ignored if the item has no `items` slot content.
|
|
108
|
+
*/
|
|
109
|
+
expanded?: boolean | undefined;
|
|
110
|
+
/**
|
|
111
|
+
* Text label displayed on the item.
|
|
112
|
+
*/
|
|
113
|
+
label?: string | undefined;
|
|
114
|
+
/**
|
|
115
|
+
* The `data-value` attribute on the item. Default: the `label`.
|
|
116
|
+
*/
|
|
117
|
+
value?: any;
|
|
118
|
+
/**
|
|
119
|
+
* Data type of the `value`. Typically `string`, `number` or
|
|
120
|
+
* `boolean`. Default: auto detect.
|
|
121
|
+
*/
|
|
122
|
+
valueType?: string | undefined;
|
|
123
|
+
/**
|
|
124
|
+
* Primary slot content, used instead of the `label`.
|
|
125
|
+
*/
|
|
126
|
+
children?: Snippet<[]> | undefined;
|
|
127
|
+
/**
|
|
128
|
+
* Child items slot content, which makes the item a parent node.
|
|
129
|
+
*/
|
|
130
|
+
items?: Snippet<[]> | undefined;
|
|
131
|
+
/**
|
|
132
|
+
* Start icon slot content.
|
|
133
|
+
*/
|
|
134
|
+
startIcon?: Snippet<[]> | undefined;
|
|
135
|
+
/**
|
|
136
|
+
* End icon slot content.
|
|
137
|
+
*/
|
|
138
|
+
endIcon?: Snippet<[]> | undefined;
|
|
139
|
+
/**
|
|
140
|
+
* Chevron icon slot content.
|
|
141
|
+
*/
|
|
142
|
+
chevronIcon?: Snippet<[]> | undefined;
|
|
143
|
+
/**
|
|
144
|
+
* Custom `Change` event handler, called when
|
|
145
|
+
* the selection state is changed.
|
|
146
|
+
*/
|
|
147
|
+
onChange?: ((event: CustomEvent) => void) | undefined;
|
|
148
|
+
/**
|
|
149
|
+
* Custom `Select` event handler.
|
|
150
|
+
*/
|
|
151
|
+
onSelect?: ((event: CustomEvent) => void) | undefined;
|
|
152
|
+
/**
|
|
153
|
+
* Custom `Expand` event handler, called when
|
|
154
|
+
* the item is expanded or collapsed.
|
|
155
|
+
*/
|
|
156
|
+
onExpand?: ((event: CustomEvent) => void) | undefined;
|
|
157
|
+
};
|
|
158
|
+
import type { Snippet } from 'svelte';
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
A tree view widget that displays a hierarchical list of items, which can be expanded, collapsed
|
|
4
|
+
and selected.
|
|
5
|
+
@see https://w3c.github.io/aria/#tree
|
|
6
|
+
@see https://www.w3.org/WAI/ARIA/apg/patterns/treeview/
|
|
7
|
+
-->
|
|
8
|
+
<script>
|
|
9
|
+
import { activateTree } from '../../services/tree.svelte.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @import { Snippet } from 'svelte';
|
|
13
|
+
* @import { CommonEventHandlers } from '../../typedefs';
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {object} Props
|
|
18
|
+
* @property {HTMLElement} [element] A reference to the wrapper element.
|
|
19
|
+
* @property {string} [class] The `class` attribute on the wrapper element.
|
|
20
|
+
* @property {boolean} [hidden] Whether to hide the widget. An alias of the `aria-hidden`
|
|
21
|
+
* attribute.
|
|
22
|
+
* @property {boolean} [disabled] Whether to disable the widget. An alias of the `aria-disabled`
|
|
23
|
+
* attribute.
|
|
24
|
+
* @property {boolean} [readonly] Whether to make the widget read-only. An alias of the
|
|
25
|
+
* `aria-readonly` attribute.
|
|
26
|
+
* @property {boolean} [multiple] Whether to allow selecting more than one `<TreeItem>`. An alias
|
|
27
|
+
* of the `aria-multiselectable` attribute.
|
|
28
|
+
* @property {boolean} [clickToSelect] Whether to select an item by clicking on it.
|
|
29
|
+
* @property {boolean} [selectionFollowsFocus] Whether to select an item as soon as it receives
|
|
30
|
+
* focus. Default: `true` on a single-select tree, `false` on a multi-select tree.
|
|
31
|
+
* @property {boolean} [expandOnSelect] Whether to expand or collapse a parent item when the item
|
|
32
|
+
* itself, rather than its chevron, is clicked or activated with the Enter key.
|
|
33
|
+
* @property {string} [ariaLabel] The `aria-label` attribute on the wrapper element. Required
|
|
34
|
+
* unless the `aria-labelledby` attribute is provided.
|
|
35
|
+
* @property {Snippet} [children] Primary slot content.
|
|
36
|
+
* @property {(event: CustomEvent) => void} [onChange] Custom `Change` event handler.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @type {CommonEventHandlers & Props & Record<string, any>}
|
|
41
|
+
*/
|
|
42
|
+
let {
|
|
43
|
+
/* eslint-disable prefer-const */
|
|
44
|
+
element = $bindable(),
|
|
45
|
+
class: className,
|
|
46
|
+
hidden = false,
|
|
47
|
+
disabled = false,
|
|
48
|
+
readonly = false,
|
|
49
|
+
multiple = false,
|
|
50
|
+
clickToSelect = true,
|
|
51
|
+
selectionFollowsFocus = undefined,
|
|
52
|
+
expandOnSelect = true,
|
|
53
|
+
ariaLabel = undefined,
|
|
54
|
+
children,
|
|
55
|
+
onChange,
|
|
56
|
+
...restProps
|
|
57
|
+
/* eslint-enable prefer-const */
|
|
58
|
+
} = $props();
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<!--
|
|
62
|
+
The `aria-readonly` attribute is not part of the `tree` role in ARIA, but we still use it to keep
|
|
63
|
+
the API consistent with the other selection widgets, including `<Listbox>`. It’s only rendered
|
|
64
|
+
when the widget is actually read-only, so assistive technologies can safely ignore it.
|
|
65
|
+
-->
|
|
66
|
+
<!-- svelte-ignore a11y_role_supports_aria_props -->
|
|
67
|
+
<div
|
|
68
|
+
bind:this={element}
|
|
69
|
+
{...restProps}
|
|
70
|
+
role="tree"
|
|
71
|
+
class="sui tree {className}"
|
|
72
|
+
{hidden}
|
|
73
|
+
aria-hidden={hidden}
|
|
74
|
+
aria-disabled={disabled}
|
|
75
|
+
aria-readonly={readonly || undefined}
|
|
76
|
+
aria-multiselectable={multiple}
|
|
77
|
+
aria-label={ariaLabel}
|
|
78
|
+
onChange={(/** @type {CustomEvent} */ event) => {
|
|
79
|
+
onChange?.(event);
|
|
80
|
+
}}
|
|
81
|
+
{@attach activateTree({ clickToSelect, selectionFollowsFocus, expandOnSelect })}
|
|
82
|
+
>
|
|
83
|
+
<div role="none" class="inner" inert={disabled}>
|
|
84
|
+
{@render children?.()}
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<style>[role=tree] {
|
|
89
|
+
display: flex;
|
|
90
|
+
flex-direction: column;
|
|
91
|
+
margin: var(--sui-focus-ring-width);
|
|
92
|
+
border-width: var(--sui-tree-border-width, 1px);
|
|
93
|
+
border-style: var(--sui-tree-border-style, solid);
|
|
94
|
+
border-color: var(--sui-tree-border-color, var(--sui-secondary-border-color));
|
|
95
|
+
border-radius: var(--sui-tree-border-radius, 4px);
|
|
96
|
+
padding: var(--sui-tree-padding, 4px);
|
|
97
|
+
min-width: var(--sui-tree-min-width, calc(var(--sui-option-height) * 5));
|
|
98
|
+
overflow: auto;
|
|
99
|
+
color: var(--sui-tree-foreground-color, var(--sui-control-foreground-color));
|
|
100
|
+
background-color: var(--sui-tree-background-color);
|
|
101
|
+
font-family: var(--sui-control-font-family);
|
|
102
|
+
font-size: var(--sui-control-font-size);
|
|
103
|
+
line-height: var(--sui-control-line-height);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.inner {
|
|
107
|
+
display: contents;
|
|
108
|
+
}</style>
|