@txstate-mws/svelte-components 1.6.9 → 1.6.11
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/actions/glue.js +11 -6
- package/dist/components/MultiSelect.svelte +36 -3
- package/dist/components/MultiSelect.svelte.d.ts +8 -3
- package/dist/components/PopupMenu.svelte +94 -47
- package/dist/components/PopupMenu.svelte.d.ts +6 -6
- package/dist/types/PopupMenu.d.ts +6 -2
- package/package.json +1 -1
package/dist/actions/glue.js
CHANGED
|
@@ -20,17 +20,18 @@ export function glue(el, { target, align = 'auto', cover = false, gap = 0, adjus
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
let lastrect;
|
|
23
|
+
let timer;
|
|
23
24
|
function reposition() {
|
|
24
25
|
const fixedParent = fixedContainingBlock(el);
|
|
25
26
|
let fixedRect = { left: 0, top: 0, right: 0, bottom: 0 };
|
|
26
27
|
if (fixedParent) {
|
|
27
28
|
const tmpRect = fixedParent.getBoundingClientRect();
|
|
28
|
-
fixedRect = { left: tmpRect.left, right: document.documentElement.clientWidth - tmpRect.right, top: tmpRect.top, bottom:
|
|
29
|
+
fixedRect = { left: tmpRect.left, right: document.documentElement.clientWidth - tmpRect.right, top: tmpRect.top, bottom: document.documentElement.clientHeight - tmpRect.bottom };
|
|
29
30
|
}
|
|
30
31
|
if (!target)
|
|
31
32
|
return;
|
|
32
33
|
const tmpRect = target.getBoundingClientRect();
|
|
33
|
-
const rect = { left: tmpRect.left - fixedRect.left, right: document.documentElement.clientWidth - tmpRect.right - fixedRect.right, top: tmpRect.top - fixedRect.top, bottom:
|
|
34
|
+
const rect = { left: tmpRect.left - fixedRect.left, right: document.documentElement.clientWidth - tmpRect.right - fixedRect.right, top: tmpRect.top - fixedRect.top, bottom: document.documentElement.clientHeight - tmpRect.bottom - fixedRect.bottom, width: tmpRect.width, height: tmpRect.height };
|
|
34
35
|
if (equal(rect, lastrect))
|
|
35
36
|
return;
|
|
36
37
|
let autoalign = align;
|
|
@@ -41,7 +42,8 @@ export function glue(el, { target, align = 'auto', cover = false, gap = 0, adjus
|
|
|
41
42
|
}
|
|
42
43
|
const targetHeight = rect.height;
|
|
43
44
|
if (autoalign === 'bottomleft') {
|
|
44
|
-
|
|
45
|
+
cancelAnimationFrame(timer);
|
|
46
|
+
timer = requestAnimationFrame(() => {
|
|
45
47
|
el.style.top = `${rect.top + (cover ? 0 : targetHeight) + gap}px`;
|
|
46
48
|
el.style.left = `${rect.left}px`;
|
|
47
49
|
el.style.removeProperty('bottom');
|
|
@@ -52,7 +54,8 @@ export function glue(el, { target, align = 'auto', cover = false, gap = 0, adjus
|
|
|
52
54
|
halign = 'left';
|
|
53
55
|
}
|
|
54
56
|
else if (autoalign === 'bottomright') {
|
|
55
|
-
|
|
57
|
+
cancelAnimationFrame(timer);
|
|
58
|
+
timer = requestAnimationFrame(() => {
|
|
56
59
|
el.style.top = `${rect.top + (cover ? 0 : targetHeight) + gap}px`;
|
|
57
60
|
el.style.removeProperty('left');
|
|
58
61
|
el.style.removeProperty('bottom');
|
|
@@ -63,7 +66,8 @@ export function glue(el, { target, align = 'auto', cover = false, gap = 0, adjus
|
|
|
63
66
|
halign = 'right';
|
|
64
67
|
}
|
|
65
68
|
else if (autoalign === 'topleft') {
|
|
66
|
-
|
|
69
|
+
cancelAnimationFrame(timer);
|
|
70
|
+
timer = requestAnimationFrame(() => {
|
|
67
71
|
el.style.removeProperty('top');
|
|
68
72
|
el.style.left = `${rect.left}px`;
|
|
69
73
|
el.style.bottom = `${rect.bottom + (cover ? 0 : targetHeight) + gap}px`;
|
|
@@ -74,7 +78,8 @@ export function glue(el, { target, align = 'auto', cover = false, gap = 0, adjus
|
|
|
74
78
|
halign = 'left';
|
|
75
79
|
}
|
|
76
80
|
else if (autoalign === 'topright') {
|
|
77
|
-
|
|
81
|
+
cancelAnimationFrame(timer);
|
|
82
|
+
timer = requestAnimationFrame(() => {
|
|
78
83
|
el.style.removeProperty('top');
|
|
79
84
|
el.style.removeProperty('left');
|
|
80
85
|
el.style.bottom = `${rect.bottom + (cover ? 0 : targetHeight) + gap}px`;
|
|
@@ -25,7 +25,7 @@ export let menuClass = '';
|
|
|
25
25
|
export let menuItemClass = '';
|
|
26
26
|
export let menuItemHilitedClass = '';
|
|
27
27
|
export let inputClass = '';
|
|
28
|
-
export let
|
|
28
|
+
export let menuCategoryClass = '';
|
|
29
29
|
/** The maximum number of selections allowed before making new selections is disabled. Default of 0 is unlimited. */
|
|
30
30
|
export let maxSelections = 0;
|
|
31
31
|
/** Consuming components may need to make decisions about what to display or return as options in the popup based
|
|
@@ -41,6 +41,13 @@ export let usePortal = undefined;
|
|
|
41
41
|
export let descid = undefined;
|
|
42
42
|
/** You can define your own PopupMenu and pass for that to be used or accept DefaultPopupMenu. */
|
|
43
43
|
export let PopupMenu = DefaultPopupMenu;
|
|
44
|
+
/** Add a Delete All button to clear all selected items */
|
|
45
|
+
export let includeDeleteAll = false;
|
|
46
|
+
/** Show a confirmation message before clearing all selections */
|
|
47
|
+
export let confirmDelete = undefined;
|
|
48
|
+
/** A function that can be used to customize the labels on the selected item pills. Default is a function
|
|
49
|
+
* that takes a PopupMenuItem and returns its label, if available, or its value when no label is provided. */
|
|
50
|
+
export let selectedItemLabel = (item) => item.label || item.value;
|
|
44
51
|
let menushown;
|
|
45
52
|
let loading = false;
|
|
46
53
|
let options = [];
|
|
@@ -137,6 +144,20 @@ function inputblur() {
|
|
|
137
144
|
active = false;
|
|
138
145
|
dispatch('blur');
|
|
139
146
|
}
|
|
147
|
+
function removeAllSelections() {
|
|
148
|
+
selected = [];
|
|
149
|
+
dispatch('change', selected);
|
|
150
|
+
}
|
|
151
|
+
function confirmRemoveAllSelections() {
|
|
152
|
+
if (confirmDelete) {
|
|
153
|
+
if (confirm(confirmDelete)) {
|
|
154
|
+
removeAllSelections();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
removeAllSelections();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
140
161
|
let popuphilited;
|
|
141
162
|
$: if (popuphilited != null)
|
|
142
163
|
hilitedpill = undefined;
|
|
@@ -161,6 +182,13 @@ onMount(() => {
|
|
|
161
182
|
</script>
|
|
162
183
|
|
|
163
184
|
<div class="multiselect" class:disabled role="combobox" aria-controls={menushown ? menuid : undefined} aria-expanded={menushown}>
|
|
185
|
+
{#if includeDeleteAll && selected.length}
|
|
186
|
+
<div class="delete-all-container">
|
|
187
|
+
<button on:click|preventDefault|stopPropagation={confirmRemoveAllSelections}>
|
|
188
|
+
<slot name="deleteall">Delete All <ScreenReaderOnly>{selected.length} items</ScreenReaderOnly></slot>
|
|
189
|
+
</button>
|
|
190
|
+
</div>
|
|
191
|
+
{/if}
|
|
164
192
|
{#if selected.length}
|
|
165
193
|
<div class="multiselect-selected" role="listbox" aria-label="selected options">
|
|
166
194
|
{#each selected as option, i (option.value)}
|
|
@@ -168,7 +196,7 @@ onMount(() => {
|
|
|
168
196
|
<div id={id + option.value} role="option" tabindex="-1" class="multiselect-pill" class:hilited={hilitedpill === option.value}
|
|
169
197
|
on:click|preventDefault|stopPropagation={() => { !disabled && removeSelection(option, i, 1) }} on:mousedown={e => { disabled && e.preventDefault() }}
|
|
170
198
|
aria-selected="true" aria-posinset={i + 1} aria-setsize={selected.length}>
|
|
171
|
-
{option
|
|
199
|
+
{selectedItemLabel(option)}
|
|
172
200
|
<i aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256"><path fill="currentColor" d="M205.66 194.34a8 8 0 0 1-11.32 11.32L128 139.31l-66.34 66.35a8 8 0 0 1-11.32-11.32L116.69 128L50.34 61.66a8 8 0 0 1 11.32-11.32L128 116.69l66.34-66.35a8 8 0 0 1 11.32 11.32L139.31 128Z"/></svg></i>
|
|
173
201
|
<ScreenReaderOnly>, click to deselect</ScreenReaderOnly>
|
|
174
202
|
</div>
|
|
@@ -188,7 +216,7 @@ onMount(() => {
|
|
|
188
216
|
|
|
189
217
|
<svelte:component this={PopupMenu} bind:menushown bind:menuid bind:hilited={popuphilited} bind:value={popupvalue} align='bottomleft'
|
|
190
218
|
{usePortal} {loading} {emptyText} {gap}
|
|
191
|
-
{menuContainerClass} {menuClass} {menuItemClass} {menuItemHilitedClass} {
|
|
219
|
+
{menuContainerClass} {menuClass} {menuItemClass} {menuItemHilitedClass} {menuCategoryClass}
|
|
192
220
|
items={options} buttonelement={inputelement}
|
|
193
221
|
on:change={addSelection}/>
|
|
194
222
|
|
|
@@ -257,4 +285,9 @@ onMount(() => {
|
|
|
257
285
|
border: var(--multiselect-pill-selected-border, 1px solid transparent);
|
|
258
286
|
color: var(--multiselect-pill-selected-text, white);
|
|
259
287
|
}
|
|
288
|
+
.delete-all-container {
|
|
289
|
+
width: 100%;
|
|
290
|
+
display: flex;
|
|
291
|
+
justify-content: flex-end;
|
|
292
|
+
}
|
|
260
293
|
</style>
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { SvelteComponentTyped } from "svelte";
|
|
2
2
|
import DefaultPopupMenu from './PopupMenu.svelte';
|
|
3
|
-
import type { PopupMenuItem
|
|
3
|
+
import type { PopupMenuItem } from '../types';
|
|
4
4
|
declare const __propDef: {
|
|
5
5
|
props: {
|
|
6
6
|
name: string;
|
|
7
7
|
/**
|
|
8
8
|
* Function to pass to the component that tells it how to use the text in the text input to determine what
|
|
9
9
|
* `PopupMenuItem[]` should be displayed in the `PopupMenu`. Items already 'selected' from the popup menu will
|
|
10
|
-
* be tracked and automatically filtered from the popup if returned as one of the `PopupMenuItem[]` by `getOptions`. */ getOptions: (search: string) => Promise<
|
|
10
|
+
* be tracked and automatically filtered from the popup if returned as one of the `PopupMenuItem[]` by `getOptions`. */ getOptions: (search: string) => Promise<PopupMenuItem[]> | PopupMenuItem[];
|
|
11
11
|
id?: string;
|
|
12
12
|
disabled?: boolean;
|
|
13
13
|
menuContainerClass?: string;
|
|
@@ -15,7 +15,7 @@ declare const __propDef: {
|
|
|
15
15
|
menuItemClass?: string;
|
|
16
16
|
menuItemHilitedClass?: string;
|
|
17
17
|
inputClass?: string;
|
|
18
|
-
|
|
18
|
+
menuCategoryClass?: string;
|
|
19
19
|
/** The maximum number of selections allowed before making new selections is disabled. Default of 0 is unlimited. */ maxSelections?: number;
|
|
20
20
|
/** Consuming components may need to make decisions about what to display or return as options in the popup based
|
|
21
21
|
* on what's already selected. This component property can be used to both pass an initial set of selections but
|
|
@@ -26,6 +26,10 @@ declare const __propDef: {
|
|
|
26
26
|
usePortal?: HTMLElement | true | undefined;
|
|
27
27
|
descid?: string | undefined;
|
|
28
28
|
/** You can define your own PopupMenu and pass for that to be used or accept DefaultPopupMenu. */ PopupMenu?: typeof DefaultPopupMenu;
|
|
29
|
+
/** Add a Delete All button to clear all selected items */ includeDeleteAll?: boolean;
|
|
30
|
+
/** Show a confirmation message before clearing all selections */ confirmDelete?: string | undefined;
|
|
31
|
+
/** A function that can be used to customize the labels on the selected item pills. Default is a function
|
|
32
|
+
* that takes a PopupMenuItem and returns its label, if available, or its value when no label is provided. */ selectedItemLabel?: (item: PopupMenuItem) => string;
|
|
29
33
|
};
|
|
30
34
|
events: {
|
|
31
35
|
change: CustomEvent<any>;
|
|
@@ -35,6 +39,7 @@ declare const __propDef: {
|
|
|
35
39
|
[evt: string]: CustomEvent<any>;
|
|
36
40
|
};
|
|
37
41
|
slots: {
|
|
42
|
+
deleteall: {};
|
|
38
43
|
default: {};
|
|
39
44
|
};
|
|
40
45
|
};
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
bound to `buttonelement`. It can also be controlled from the parent, by binding the `menushown` boolean.
|
|
4
4
|
-->
|
|
5
5
|
<script lang="ts">import { createEventDispatcher, onDestroy, tick } from 'svelte';
|
|
6
|
-
import {
|
|
6
|
+
import { groupby, isNull, randomid } from 'txstate-utils';
|
|
7
7
|
import { Store } from '@txstate-mws/svelte-store';
|
|
8
8
|
import { glue, portal } from '../actions';
|
|
9
9
|
import ScreenReaderOnly from './ScreenReaderOnly.svelte';
|
|
@@ -60,22 +60,40 @@ export let menuClass = '';
|
|
|
60
60
|
export let menuItemClass = '';
|
|
61
61
|
export let menuItemHilitedClass = '';
|
|
62
62
|
export let menuItemSelectedClass = '';
|
|
63
|
-
export let
|
|
63
|
+
export let menuCategoryClass = '';
|
|
64
64
|
export let hideSelectedIndicator = false;
|
|
65
65
|
export let hideEmptyText = false;
|
|
66
|
+
/** If true, the role for the popup menu will be 'menu' instead of the default 'listbox' value */
|
|
66
67
|
export let usemenurole = false;
|
|
67
68
|
let menuelement;
|
|
68
69
|
const itemelements = [];
|
|
69
70
|
let firstactive = 0;
|
|
70
71
|
let lastactive = items.length - 1;
|
|
71
|
-
$: hasMeaningfulItems = showSelected ? !!items.
|
|
72
|
+
$: hasMeaningfulItems = showSelected ? !!items.length : !!items.filter(itm => itm.value !== value).length;
|
|
72
73
|
function hiddenItem(item) {
|
|
73
74
|
return !!item && !showSelected && item.value === value;
|
|
74
75
|
}
|
|
76
|
+
$: grouped = groupby(items, item => item.group ?? 'default-group');
|
|
77
|
+
let itemsOrderedByGroup = [];
|
|
78
|
+
function getItemIndex(item) {
|
|
79
|
+
return itemsOrderedByGroup.findIndex(itm => {
|
|
80
|
+
// need to handle default group options
|
|
81
|
+
if (itm.value === item.value) {
|
|
82
|
+
if (item.group === 'default-group') {
|
|
83
|
+
return isNull(itm.group);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
return item.group === itm.group;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
75
92
|
async function reactToItems(..._) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
93
|
+
itemsOrderedByGroup = Object.values(grouped).flat();
|
|
94
|
+
firstactive = itemsOrderedByGroup.findIndex(itm => 'value' in itm && !itm.disabled && !hiddenItem(itm));
|
|
95
|
+
lastactive = itemsOrderedByGroup.length - [...itemsOrderedByGroup].reverse().findIndex(itm => 'value' in itm && !itm.disabled && !hiddenItem(itm)) - 1;
|
|
96
|
+
if (hilited && (itemsOrderedByGroup[hilited])?.disabled)
|
|
79
97
|
hilited = firstactive;
|
|
80
98
|
}
|
|
81
99
|
$: void reactToItems(items, value);
|
|
@@ -107,9 +125,7 @@ $: void reactToMenuShown(menushown);
|
|
|
107
125
|
function move(idx) {
|
|
108
126
|
if (!menushown)
|
|
109
127
|
return;
|
|
110
|
-
|
|
111
|
-
idx++;
|
|
112
|
-
if (items[idx] == null || items[idx]?.disabled)
|
|
128
|
+
if (itemsOrderedByGroup[idx] == null || (itemsOrderedByGroup[idx])?.disabled)
|
|
113
129
|
return;
|
|
114
130
|
hilited = Math.max(firstactive, Math.min(lastactive, idx));
|
|
115
131
|
itemelements[hilited].scrollIntoView({ block: 'center' });
|
|
@@ -125,7 +141,7 @@ function onkeydown(e) {
|
|
|
125
141
|
e.preventDefault();
|
|
126
142
|
if (menushown) {
|
|
127
143
|
let i = (hilited ?? firstactive - 1) + 1;
|
|
128
|
-
while (
|
|
144
|
+
while (itemsOrderedByGroup[i] && !isSelectable(itemsOrderedByGroup[i]))
|
|
129
145
|
i++;
|
|
130
146
|
move(i);
|
|
131
147
|
}
|
|
@@ -138,7 +154,7 @@ function onkeydown(e) {
|
|
|
138
154
|
e.preventDefault();
|
|
139
155
|
if (menushown) {
|
|
140
156
|
let i = (hilited ?? lastactive + 1) - 1;
|
|
141
|
-
while (
|
|
157
|
+
while (itemsOrderedByGroup[i] && !isSelectable(itemsOrderedByGroup[i]))
|
|
142
158
|
i--;
|
|
143
159
|
move(i);
|
|
144
160
|
}
|
|
@@ -164,8 +180,8 @@ function onkeydown(e) {
|
|
|
164
180
|
if (menushown) {
|
|
165
181
|
menushown = false;
|
|
166
182
|
if (typeof hilited !== 'undefined') {
|
|
167
|
-
value =
|
|
168
|
-
dispatch('change',
|
|
183
|
+
value = (itemsOrderedByGroup[hilited])?.value;
|
|
184
|
+
dispatch('change', itemsOrderedByGroup[hilited]);
|
|
169
185
|
}
|
|
170
186
|
}
|
|
171
187
|
else {
|
|
@@ -180,8 +196,8 @@ function onkeydown(e) {
|
|
|
180
196
|
if (typeof hilited !== 'undefined') {
|
|
181
197
|
e.preventDefault();
|
|
182
198
|
menushown = false;
|
|
183
|
-
value =
|
|
184
|
-
dispatch('change',
|
|
199
|
+
value = (itemsOrderedByGroup[hilited])?.value;
|
|
200
|
+
dispatch('change', itemsOrderedByGroup[hilited]);
|
|
185
201
|
}
|
|
186
202
|
else {
|
|
187
203
|
if (buttonelement.tagName !== 'INPUT') {
|
|
@@ -247,6 +263,9 @@ function reactToButtonElement(buttonelement) {
|
|
|
247
263
|
if (buttonelement) {
|
|
248
264
|
buttonelement.setAttribute('aria-haspopup', (usemenurole ? 'true' : 'listbox'));
|
|
249
265
|
buttonelement.setAttribute('aria-expanded', menushown ? 'true' : 'false');
|
|
266
|
+
if (!usemenurole) {
|
|
267
|
+
buttonelement.setAttribute('role', 'combobox');
|
|
268
|
+
}
|
|
250
269
|
buttonelement.addEventListener('click', onbuttonclick);
|
|
251
270
|
buttonelement.addEventListener('keydown', onkeydown);
|
|
252
271
|
buttonelement.addEventListener('blur', onblur);
|
|
@@ -255,10 +274,10 @@ function reactToButtonElement(buttonelement) {
|
|
|
255
274
|
for (const parent of [...parents, undefined]) {
|
|
256
275
|
const observer = new IntersectionObserver((entries) => {
|
|
257
276
|
for (const entry of entries) {
|
|
258
|
-
if (entry.intersectionRatio < 1)
|
|
277
|
+
if (entry.intersectionRatio < 0.1)
|
|
259
278
|
menushown = false;
|
|
260
279
|
}
|
|
261
|
-
}, { root: parent, threshold: 1 });
|
|
280
|
+
}, { root: parent, threshold: 0.1 });
|
|
262
281
|
observer.observe(buttonelement);
|
|
263
282
|
observers.push(observer);
|
|
264
283
|
}
|
|
@@ -278,41 +297,62 @@ $: hasSelected = showSelected && !hideSelectedIndicator && items.some(itm => 'va
|
|
|
278
297
|
</script>
|
|
279
298
|
|
|
280
299
|
{#if menushown && !loading && (hasMeaningfulItems || !hideEmptyText)}
|
|
281
|
-
<div use:portal={usePortal === true ? undefined : (usePortal || null)}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
{:else
|
|
304
|
-
|
|
305
|
-
|
|
300
|
+
<div use:portal={usePortal === true ? undefined : (usePortal || null)} use:glue={{ target: buttonelement, align, cover, gap, adjustparentheight, store: computedalign }} class={menuContainerClass}>
|
|
301
|
+
<ul bind:this={menuelement} id={menuid} role={usemenurole ? 'menu' : 'listbox'} tabindex="-1" style={width ? `width: ${width}` : ''} class={menuClass} class:hasSelected class:defaultmenu={!menuClass && !menuContainerClass} on:keydown={onkeydown}>
|
|
302
|
+
{#each Object.entries(grouped) as [group, groupitems]}
|
|
303
|
+
{#if group === 'default-group'}
|
|
304
|
+
{#each groupitems as item, i (item.value)}
|
|
305
|
+
{@const itemIndex = getItemIndex(item)}
|
|
306
|
+
{#if (showSelected || item.value !== value)}
|
|
307
|
+
<li
|
|
308
|
+
id={`${menuid}-${itemIndex}`}
|
|
309
|
+
bind:this={itemelements[itemIndex]}
|
|
310
|
+
class={`${menuItemClass} ${itemIndex === hilited ? menuItemHilitedClass : ''} ${value === item.value ? menuItemSelectedClass : ''}`}
|
|
311
|
+
class:disabled={!!item.disabled}
|
|
312
|
+
class:hilited={!menuItemHilitedClass && itemIndex === hilited}
|
|
313
|
+
class:selected={showSelected && !hideSelectedIndicator && value === item.value}
|
|
314
|
+
on:click={onclick(item)}
|
|
315
|
+
role={usemenurole ? 'menuitem' : 'option'}
|
|
316
|
+
tabindex=-1
|
|
317
|
+
aria-selected={usemenurole ? undefined : (value === item.value)}
|
|
318
|
+
aria-disabled={item.disabled}
|
|
319
|
+
><slot {item} label={item.label || item.value} hilited={itemIndex === hilited} selected={value === item.value}>{item.label || item.value}</slot></li>
|
|
320
|
+
{/if}
|
|
321
|
+
{/each}
|
|
322
|
+
{:else}
|
|
323
|
+
<li role="group" aria-labelledby="{menuid}-{group}" class="group">
|
|
324
|
+
<span id="{menuid}-{group}" class="category {menuCategoryClass}">{group}</span>
|
|
325
|
+
<ul role="presentation">
|
|
326
|
+
{#each groupitems as item, i (item.value)}
|
|
327
|
+
{@const itemIndex = getItemIndex(item)}
|
|
328
|
+
{#if (showSelected || item.value !== value)}
|
|
329
|
+
<li
|
|
330
|
+
id={`${menuid}-${itemIndex}`}
|
|
331
|
+
bind:this={itemelements[itemIndex]}
|
|
332
|
+
class={`${menuItemClass} ${itemIndex === hilited ? menuItemHilitedClass : ''} ${value === item.value ? menuItemSelectedClass : ''}`}
|
|
333
|
+
class:disabled={!!item.disabled}
|
|
334
|
+
class:hilited={!menuItemHilitedClass && itemIndex === hilited}
|
|
335
|
+
class:selected={showSelected && !hideSelectedIndicator && value === item.value}
|
|
336
|
+
on:click={onclick(item)}
|
|
337
|
+
role={usemenurole ? 'menuitem' : 'option'}
|
|
338
|
+
tabindex=-1
|
|
339
|
+
aria-selected={usemenurole ? undefined : (value === item.value)}
|
|
340
|
+
aria-disabled={item.disabled}
|
|
341
|
+
><slot {item} label={item.label || item.value} hilited={itemIndex === hilited} selected={value === item.value}>{item.label || item.value}</slot></li>
|
|
342
|
+
{/if}
|
|
343
|
+
{/each}
|
|
344
|
+
</ul>
|
|
345
|
+
</li>
|
|
306
346
|
{/if}
|
|
307
347
|
{/each}
|
|
308
348
|
{#if !hasMeaningfulItems}
|
|
309
349
|
<li role={usemenurole ? 'menuitem' : 'option'} class={`${menuItemClass} disabled`} aria-live="assertive" aria-selected={usemenurole ? undefined : false}>
|
|
310
350
|
<slot name="noresults">
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
351
|
+
{#if !emptyText}
|
|
352
|
+
<span aria-hidden="true">{'¯\\_(ツ)_/¯'}</span><ScreenReaderOnly>{emptyText || 'no results found'}</ScreenReaderOnly>
|
|
353
|
+
{:else}
|
|
354
|
+
{emptyText}
|
|
355
|
+
{/if}
|
|
316
356
|
</slot>
|
|
317
357
|
</li>
|
|
318
358
|
{/if}
|
|
@@ -389,4 +429,11 @@ $: hasSelected = showSelected && !hideSelectedIndicator && items.some(itm => 'va
|
|
|
389
429
|
border-right: 0.2em solid;
|
|
390
430
|
box-sizing: border-box;
|
|
391
431
|
}
|
|
432
|
+
span.category {
|
|
433
|
+
display: block;
|
|
434
|
+
font-weight: bold;
|
|
435
|
+
}
|
|
436
|
+
.group ul {
|
|
437
|
+
padding-inline-start: 20px;
|
|
438
|
+
}
|
|
392
439
|
</style>
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { SvelteComponentTyped } from "svelte";
|
|
2
2
|
import { Store } from '@txstate-mws/svelte-store';
|
|
3
3
|
import type { GlueAlignOpts, GlueAlignStore } from '../actions';
|
|
4
|
-
import type { PopupMenuItem
|
|
4
|
+
import type { PopupMenuItem } from '../types';
|
|
5
5
|
declare const __propDef: {
|
|
6
6
|
props: {
|
|
7
7
|
/** The DOM element that will act as the "button" for this menu. The menu will be placed next to the
|
|
8
8
|
button and be controlled by the button. Keyboard access will be enabled when the button has focus.
|
|
9
9
|
This component adds all appropriate attributes (tabindex, roles, and aria) automatically. */ buttonelement: HTMLElement;
|
|
10
|
-
/** The list of menu items to be shown. Parent may change this at any time based on user activity. */ items?:
|
|
10
|
+
/** The list of menu items to be shown. Parent may change this at any time based on user activity. */ items?: PopupMenuItem[];
|
|
11
11
|
menushown?: boolean;
|
|
12
12
|
value?: string | undefined;
|
|
13
13
|
/** Control where the menu will appear. Default is to use the current viewport to make a decision to
|
|
@@ -41,15 +41,15 @@ declare const __propDef: {
|
|
|
41
41
|
menuItemClass?: string;
|
|
42
42
|
menuItemHilitedClass?: string;
|
|
43
43
|
menuItemSelectedClass?: string;
|
|
44
|
-
|
|
44
|
+
menuCategoryClass?: string;
|
|
45
45
|
hideSelectedIndicator?: boolean;
|
|
46
46
|
hideEmptyText?: boolean;
|
|
47
|
-
usemenurole?: boolean;
|
|
47
|
+
/** If true, the role for the popup menu will be 'menu' instead of the default 'listbox' value */ usemenurole?: boolean;
|
|
48
48
|
};
|
|
49
49
|
slots: {
|
|
50
50
|
default: {
|
|
51
|
-
item:
|
|
52
|
-
label:
|
|
51
|
+
item: PopupMenuItem;
|
|
52
|
+
label: string;
|
|
53
53
|
hilited: boolean;
|
|
54
54
|
selected: boolean;
|
|
55
55
|
};
|
|
@@ -7,8 +7,12 @@ export interface PopupMenuItem {
|
|
|
7
7
|
/** Toggles the choice as one that's visible but is disabled and unclickable until conditions
|
|
8
8
|
* are met that would cause it to become enabled. */
|
|
9
9
|
disabled?: boolean;
|
|
10
|
+
/** Allows PopupMenuItems to be grouped by categories in the menu */
|
|
11
|
+
group?: string;
|
|
10
12
|
}
|
|
11
|
-
/** A PopupMenu list native type for displaying
|
|
13
|
+
/** A PopupMenu list native type for displaying
|
|
14
|
+
* @note Not currently in use
|
|
15
|
+
*/
|
|
12
16
|
export interface PopupMenuDivider {
|
|
13
17
|
/** Toggle for whether this instance of this type is shown. */
|
|
14
18
|
divider: boolean;
|
|
@@ -23,7 +27,7 @@ export interface PopupMenuDivider {
|
|
|
23
27
|
* @note Not currently in use. */
|
|
24
28
|
export interface PopupMenuGroup {
|
|
25
29
|
/** The label for the group. */
|
|
26
|
-
|
|
30
|
+
label: string;
|
|
27
31
|
/** The listing of options that would be listed under the parent `<ul>`. */
|
|
28
32
|
options: PopupMenuTypes[];
|
|
29
33
|
}
|