@txstate-mws/svelte-components 1.5.13 → 1.6.0
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 +1 -0
- package/dist/components/FocusLock.svelte +2 -2
- package/dist/components/FocusLock.svelte.d.ts +1 -1
- package/dist/components/MultiSelect.svelte +59 -55
- package/dist/components/MultiSelect.svelte.d.ts +2 -1
- package/dist/components/PopupMenu.svelte +4 -4
- package/dist/stores/toasts.d.ts +24 -0
- package/dist/stores/toasts.js +23 -0
- package/dist/util/repositioned.d.ts +3 -3
- package/dist/util/repositioned.js +3 -3
- package/dist/util/util.js +1 -0
- package/package.json +1 -1
package/dist/actions/glue.js
CHANGED
|
@@ -78,6 +78,7 @@ export function glue(el, { target, align = 'auto', cover = false, gap = 0, adjus
|
|
|
78
78
|
}
|
|
79
79
|
store?.update(v => ({ ...v, valign, halign }));
|
|
80
80
|
}
|
|
81
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
81
82
|
const { destroy: watchDestroy } = watchForMutations(reposition);
|
|
82
83
|
document.addEventListener('scroll', reposition, { capture: true });
|
|
83
84
|
return {
|
|
@@ -13,7 +13,7 @@ When they do, the escape event will be fired, allowing you to remove the dialog.
|
|
|
13
13
|
When it goes away, they'll be trapped inside the previous `FocusLock`, and so on.
|
|
14
14
|
-->
|
|
15
15
|
<script context="module">export const FocusLockStack = [];
|
|
16
|
-
const waitAtick = typeof requestAnimationFrame !== 'undefined' ? resolve => requestAnimationFrame(resolve) : resolve => resolve(0);
|
|
16
|
+
const waitAtick = typeof requestAnimationFrame !== 'undefined' ? (resolve) => requestAnimationFrame(resolve) : resolve => resolve(0);
|
|
17
17
|
</script>
|
|
18
18
|
|
|
19
19
|
<script>import { onMount, onDestroy, createEventDispatcher } from 'svelte';
|
|
@@ -53,7 +53,7 @@ onMount(async () => {
|
|
|
53
53
|
for (const entry of FocusLockStack)
|
|
54
54
|
entry.update();
|
|
55
55
|
if (typeof returnfocusto === 'undefined') {
|
|
56
|
-
returnfocusto = document.
|
|
56
|
+
returnfocusto = document.activeElement;
|
|
57
57
|
}
|
|
58
58
|
await sleep(1); // sapper resets focus on page load, so we'll wait a millisecond in case our modal is on screen at load
|
|
59
59
|
if (!lockelement)
|
|
@@ -11,7 +11,7 @@ declare const __propDef: {
|
|
|
11
11
|
hidefocus?: boolean | undefined;
|
|
12
12
|
hidefocuslabel?: string | undefined;
|
|
13
13
|
initialfocus?: string | undefined;
|
|
14
|
-
returnfocusto?: HTMLElement |
|
|
14
|
+
returnfocusto?: HTMLElement | undefined;
|
|
15
15
|
/** If you expect any popup menus to be added to the body, we need to know that they
|
|
16
16
|
are considered to be part of the focus lock, or else the modal will be dismissed
|
|
17
17
|
when the user clicks inside. Use commas to include multiple selectors. */ includeselector?: string | undefined;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
debounced callback on the contents of the text input.
|
|
8
8
|
-->
|
|
9
9
|
<script>import { createEventDispatcher, onMount } from 'svelte';
|
|
10
|
-
import { randomid, Cache } from 'txstate-utils';
|
|
10
|
+
import { randomid, Cache, isNotBlank } from 'txstate-utils';
|
|
11
11
|
import ScreenReaderOnly from './ScreenReaderOnly.svelte';
|
|
12
12
|
import DefaultPopupMenu from './PopupMenu.svelte';
|
|
13
13
|
import { modifierKey, selectionIsLeft } from '../util';
|
|
@@ -45,9 +45,11 @@ let menushown;
|
|
|
45
45
|
let loading = false;
|
|
46
46
|
let options = [];
|
|
47
47
|
let hilitedpill;
|
|
48
|
-
let inputvalue = '';
|
|
49
48
|
let popupvalue = undefined;
|
|
50
49
|
let inputelement;
|
|
50
|
+
let menuid;
|
|
51
|
+
let announcement = '';
|
|
52
|
+
const instructionid = randomid();
|
|
51
53
|
const dispatch = createEventDispatcher();
|
|
52
54
|
const optionsCache = new Cache(async (ipt) => {
|
|
53
55
|
return await getOptions(ipt);
|
|
@@ -63,23 +65,23 @@ async function reactToInput(..._) {
|
|
|
63
65
|
loading = true;
|
|
64
66
|
clearTimeout(optionsTimer);
|
|
65
67
|
setTimeout(async () => {
|
|
66
|
-
const saveval =
|
|
68
|
+
const saveval = inputelement?.value;
|
|
67
69
|
const rawOptions = await optionsCache.get(saveval);
|
|
68
|
-
if (
|
|
70
|
+
if (inputelement?.value !== saveval)
|
|
69
71
|
return; // ignore any results that are out of date
|
|
70
72
|
options = rawOptions.filter(o => !('value' in o) || !selectedSet.has(o.value));
|
|
71
|
-
if (typeof document !== 'undefined' && inputelement === document.activeElement && (options.length ||
|
|
73
|
+
if (typeof document !== 'undefined' && inputelement === document.activeElement && (options.length || inputelement?.value.length) && !menushown)
|
|
72
74
|
menushown = true;
|
|
73
75
|
loading = false;
|
|
74
76
|
}, 250);
|
|
75
77
|
}
|
|
76
|
-
$: void reactToInput(
|
|
77
|
-
$: availablemessage = options.filter(o => 'value' in o && o.value).length + ' autocomplete choices available';
|
|
78
|
+
$: void reactToInput(getOptions, selectedSet);
|
|
78
79
|
function addSelection(e) {
|
|
79
|
-
|
|
80
|
+
inputelement.value = '';
|
|
80
81
|
const opt = { ...e.detail, label: e.detail.label || e.detail.value };
|
|
81
82
|
selected = maxSelections === 1 ? [opt] : [...selected, opt];
|
|
82
83
|
popupvalue = undefined;
|
|
84
|
+
announcement = `${maxSelections === 1 ? 'selected' : 'added'} ${opt.value}`;
|
|
83
85
|
dispatch('change', selected);
|
|
84
86
|
}
|
|
85
87
|
function removeSelection(opt, idx, nextfocus = 1) {
|
|
@@ -87,6 +89,9 @@ function removeSelection(opt, idx, nextfocus = 1) {
|
|
|
87
89
|
hilitedpill = selected[idx + nextfocus]?.value;
|
|
88
90
|
}
|
|
89
91
|
selected = selected.filter(s => s.value !== opt.value);
|
|
92
|
+
if (document.activeElement !== inputelement)
|
|
93
|
+
inputelement.focus();
|
|
94
|
+
announcement = `removed ${opt.value}`;
|
|
90
95
|
dispatch('change', selected);
|
|
91
96
|
}
|
|
92
97
|
function inputkeydown(e) {
|
|
@@ -114,17 +119,23 @@ function inputkeydown(e) {
|
|
|
114
119
|
removeSelection(selected[idx], idx, e.key === 'Delete' ? 1 : -1);
|
|
115
120
|
e.preventDefault();
|
|
116
121
|
}
|
|
117
|
-
else if (e.key === 'Backspace' && selectionIsLeft(inputelement)) {
|
|
118
|
-
removeSelection(selected[selected.length - 1], selected.length - 1, -1);
|
|
119
|
-
e.preventDefault();
|
|
120
|
-
}
|
|
121
122
|
}
|
|
122
123
|
else if (e.key !== 'Tab' && maxSelections > 1 && selected.length >= maxSelections) {
|
|
123
124
|
e.preventDefault();
|
|
124
125
|
}
|
|
126
|
+
else if (e.key === 'ArrowDown') {
|
|
127
|
+
hilitedpill = undefined;
|
|
128
|
+
}
|
|
125
129
|
}
|
|
130
|
+
let active = false;
|
|
126
131
|
function inputfocus() {
|
|
127
132
|
void reactToInput();
|
|
133
|
+
active = true;
|
|
134
|
+
dispatch('focus');
|
|
135
|
+
}
|
|
136
|
+
function inputblur() {
|
|
137
|
+
active = false;
|
|
138
|
+
dispatch('blur');
|
|
128
139
|
}
|
|
129
140
|
let popuphilited;
|
|
130
141
|
$: if (popuphilited != null)
|
|
@@ -140,6 +151,7 @@ function reactToHilite(..._) {
|
|
|
140
151
|
inputelement.removeAttribute('aria-activedescendant');
|
|
141
152
|
}
|
|
142
153
|
$: reactToHilite(hilitedpill, id);
|
|
154
|
+
$: ariainstructions = `${selected.length === 0 ? 'nothing' : selected.length} selected, ${maxSelections ? 'select up to ' + maxSelections + ',' : 'select multiple,'} up down to browse choices, ${selected.length ? 'left right to browse selected,' : ''} touch users must explore screen to find menu`;
|
|
143
155
|
let gap = 0;
|
|
144
156
|
onMount(() => {
|
|
145
157
|
const ul = inputelement.closest('.multiselect-selected');
|
|
@@ -148,72 +160,64 @@ onMount(() => {
|
|
|
148
160
|
});
|
|
149
161
|
</script>
|
|
150
162
|
|
|
151
|
-
<
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
<span>{selected.length ? selected.length + ' selected' : 'none selected'}, select {maxSelections ? 'up to ' + maxSelections : 'multiple'}, up down to choose, left right to hilite existing choices</span>
|
|
173
|
-
{#if menushown}<span>{availablemessage}, touch users explore to find autocomplete menu</span>{/if}
|
|
174
|
-
</ScreenReaderOnly>
|
|
163
|
+
<div class="multiselect" class:disabled role="combobox" aria-controls={menushown ? menuid : undefined} aria-expanded={menushown}>
|
|
164
|
+
{#if selected.length}
|
|
165
|
+
<div class="multiselect-selected" role="listbox" aria-label="selected options">
|
|
166
|
+
{#each selected as option, i (option.value)}
|
|
167
|
+
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
168
|
+
<div id={id + option.value} role="option" tabindex="-1" class="multiselect-pill" class:hilited={hilitedpill === option.value}
|
|
169
|
+
on:click|preventDefault|stopPropagation={() => { !disabled && removeSelection(option, i, 1) }} on:mousedown={e => { disabled && e.preventDefault() }}
|
|
170
|
+
aria-selected="true" aria-posinset={i + 1} aria-setsize={selected.length}>
|
|
171
|
+
{option.label || option.value}
|
|
172
|
+
<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
|
+
<ScreenReaderOnly>, click to deselect</ScreenReaderOnly>
|
|
174
|
+
</div>
|
|
175
|
+
{/each}
|
|
176
|
+
</div>
|
|
177
|
+
{/if}
|
|
178
|
+
<!-- svelte-ignore a11y-role-has-required-aria-props -->
|
|
179
|
+
<input type="text" class={inputClass} {id} {name} {disabled} {placeholder}
|
|
180
|
+
bind:this={inputelement} on:input={reactToInput} on:blur={inputblur}
|
|
181
|
+
on:focus={inputfocus} on:keydown={inputkeydown}
|
|
182
|
+
autocomplete="off" autocorrect="off" spellcheck="false"
|
|
183
|
+
aria-describedby="{[descid, instructionid].filter(isNotBlank).join(' ')}">
|
|
175
184
|
<slot></slot>
|
|
176
|
-
</
|
|
177
|
-
<
|
|
185
|
+
<ScreenReaderOnly id={instructionid}>{ariainstructions}</ScreenReaderOnly>
|
|
186
|
+
<ScreenReaderOnly arialive="polite">{announcement}</ScreenReaderOnly>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<svelte:component this={PopupMenu} bind:menushown bind:menuid bind:hilited={popuphilited} bind:value={popupvalue} align='bottomleft'
|
|
178
190
|
{usePortal} {loading} {emptyText} {gap}
|
|
179
191
|
{menuContainerClass} {menuClass} {menuItemClass} {menuItemHilitedClass} {menuDividerClass}
|
|
180
192
|
items={options} buttonelement={inputelement}
|
|
181
193
|
on:change={addSelection}/>
|
|
182
194
|
|
|
183
195
|
<style>
|
|
184
|
-
|
|
185
|
-
border: 0;
|
|
186
|
-
padding: 0;
|
|
187
|
-
margin: 0;
|
|
188
|
-
}
|
|
189
|
-
fieldset * {
|
|
196
|
+
.multiselect, .multiselect * {
|
|
190
197
|
box-sizing: border-box;
|
|
191
198
|
}
|
|
192
|
-
.multiselect
|
|
193
|
-
margin: 0;
|
|
194
|
-
padding: 0;
|
|
195
|
-
list-style: none;
|
|
199
|
+
.multiselect {
|
|
196
200
|
display: flex;
|
|
197
201
|
flex-wrap: wrap;
|
|
198
|
-
align-items: stretch;
|
|
199
202
|
padding: var(--multiselect-padding, 0.3em);
|
|
200
203
|
border: var(--multiselect-border, 1px solid #666666);
|
|
201
204
|
border-radius: var(--multiselect-radius, 0.3em);
|
|
202
205
|
}
|
|
203
|
-
.multiselect
|
|
206
|
+
.multiselect:focus-within {
|
|
204
207
|
outline-width: var(--multiselect-focus-width, 2px);
|
|
205
208
|
outline-style: solid;
|
|
206
209
|
outline-color: var(--multiselect-focus-color, Highlight);
|
|
207
210
|
}
|
|
208
|
-
|
|
209
|
-
|
|
211
|
+
.multiselect-selected {
|
|
212
|
+
display: flex;
|
|
213
|
+
flex-wrap: wrap;
|
|
214
|
+
align-items: center;
|
|
210
215
|
}
|
|
211
216
|
input {
|
|
217
|
+
flex-grow: 1;
|
|
212
218
|
outline: 0;
|
|
213
219
|
border: 0;
|
|
214
220
|
min-width: 0;
|
|
215
|
-
width: 100%;
|
|
216
|
-
height: 100%;
|
|
217
221
|
}
|
|
218
222
|
.multiselect-pill {
|
|
219
223
|
position: relative;
|
|
@@ -244,7 +248,7 @@ onMount(() => {
|
|
|
244
248
|
.multiselect-selected.disabled {
|
|
245
249
|
opacity: 0.5;
|
|
246
250
|
}
|
|
247
|
-
.multiselect
|
|
251
|
+
.multiselect.disabled .multiselect-pill {
|
|
248
252
|
cursor: default;
|
|
249
253
|
}
|
|
250
254
|
.multiselect-pill.hilited {
|
|
@@ -28,8 +28,9 @@ declare const __propDef: {
|
|
|
28
28
|
/** You can define your own PopupMenu and pass for that to be used or accept DefaultPopupMenu. */ PopupMenu?: typeof DefaultPopupMenu | undefined;
|
|
29
29
|
};
|
|
30
30
|
events: {
|
|
31
|
-
blur: FocusEvent;
|
|
32
31
|
change: CustomEvent<any>;
|
|
32
|
+
focus: CustomEvent<any>;
|
|
33
|
+
blur: CustomEvent<any>;
|
|
33
34
|
} & {
|
|
34
35
|
[evt: string]: CustomEvent<any>;
|
|
35
36
|
};
|
|
@@ -107,9 +107,9 @@ $: void reactToMenuShown(menushown);
|
|
|
107
107
|
function move(idx) {
|
|
108
108
|
if (!menushown)
|
|
109
109
|
return;
|
|
110
|
-
while (idx <= lastactive && 'divider' in items[idx])
|
|
110
|
+
while (idx <= lastactive && items[idx] != null && 'divider' in items[idx])
|
|
111
111
|
idx++;
|
|
112
|
-
if (items[idx]?.disabled)
|
|
112
|
+
if (items[idx] == null || items[idx]?.disabled)
|
|
113
113
|
return;
|
|
114
114
|
hilited = Math.max(firstactive, Math.min(lastactive, idx));
|
|
115
115
|
itemelements[hilited].scrollIntoView({ block: 'center' });
|
|
@@ -125,7 +125,7 @@ function onkeydown(e) {
|
|
|
125
125
|
e.preventDefault();
|
|
126
126
|
if (menushown) {
|
|
127
127
|
let i = (hilited ?? firstactive - 1) + 1;
|
|
128
|
-
while (!isSelectable(items[i]))
|
|
128
|
+
while (items[i] && !isSelectable(items[i]))
|
|
129
129
|
i++;
|
|
130
130
|
move(i);
|
|
131
131
|
}
|
|
@@ -138,7 +138,7 @@ function onkeydown(e) {
|
|
|
138
138
|
e.preventDefault();
|
|
139
139
|
if (menushown) {
|
|
140
140
|
let i = (hilited ?? lastactive + 1) - 1;
|
|
141
|
-
while (!isSelectable(items[i]))
|
|
141
|
+
while (items[i] && !isSelectable(items[i]))
|
|
142
142
|
i--;
|
|
143
143
|
move(i);
|
|
144
144
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Store } from '@txstate-mws/svelte-store';
|
|
2
|
+
export interface Toast {
|
|
3
|
+
message: string;
|
|
4
|
+
/** default is error */
|
|
5
|
+
type?: 'error' | 'warning' | 'info' | 'success';
|
|
6
|
+
/**
|
|
7
|
+
* how long the message should appear on screen in milliseconds
|
|
8
|
+
* default 15 seconds
|
|
9
|
+
* use Number.MAX_SAFE_INTEGER for permanent
|
|
10
|
+
*/
|
|
11
|
+
ttlMs?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface ToastStorage extends Required<Toast> {
|
|
14
|
+
/** each toast gets a random id so it can be dismissed by user interaction */
|
|
15
|
+
id: string;
|
|
16
|
+
/** when the toast was created, so we know when it expires */
|
|
17
|
+
stamp: Date;
|
|
18
|
+
}
|
|
19
|
+
export declare class ToastStore extends Store<ToastStorage[]> {
|
|
20
|
+
add(msg: string, type?: Toast['type'], ttlMs?: number): void;
|
|
21
|
+
close(id: string): void;
|
|
22
|
+
clean(): void;
|
|
23
|
+
}
|
|
24
|
+
export declare const toasts: ToastStore;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Store } from '@txstate-mws/svelte-store';
|
|
2
|
+
import { randomid } from 'txstate-utils';
|
|
3
|
+
export class ToastStore extends Store {
|
|
4
|
+
add(msg, type, ttlMs) {
|
|
5
|
+
const stored = {
|
|
6
|
+
id: randomid(),
|
|
7
|
+
stamp: new Date(),
|
|
8
|
+
message: msg,
|
|
9
|
+
type: type ?? 'error',
|
|
10
|
+
ttlMs: ttlMs ?? 15000
|
|
11
|
+
};
|
|
12
|
+
this.update(v => [...v.filter(t => t.message !== stored.message), stored]);
|
|
13
|
+
setTimeout(() => { this.clean(); }, stored.ttlMs);
|
|
14
|
+
}
|
|
15
|
+
close(id) {
|
|
16
|
+
this.update(v => v.filter(t => t.id !== id));
|
|
17
|
+
}
|
|
18
|
+
clean() {
|
|
19
|
+
const now = new Date().getTime();
|
|
20
|
+
this.update(v => v.filter(t => now - t.stamp.getTime() < t.ttlMs));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export const toasts = new ToastStore([]);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { ElementOffsets } from './bodyoffset.js';
|
|
2
2
|
export declare function watchForMutations(cb: () => void): {
|
|
3
|
-
destroy()
|
|
3
|
+
destroy: () => void;
|
|
4
4
|
};
|
|
5
5
|
export declare function watchForPositionChange(el: (HTMLElement | undefined)[] | HTMLElement | undefined, cb: ((offset: Required<ElementOffsets>) => void) | ((offsets: Required<ElementOffsets>[]) => void)): {
|
|
6
|
-
update(newEl: (HTMLElement | undefined)[] | HTMLElement | undefined, newCb: ((offset: Required<ElementOffsets>) => void) | ((offsets: Required<ElementOffsets>[]) => void))
|
|
6
|
+
update: (newEl: (HTMLElement | undefined)[] | HTMLElement | undefined, newCb: ((offset: Required<ElementOffsets>) => void) | ((offsets: Required<ElementOffsets>[]) => void)) => void;
|
|
7
7
|
destroy: () => void;
|
|
8
8
|
};
|
|
9
9
|
export declare function watchForPositionChangeInContainer(el: HTMLElement | undefined, container: HTMLElement | undefined, cb: (offset: Required<ElementOffsets>) => void): {
|
|
10
|
-
update(newEl: HTMLElement | undefined, newContainer: HTMLElement | undefined, newCb: (offset: Required<ElementOffsets>) => void)
|
|
10
|
+
update: (newEl: HTMLElement | undefined, newContainer: HTMLElement | undefined, newCb: (offset: Required<ElementOffsets>) => void) => void;
|
|
11
11
|
destroy: () => void;
|
|
12
12
|
};
|
|
@@ -21,7 +21,7 @@ export function watchForMutations(cb) {
|
|
|
21
21
|
observers.set(id, cb);
|
|
22
22
|
cb();
|
|
23
23
|
return {
|
|
24
|
-
destroy() {
|
|
24
|
+
destroy: () => {
|
|
25
25
|
observers.delete(id);
|
|
26
26
|
if (observers.size === 0) {
|
|
27
27
|
window.removeEventListener('resize', watch);
|
|
@@ -42,7 +42,7 @@ export function watchForPositionChange(el, cb) {
|
|
|
42
42
|
}
|
|
43
43
|
const { destroy } = watchForMutations(watch);
|
|
44
44
|
return {
|
|
45
|
-
update(newEl, newCb) {
|
|
45
|
+
update: (newEl, newCb) => {
|
|
46
46
|
const changed = newEl !== el || newCb !== cb;
|
|
47
47
|
if (changed) {
|
|
48
48
|
el = newEl;
|
|
@@ -67,7 +67,7 @@ export function watchForPositionChangeInContainer(el, container, cb) {
|
|
|
67
67
|
}
|
|
68
68
|
const { destroy } = watchForMutations(watch);
|
|
69
69
|
return {
|
|
70
|
-
update(newEl, newContainer, newCb) {
|
|
70
|
+
update: (newEl, newContainer, newCb) => {
|
|
71
71
|
const changed = newEl !== el || newContainer !== container || newCb !== cb;
|
|
72
72
|
if (changed) {
|
|
73
73
|
el = newEl;
|
package/dist/util/util.js
CHANGED