@geoffcox/sterling-svelte 2.0.2 → 2.0.3
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/README.md +18 -0
- package/dist/@types/clickOutside.d.ts +15 -0
- package/dist/Button.svelte +29 -0
- package/dist/Button.svelte.d.ts +8 -0
- package/dist/Callout.svelte +243 -0
- package/dist/Callout.svelte.d.ts +14 -0
- package/dist/Callout.types.d.ts +11 -0
- package/dist/Callout.types.js +1 -0
- package/dist/Checkbox.svelte +62 -0
- package/dist/Checkbox.svelte.d.ts +9 -0
- package/dist/Dialog.svelte +201 -0
- package/dist/Dialog.svelte.d.ts +14 -0
- package/dist/Dropdown.svelte +159 -0
- package/dist/Dropdown.svelte.d.ts +23 -0
- package/dist/Input.svelte +80 -0
- package/dist/Input.svelte.d.ts +11 -0
- package/dist/Label.constants.d.ts +2 -0
- package/dist/Label.constants.js +2 -0
- package/dist/Label.svelte +135 -0
- package/dist/Label.svelte.d.ts +17 -0
- package/dist/Link.svelte +31 -0
- package/dist/Link.svelte.d.ts +11 -0
- package/dist/List.constants.d.ts +1 -0
- package/dist/List.constants.js +1 -0
- package/dist/List.svelte +258 -0
- package/dist/List.svelte.d.ts +19 -0
- package/dist/List.types.d.ts +5 -0
- package/dist/List.types.js +1 -0
- package/dist/ListItem.svelte +64 -0
- package/dist/ListItem.svelte.d.ts +12 -0
- package/dist/Menu.svelte +105 -0
- package/dist/Menu.svelte.d.ts +12 -0
- package/dist/MenuBar.constants.d.ts +1 -0
- package/dist/MenuBar.constants.js +1 -0
- package/dist/MenuBar.svelte +144 -0
- package/dist/MenuBar.svelte.d.ts +12 -0
- package/dist/MenuBar.types.d.ts +4 -0
- package/dist/MenuBar.types.js +1 -0
- package/dist/MenuButton.svelte +156 -0
- package/dist/MenuButton.svelte.d.ts +20 -0
- package/dist/MenuItem.constants.d.ts +2 -0
- package/dist/MenuItem.constants.js +2 -0
- package/dist/MenuItem.svelte +431 -0
- package/dist/MenuItem.svelte.d.ts +22 -0
- package/dist/MenuItem.types.d.ts +20 -0
- package/dist/MenuItem.types.js +1 -0
- package/dist/MenuItem.utils.d.ts +7 -0
- package/dist/MenuItem.utils.js +36 -0
- package/dist/MenuSeparator.svelte +11 -0
- package/dist/MenuSeparator.svelte.d.ts +5 -0
- package/dist/Pagination.svelte +267 -0
- package/dist/Pagination.svelte.d.ts +4 -0
- package/dist/Pagination.types.d.ts +24 -0
- package/dist/Pagination.types.js +1 -0
- package/dist/Popover.constants.d.ts +1 -0
- package/dist/Popover.constants.js +14 -0
- package/dist/Popover.svelte +175 -0
- package/dist/Popover.svelte.d.ts +14 -0
- package/dist/Popover.types.d.ts +4 -0
- package/dist/Popover.types.js +1 -0
- package/dist/Portal.constants.d.ts +2 -0
- package/dist/Portal.constants.js +2 -0
- package/dist/Portal.types.d.ts +3 -0
- package/dist/Portal.types.js +1 -0
- package/dist/Progress.constants.d.ts +1 -0
- package/dist/Progress.constants.js +1 -0
- package/dist/Progress.svelte +61 -0
- package/dist/Progress.svelte.d.ts +11 -0
- package/dist/Progress.types.d.ts +4 -0
- package/dist/Progress.types.js +1 -0
- package/dist/Radio.svelte +65 -0
- package/dist/Radio.svelte.d.ts +12 -0
- package/dist/Select.svelte +262 -0
- package/dist/Select.svelte.d.ts +26 -0
- package/dist/Slider.svelte +182 -0
- package/dist/Slider.svelte.d.ts +18 -0
- package/dist/Switch.svelte +92 -0
- package/dist/Switch.svelte.d.ts +21 -0
- package/dist/Tab.svelte +66 -0
- package/dist/Tab.svelte.d.ts +11 -0
- package/dist/TabList.constants.d.ts +1 -0
- package/dist/TabList.constants.js +1 -0
- package/dist/TabList.svelte +253 -0
- package/dist/TabList.svelte.d.ts +17 -0
- package/dist/TabList.types.d.ts +5 -0
- package/dist/TabList.types.js +1 -0
- package/dist/TextArea.constants.d.ts +1 -0
- package/dist/TextArea.constants.js +1 -0
- package/dist/TextArea.svelte +116 -0
- package/dist/TextArea.svelte.d.ts +18 -0
- package/dist/TextArea.types.d.ts +4 -0
- package/dist/TextArea.types.js +1 -0
- package/dist/Tooltip.svelte +95 -0
- package/dist/Tooltip.svelte.d.ts +10 -0
- package/dist/Tree.constants.d.ts +1 -0
- package/dist/Tree.constants.js +1 -0
- package/dist/Tree.svelte +81 -0
- package/dist/Tree.svelte.d.ts +14 -0
- package/dist/Tree.types.d.ts +5 -0
- package/dist/Tree.types.js +1 -0
- package/dist/TreeChevron.svelte +39 -0
- package/dist/TreeChevron.svelte.d.ts +8 -0
- package/dist/TreeItem.constants.d.ts +1 -0
- package/dist/TreeItem.constants.js +1 -0
- package/dist/TreeItem.svelte +396 -0
- package/dist/TreeItem.svelte.d.ts +22 -0
- package/dist/TreeItem.types.d.ts +4 -0
- package/dist/TreeItem.types.js +1 -0
- package/dist/actions/applyLightDarkMode.d.ts +10 -0
- package/dist/actions/applyLightDarkMode.js +36 -0
- package/dist/actions/clickOutside.d.ts +15 -0
- package/dist/actions/clickOutside.js +28 -0
- package/dist/actions/extraClass.d.ts +9 -0
- package/dist/actions/extraClass.js +15 -0
- package/dist/actions/forwardEvents.d.ts +12 -0
- package/dist/actions/forwardEvents.js +32 -0
- package/dist/actions/portal.d.ts +8 -0
- package/dist/actions/portal.js +19 -0
- package/dist/actions/trapKeyboardFocus.d.ts +3 -0
- package/dist/actions/trapKeyboardFocus.js +52 -0
- package/dist/idGenerator.d.ts +5 -0
- package/dist/idGenerator.js +11 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +55 -0
- package/dist/mediaQueries/prefersColorSchemeDark.d.ts +1 -0
- package/dist/mediaQueries/prefersColorSchemeDark.js +14 -0
- package/dist/mediaQueries/prefersReducedMotion.d.ts +1 -0
- package/dist/mediaQueries/prefersReducedMotion.js +14 -0
- package/dist/mediaQueries/usingKeyboard.d.ts +1 -0
- package/dist/mediaQueries/usingKeyboard.js +17 -0
- package/package.json +8 -7
package/dist/List.svelte
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
<svelte:options runes={true} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
import { setContext } from 'svelte';
|
|
5
|
+
import type { HTMLAttributes, KeyboardEventHandler, MouseEventHandler } from 'svelte/elements';
|
|
6
|
+
import { LIST_CONTEXT_KEY } from './List.constants';
|
|
7
|
+
import type { ListContext } from './List.types';
|
|
8
|
+
import { usingKeyboard } from './mediaQueries/usingKeyboard';
|
|
9
|
+
|
|
10
|
+
type Props = HTMLAttributes<HTMLDivElement> & {
|
|
11
|
+
disabled?: boolean | null;
|
|
12
|
+
horizontal?: boolean | null;
|
|
13
|
+
selectedValue?: string;
|
|
14
|
+
onSelect?: (value?: string) => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
children,
|
|
19
|
+
class: _class,
|
|
20
|
+
disabled = false,
|
|
21
|
+
horizontal = false,
|
|
22
|
+
selectedValue = $bindable(),
|
|
23
|
+
onSelect,
|
|
24
|
+
...rest
|
|
25
|
+
}: Props = $props();
|
|
26
|
+
|
|
27
|
+
let listRef: HTMLDivElement;
|
|
28
|
+
let lastSelectedItemRef: HTMLElement;
|
|
29
|
+
|
|
30
|
+
let listContext = $state({
|
|
31
|
+
disabled,
|
|
32
|
+
selectedValue,
|
|
33
|
+
horizontal
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
$effect(() => {
|
|
37
|
+
listContext.disabled = disabled;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
$effect(() => {
|
|
41
|
+
listContext.horizontal = horizontal;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
$effect(() => {
|
|
45
|
+
listContext.selectedValue = selectedValue;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
setContext<ListContext>(LIST_CONTEXT_KEY, listContext);
|
|
49
|
+
|
|
50
|
+
export const blur = () => {
|
|
51
|
+
listRef?.blur();
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const click = () => {
|
|
55
|
+
listRef?.click();
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const focus = (options?: FocusOptions) => {
|
|
59
|
+
listRef?.focus();
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const scrollToSelectedItem = () => {
|
|
63
|
+
const element = getSelectedItemElement();
|
|
64
|
+
element?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
$effect(() => {
|
|
68
|
+
onSelect?.(selectedValue);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const isElementListItem = (candidate: Element) => {
|
|
72
|
+
return (
|
|
73
|
+
candidate &&
|
|
74
|
+
candidate.getAttribute('data-value') !== null &&
|
|
75
|
+
candidate.getAttribute('data-value') !== undefined &&
|
|
76
|
+
candidate.getAttribute('role') === 'listitem'
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const getSelectedItemElement = () => {
|
|
81
|
+
if (
|
|
82
|
+
isElementListItem(lastSelectedItemRef) &&
|
|
83
|
+
lastSelectedItemRef?.getAttribute('data-value') === selectedValue &&
|
|
84
|
+
lastSelectedItemRef?.closest('[role="list"]') === listRef
|
|
85
|
+
) {
|
|
86
|
+
return lastSelectedItemRef;
|
|
87
|
+
} else {
|
|
88
|
+
return listRef?.querySelector('[data-value][aria-selected=true]');
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const selectItem = (value: string, element: HTMLElement) => {
|
|
93
|
+
selectedValue = value;
|
|
94
|
+
lastSelectedItemRef = element;
|
|
95
|
+
element.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const selectFirstItem = () => {
|
|
99
|
+
let candidate: Element | undefined | null = listRef?.querySelector(
|
|
100
|
+
'[data-value][role=listitem]'
|
|
101
|
+
);
|
|
102
|
+
while (candidate && !isElementListItem(candidate)) {
|
|
103
|
+
candidate = candidate.nextElementSibling;
|
|
104
|
+
}
|
|
105
|
+
let candidateValue = candidate?.getAttribute('data-value');
|
|
106
|
+
|
|
107
|
+
if (candidateValue && candidate) {
|
|
108
|
+
selectItem(candidateValue, candidate as HTMLElement);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return false;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const selectPreviousItem = () => {
|
|
116
|
+
let selectedItem = getSelectedItemElement();
|
|
117
|
+
let candidate = selectedItem?.previousElementSibling;
|
|
118
|
+
while (candidate && !isElementListItem(candidate)) {
|
|
119
|
+
candidate = candidate.previousElementSibling;
|
|
120
|
+
}
|
|
121
|
+
let candidateValue = candidate?.getAttribute('data-value');
|
|
122
|
+
|
|
123
|
+
if (candidateValue && candidate) {
|
|
124
|
+
selectItem(candidateValue, candidate as HTMLElement);
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return false;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export const selectNextItem = () => {
|
|
132
|
+
let selectedItem = getSelectedItemElement();
|
|
133
|
+
let candidate = selectedItem?.nextElementSibling;
|
|
134
|
+
while (candidate && !isElementListItem(candidate)) {
|
|
135
|
+
candidate = candidate.nextElementSibling;
|
|
136
|
+
}
|
|
137
|
+
let candidateValue = candidate?.getAttribute('data-value');
|
|
138
|
+
|
|
139
|
+
if (candidateValue && candidate) {
|
|
140
|
+
selectItem(candidateValue, candidate as HTMLElement);
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return false;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export const selectLastItem = () => {
|
|
148
|
+
let candidate: Element | undefined | null = listRef?.querySelector(
|
|
149
|
+
'[data-value][role=listitem]:last-of-type'
|
|
150
|
+
);
|
|
151
|
+
while (candidate && !isElementListItem(candidate)) {
|
|
152
|
+
candidate = candidate.previousElementSibling;
|
|
153
|
+
}
|
|
154
|
+
let candidateValue = candidate?.getAttribute('data-value');
|
|
155
|
+
|
|
156
|
+
if (candidateValue && candidate) {
|
|
157
|
+
selectItem(candidateValue, candidate as HTMLElement);
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return false;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const onClick: MouseEventHandler<HTMLDivElement> = (event) => {
|
|
165
|
+
if (!disabled) {
|
|
166
|
+
let candidate: HTMLElement | null | undefined = event.target as HTMLElement;
|
|
167
|
+
let candidateValue: string | null | undefined = candidate?.getAttribute('data-value');
|
|
168
|
+
|
|
169
|
+
if (candidateValue === undefined || candidateValue === null) {
|
|
170
|
+
candidate = candidate?.closest<HTMLElement>('[data-value]');
|
|
171
|
+
candidateValue = candidate?.getAttribute('data-value');
|
|
172
|
+
}
|
|
173
|
+
if (candidateValue && candidate) {
|
|
174
|
+
selectItem(candidateValue, candidate);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
rest.onclick?.(event);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const onKeydown: KeyboardEventHandler<HTMLDivElement> = (event) => {
|
|
181
|
+
if (!disabled && !event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey) {
|
|
182
|
+
switch (event.key) {
|
|
183
|
+
case 'Home':
|
|
184
|
+
selectFirstItem();
|
|
185
|
+
event.preventDefault();
|
|
186
|
+
event.stopPropagation();
|
|
187
|
+
return false;
|
|
188
|
+
case 'End':
|
|
189
|
+
selectLastItem();
|
|
190
|
+
event.preventDefault();
|
|
191
|
+
event.stopPropagation();
|
|
192
|
+
return false;
|
|
193
|
+
case 'ArrowLeft':
|
|
194
|
+
if (horizontal) {
|
|
195
|
+
selectedValue !== undefined && selectedValue.length > 0
|
|
196
|
+
? selectPreviousItem()
|
|
197
|
+
: selectLastItem();
|
|
198
|
+
}
|
|
199
|
+
event.preventDefault();
|
|
200
|
+
event.stopPropagation();
|
|
201
|
+
return false;
|
|
202
|
+
case 'ArrowRight':
|
|
203
|
+
if (horizontal) {
|
|
204
|
+
selectedValue !== undefined && selectedValue.length > 0
|
|
205
|
+
? selectNextItem()
|
|
206
|
+
: selectFirstItem();
|
|
207
|
+
}
|
|
208
|
+
event.preventDefault();
|
|
209
|
+
event.stopPropagation();
|
|
210
|
+
return false;
|
|
211
|
+
case 'ArrowUp':
|
|
212
|
+
if (!horizontal) {
|
|
213
|
+
selectedValue !== undefined && selectedValue.length > 0
|
|
214
|
+
? selectPreviousItem()
|
|
215
|
+
: selectLastItem();
|
|
216
|
+
}
|
|
217
|
+
event.preventDefault();
|
|
218
|
+
event.stopPropagation();
|
|
219
|
+
return false;
|
|
220
|
+
case 'ArrowDown':
|
|
221
|
+
if (!horizontal) {
|
|
222
|
+
selectedValue !== undefined && selectedValue.length > 0
|
|
223
|
+
? selectNextItem()
|
|
224
|
+
: selectFirstItem();
|
|
225
|
+
}
|
|
226
|
+
event.preventDefault();
|
|
227
|
+
event.stopPropagation();
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
rest.onkeydown?.(event);
|
|
232
|
+
};
|
|
233
|
+
</script>
|
|
234
|
+
|
|
235
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
236
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
237
|
+
<!-- svelte-ignore a11y_role_supports_aria_props -->
|
|
238
|
+
<div
|
|
239
|
+
aria-activedescendant={selectedValue}
|
|
240
|
+
aria-disabled={disabled}
|
|
241
|
+
aria-orientation={horizontal ? 'horizontal' : 'vertical'}
|
|
242
|
+
bind:this={listRef}
|
|
243
|
+
class={['sterling-list', _class]}
|
|
244
|
+
class:disabled
|
|
245
|
+
class:horizontal
|
|
246
|
+
class:using-keyboard={$usingKeyboard}
|
|
247
|
+
role="list"
|
|
248
|
+
tabindex={0}
|
|
249
|
+
{...rest}
|
|
250
|
+
onclick={onClick}
|
|
251
|
+
onkeydown={onKeydown}
|
|
252
|
+
>
|
|
253
|
+
<div class="container">
|
|
254
|
+
{#if children}
|
|
255
|
+
{@render children()}
|
|
256
|
+
{/if}
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
type Props = HTMLAttributes<HTMLDivElement> & {
|
|
3
|
+
disabled?: boolean | null;
|
|
4
|
+
horizontal?: boolean | null;
|
|
5
|
+
selectedValue?: string;
|
|
6
|
+
onSelect?: (value?: string) => void;
|
|
7
|
+
};
|
|
8
|
+
declare const List: import("svelte").Component<Props, {
|
|
9
|
+
blur: () => void;
|
|
10
|
+
click: () => void;
|
|
11
|
+
focus: (options?: FocusOptions) => void;
|
|
12
|
+
scrollToSelectedItem: () => void;
|
|
13
|
+
selectFirstItem: () => boolean;
|
|
14
|
+
selectPreviousItem: () => boolean;
|
|
15
|
+
selectNextItem: () => boolean;
|
|
16
|
+
selectLastItem: () => boolean;
|
|
17
|
+
}, "selectedValue">;
|
|
18
|
+
type List = ReturnType<typeof List>;
|
|
19
|
+
export default List;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<svelte:options runes={true} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
import { getContext } from 'svelte';
|
|
5
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
6
|
+
import { LIST_CONTEXT_KEY } from './List.constants';
|
|
7
|
+
import type { ListContext } from './List.types';
|
|
8
|
+
|
|
9
|
+
type Props = HTMLAttributes<HTMLDivElement> & {
|
|
10
|
+
disabled?: boolean | null;
|
|
11
|
+
value?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
let { children, class: _class, disabled, value, ...rest }: Props = $props();
|
|
15
|
+
|
|
16
|
+
const listContext = getContext<ListContext>(LIST_CONTEXT_KEY);
|
|
17
|
+
|
|
18
|
+
let selected = $state(listContext.selectedValue === value);
|
|
19
|
+
|
|
20
|
+
// Using $derived would be preferred, but this helps avoid
|
|
21
|
+
// updates to every list item when selectedValue changes.
|
|
22
|
+
$effect(() => {
|
|
23
|
+
if (listContext.selectedValue === value && !selected) {
|
|
24
|
+
selected = true;
|
|
25
|
+
} else if (listContext.selectedValue !== value && selected) {
|
|
26
|
+
selected = false;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
let itemRef: HTMLDivElement;
|
|
31
|
+
|
|
32
|
+
export const click = () => {
|
|
33
|
+
itemRef?.click();
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const blur = () => {
|
|
37
|
+
itemRef?.blur();
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const focus = (options?: FocusOptions) => {
|
|
41
|
+
itemRef?.focus(options);
|
|
42
|
+
};
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<!-- svelte-ignore a11y_role_supports_aria_props -->
|
|
46
|
+
<div
|
|
47
|
+
aria-selected={selected}
|
|
48
|
+
bind:this={itemRef}
|
|
49
|
+
class={['sterling-list-item', _class]}
|
|
50
|
+
class:disabled={disabled || listContext.disabled}
|
|
51
|
+
class:horizontal={listContext.horizontal}
|
|
52
|
+
class:item-disabled={disabled}
|
|
53
|
+
class:list-disabled={listContext.disabled}
|
|
54
|
+
class:selected
|
|
55
|
+
data-value={value}
|
|
56
|
+
role="listitem"
|
|
57
|
+
{...rest}
|
|
58
|
+
>
|
|
59
|
+
{#if children}
|
|
60
|
+
{@render children()}
|
|
61
|
+
{:else}
|
|
62
|
+
{value}
|
|
63
|
+
{/if}
|
|
64
|
+
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
type Props = HTMLAttributes<HTMLDivElement> & {
|
|
3
|
+
disabled?: boolean | null;
|
|
4
|
+
value?: string;
|
|
5
|
+
};
|
|
6
|
+
declare const ListItem: import("svelte").Component<Props, {
|
|
7
|
+
click: () => void;
|
|
8
|
+
blur: () => void;
|
|
9
|
+
focus: (options?: FocusOptions) => void;
|
|
10
|
+
}, "">;
|
|
11
|
+
type ListItem = ReturnType<typeof ListItem>;
|
|
12
|
+
export default ListItem;
|
package/dist/Menu.svelte
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<svelte:options runes={true} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
import { getContext } from 'svelte';
|
|
5
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
6
|
+
import { slide, type SlideParams, type TransitionConfig } from 'svelte/transition';
|
|
7
|
+
import { MENU_ITEM_CONTEXT_KEY } from './MenuItem.constants';
|
|
8
|
+
import type { MenuItemContext } from './MenuItem.types';
|
|
9
|
+
import { isElementEnabledMenuItem, isElementMenuItem } from './MenuItem.utils';
|
|
10
|
+
import { prefersReducedMotion } from './mediaQueries/prefersReducedMotion';
|
|
11
|
+
|
|
12
|
+
type Props = HTMLAttributes<HTMLDivElement>;
|
|
13
|
+
|
|
14
|
+
let { children, class: _class, ...rest }: Props = $props();
|
|
15
|
+
|
|
16
|
+
let menuRef: HTMLDivElement;
|
|
17
|
+
let menuItemsRef: HTMLDivElement;
|
|
18
|
+
|
|
19
|
+
const noSlide = (node: Element, params?: SlideParams): TransitionConfig => {
|
|
20
|
+
return { delay: 0, duration: 0 };
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
let slideMotion = $derived(!$prefersReducedMotion ? slide : noSlide);
|
|
24
|
+
|
|
25
|
+
const { rootValue = undefined } = getContext<MenuItemContext>(MENU_ITEM_CONTEXT_KEY);
|
|
26
|
+
|
|
27
|
+
const isElementInThisMenu = (candidate: Element) => {
|
|
28
|
+
return candidate && candidate.closest('[role="menu"]') === menuRef;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
//#region focus
|
|
32
|
+
export const focus = (options?: FocusOptions) => {
|
|
33
|
+
menuRef?.focus(options);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const blur = () => {
|
|
37
|
+
menuRef?.blur();
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const focusFirstMenuItem = () => {
|
|
41
|
+
let candidate: Element | undefined | null = menuItemsRef?.firstElementChild;
|
|
42
|
+
while (candidate && !isElementEnabledMenuItem(candidate)) {
|
|
43
|
+
candidate = candidate.nextElementSibling;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
(candidate as HTMLElement)?.focus({ preventScroll: true });
|
|
47
|
+
return !!candidate;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const focusPreviousMenuItem = () => {
|
|
51
|
+
let candidate = document.activeElement;
|
|
52
|
+
|
|
53
|
+
if (candidate && isElementMenuItem(candidate) && isElementInThisMenu(candidate)) {
|
|
54
|
+
candidate = menuItemsRef?.previousElementSibling;
|
|
55
|
+
while (candidate && !isElementEnabledMenuItem(candidate)) {
|
|
56
|
+
candidate = candidate.previousElementSibling;
|
|
57
|
+
}
|
|
58
|
+
(candidate as HTMLElement)?.focus();
|
|
59
|
+
}
|
|
60
|
+
return !!candidate;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const focusNextMenuItem = () => {
|
|
64
|
+
let candidate = document.activeElement;
|
|
65
|
+
|
|
66
|
+
if (candidate && isElementMenuItem(candidate) && isElementInThisMenu(candidate)) {
|
|
67
|
+
candidate = menuItemsRef?.nextElementSibling;
|
|
68
|
+
while (candidate && !isElementEnabledMenuItem(candidate)) {
|
|
69
|
+
candidate = candidate.nextElementSibling;
|
|
70
|
+
}
|
|
71
|
+
(candidate as HTMLElement)?.focus();
|
|
72
|
+
}
|
|
73
|
+
return !!candidate;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const focusLastMenuItem = () => {
|
|
77
|
+
let candidate: Element | undefined | null = menuItemsRef?.lastElementChild;
|
|
78
|
+
while (candidate && !isElementEnabledMenuItem(candidate)) {
|
|
79
|
+
candidate = candidate.previousElementSibling;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
(candidate as HTMLElement)?.focus({ preventScroll: true });
|
|
83
|
+
return !!candidate;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
//#endregion
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
<div
|
|
90
|
+
bind:this={menuRef}
|
|
91
|
+
class={['sterling-menu', _class]}
|
|
92
|
+
role="menu"
|
|
93
|
+
class:open
|
|
94
|
+
data-root-value={rootValue}
|
|
95
|
+
tabindex="-1"
|
|
96
|
+
in:slideMotion|global={{ duration: 300 }}
|
|
97
|
+
out:slideMotion|global={{ duration: 100 }}
|
|
98
|
+
{...rest}
|
|
99
|
+
>
|
|
100
|
+
<div bind:this={menuItemsRef} class="menu-items">
|
|
101
|
+
{#if children}
|
|
102
|
+
{@render children()}
|
|
103
|
+
{/if}
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
type Props = HTMLAttributes<HTMLDivElement>;
|
|
3
|
+
declare const Menu: import("svelte").Component<Props, {
|
|
4
|
+
focus: (options?: FocusOptions) => void;
|
|
5
|
+
blur: () => void;
|
|
6
|
+
focusFirstMenuItem: () => boolean;
|
|
7
|
+
focusPreviousMenuItem: () => boolean;
|
|
8
|
+
focusNextMenuItem: () => boolean;
|
|
9
|
+
focusLastMenuItem: () => boolean;
|
|
10
|
+
}, "">;
|
|
11
|
+
type Menu = ReturnType<typeof Menu>;
|
|
12
|
+
export default Menu;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const MENU_BAR_CONTEXT_KEY = "sterlingMenuBar";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const MENU_BAR_CONTEXT_KEY = 'sterlingMenuBar';
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
<svelte:options runes={true} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
import { setContext } from 'svelte';
|
|
5
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
6
|
+
import { clickOutside } from './actions/clickOutside';
|
|
7
|
+
import { MENU_BAR_CONTEXT_KEY } from './MenuBar.constants';
|
|
8
|
+
import type { MenuBarContext } from './MenuBar.types';
|
|
9
|
+
import { MENU_ITEM_CONTEXT_KEY } from './MenuItem.constants';
|
|
10
|
+
import type { MenuItemContext } from './MenuItem.types';
|
|
11
|
+
import { isElementEnabledMenuItem } from './MenuItem.utils';
|
|
12
|
+
|
|
13
|
+
const uuid = $props.id();
|
|
14
|
+
|
|
15
|
+
type Props = HTMLAttributes<HTMLDivElement> & {
|
|
16
|
+
onClose?: (value: string) => void;
|
|
17
|
+
onOpen?: (value: string) => void;
|
|
18
|
+
onSelect?: (value: string) => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
let { class: _class, children, onClose, onOpen, onSelect, ...rest }: Props = $props();
|
|
22
|
+
|
|
23
|
+
const rootValue = `MenuBar-${uuid}`;
|
|
24
|
+
let openValues: string[] = $state([]);
|
|
25
|
+
let prevOpenValue: string | undefined = $state();
|
|
26
|
+
|
|
27
|
+
let menuBarRef: HTMLDivElement;
|
|
28
|
+
|
|
29
|
+
$effect(() => {
|
|
30
|
+
prevOpenValue = openValues[0];
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Restore focus to the last open menu bar item when it closes
|
|
34
|
+
$effect(() => {
|
|
35
|
+
if (openValues.length === 0 && prevOpenValue !== undefined) {
|
|
36
|
+
const candidate = menuBarRef.querySelector(`[data-value="${prevOpenValue}"]`);
|
|
37
|
+
(candidate as HTMLElement)?.focus();
|
|
38
|
+
prevOpenValue = undefined;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export const blur = () => {
|
|
43
|
+
menuBarRef?.blur();
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const focus = (options?: FocusOptions) => {
|
|
47
|
+
menuBarRef?.focus(options);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const getOpenMenuBarItem = () => {
|
|
51
|
+
const value = openValues[0];
|
|
52
|
+
if (value) {
|
|
53
|
+
return menuBarRef.querySelector(`[data-value="${value}"]`);
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const openPreviousMenuBarItem = () => {
|
|
59
|
+
const openItem = getOpenMenuBarItem() || menuBarRef.firstElementChild;
|
|
60
|
+
let candidate = openItem?.previousElementSibling || menuBarRef.lastElementChild;
|
|
61
|
+
|
|
62
|
+
while (candidate && !isElementEnabledMenuItem(candidate)) {
|
|
63
|
+
candidate = candidate.previousElementSibling || menuBarRef.lastElementChild;
|
|
64
|
+
if (candidate === openItem) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!candidate) {
|
|
70
|
+
candidate = menuBarRef.lastElementChild;
|
|
71
|
+
candidate = candidate && isElementEnabledMenuItem(candidate) ? candidate : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
(candidate as HTMLElement)?.click();
|
|
75
|
+
return !!candidate;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const openNextMenuBarItem = () => {
|
|
79
|
+
const openItem = getOpenMenuBarItem() || menuBarRef.lastElementChild;
|
|
80
|
+
let candidate = openItem?.nextElementSibling || menuBarRef.firstElementChild;
|
|
81
|
+
|
|
82
|
+
while (candidate && !isElementEnabledMenuItem(candidate)) {
|
|
83
|
+
candidate = candidate.nextElementSibling || menuBarRef.firstElementChild;
|
|
84
|
+
if (candidate === openItem) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!candidate) {
|
|
90
|
+
candidate = menuBarRef.firstElementChild;
|
|
91
|
+
candidate = candidate && isElementEnabledMenuItem(candidate) ? candidate : null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
(candidate as HTMLElement)?.click();
|
|
95
|
+
return !!candidate;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const closeAllMenus = () => {
|
|
99
|
+
openValues = [];
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const onClickOutside = (event: MouseEvent) => {
|
|
103
|
+
let element: HTMLElement | null = event.target as HTMLElement;
|
|
104
|
+
while (element) {
|
|
105
|
+
if (element.getAttribute('data-root-value') === rootValue) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
element = element.parentElement;
|
|
109
|
+
}
|
|
110
|
+
closeAllMenus?.();
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
let menuBarContext: MenuBarContext = { openPreviousMenuBarItem, openNextMenuBarItem };
|
|
114
|
+
|
|
115
|
+
setContext<MenuBarContext>(MENU_BAR_CONTEXT_KEY, menuBarContext);
|
|
116
|
+
|
|
117
|
+
let menuItemContext: MenuItemContext = {
|
|
118
|
+
isMenuBarItem: true,
|
|
119
|
+
depth: 0,
|
|
120
|
+
get openValues() {
|
|
121
|
+
return openValues;
|
|
122
|
+
},
|
|
123
|
+
set openValues(value: string[]) {
|
|
124
|
+
openValues = value;
|
|
125
|
+
},
|
|
126
|
+
rootValue,
|
|
127
|
+
onClose,
|
|
128
|
+
onOpen,
|
|
129
|
+
onSelect
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
setContext<MenuItemContext>(MENU_ITEM_CONTEXT_KEY, menuItemContext);
|
|
133
|
+
</script>
|
|
134
|
+
|
|
135
|
+
<div
|
|
136
|
+
bind:this={menuBarRef}
|
|
137
|
+
class={['sterling-menu-bar', _class]}
|
|
138
|
+
role="menubar"
|
|
139
|
+
tabindex="-1"
|
|
140
|
+
{...rest}
|
|
141
|
+
use:clickOutside={{ onclickoutside: onClickOutside }}
|
|
142
|
+
>
|
|
143
|
+
{@render children?.()}
|
|
144
|
+
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
type Props = HTMLAttributes<HTMLDivElement> & {
|
|
3
|
+
onClose?: (value: string) => void;
|
|
4
|
+
onOpen?: (value: string) => void;
|
|
5
|
+
onSelect?: (value: string) => void;
|
|
6
|
+
};
|
|
7
|
+
declare const MenuBar: import("svelte").Component<Props, {
|
|
8
|
+
blur: () => void;
|
|
9
|
+
focus: (options?: FocusOptions) => void;
|
|
10
|
+
}, "">;
|
|
11
|
+
type MenuBar = ReturnType<typeof MenuBar>;
|
|
12
|
+
export default MenuBar;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|