@txstate-mws/svelte-components 1.6.12 → 1.6.14
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/elementqueries.d.ts +12 -9
- package/dist/actions/resize.d.ts +5 -5
- package/dist/components/ButtonMenu.svelte +31 -25
- package/dist/components/Card.svelte +12 -8
- package/dist/components/CardLayout.svelte +151 -158
- package/dist/components/CollapsingTable.svelte +65 -63
- package/dist/components/ConditionalWrapper.svelte +7 -6
- package/dist/components/FocusLock.svelte +79 -85
- package/dist/components/Loading.svelte +16 -15
- package/dist/components/Lottie.svelte +37 -31
- package/dist/components/Modal.svelte +36 -33
- package/dist/components/MultiSelect.svelte +158 -165
- package/dist/components/PopupMenu.svelte +270 -271
- package/dist/components/ScreenReaderOnly.svelte +5 -4
- package/dist/stores/toasts.d.ts +2 -2
- package/dist/util/util.d.ts +2 -2
- package/package.json +6 -7
|
@@ -6,179 +6,172 @@
|
|
|
6
6
|
controlled by the parent component via the `getOptions` function that will be used as a
|
|
7
7
|
debounced callback on the contents of the text input.
|
|
8
8
|
-->
|
|
9
|
-
<script lang="ts">
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
export let
|
|
24
|
-
export let
|
|
25
|
-
export let
|
|
26
|
-
export let
|
|
27
|
-
export let
|
|
28
|
-
export let
|
|
29
|
-
|
|
30
|
-
export let
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
export let
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
export let
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
export let
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
let
|
|
54
|
-
|
|
55
|
-
let
|
|
56
|
-
let
|
|
57
|
-
let
|
|
58
|
-
let
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
9
|
+
<script lang="ts">
|
|
10
|
+
import { createEventDispatcher, onMount } from 'svelte'
|
|
11
|
+
import { randomid, Cache, isNotBlank } from 'txstate-utils'
|
|
12
|
+
import ScreenReaderOnly from './ScreenReaderOnly.svelte'
|
|
13
|
+
import DefaultPopupMenu from './PopupMenu.svelte'
|
|
14
|
+
import { modifierKey, selectionIsLeft } from '../util'
|
|
15
|
+
import type { PopupMenuItem } from '../types'
|
|
16
|
+
import { SafeStore } from '@txstate-mws/svelte-store'
|
|
17
|
+
|
|
18
|
+
export let name: string
|
|
19
|
+
/**
|
|
20
|
+
* Function to pass to the component that tells it how to use the text in the text input to determine what
|
|
21
|
+
* `PopupMenuItem[]` should be displayed in the `PopupMenu`. Items already 'selected' from the popup menu will
|
|
22
|
+
* be tracked and automatically filtered from the popup if returned as one of the `PopupMenuItem[]` by `getOptions`. */
|
|
23
|
+
export let getOptions: (search: string) => Promise<PopupMenuItem[]> | PopupMenuItem[]
|
|
24
|
+
export let id = randomid()
|
|
25
|
+
export let disabled = false
|
|
26
|
+
export let menuContainerClass = ''
|
|
27
|
+
export let menuClass = ''
|
|
28
|
+
export let menuItemClass = ''
|
|
29
|
+
export let menuItemHilitedClass = ''
|
|
30
|
+
export let inputClass = ''
|
|
31
|
+
export let menuCategoryClass = ''
|
|
32
|
+
/** The maximum number of selections allowed before making new selections is disabled. Default of 0 is unlimited. */
|
|
33
|
+
export let maxSelections = 0
|
|
34
|
+
/** Consuming components may need to make decisions about what to display or return as options in the popup based
|
|
35
|
+
* on what's already selected. This component property can be used to both pass an initial set of selections but
|
|
36
|
+
* it can also be bound to provide a means to inspect what's selected. */
|
|
37
|
+
export let selected: PopupMenuItem[] = []
|
|
38
|
+
/** The text to be displayed in the input text box for contextual feedback on what the input text is expecting. */
|
|
39
|
+
export let placeholder = ''
|
|
40
|
+
/** When there are no items (e.g. it's a filtered search and there were no results), we still display one
|
|
41
|
+
disabled item in the menu to let the user know what is going on. Use this prop to specify the message. */
|
|
42
|
+
export let emptyText: string | undefined = undefined
|
|
43
|
+
export let usePortal: HTMLElement | true | undefined = undefined
|
|
44
|
+
export let descid: string | undefined = undefined
|
|
45
|
+
/** You can define your own PopupMenu and pass for that to be used or accept DefaultPopupMenu. */
|
|
46
|
+
export let PopupMenu = DefaultPopupMenu
|
|
47
|
+
/** Add a Delete All button to clear all selected items */
|
|
48
|
+
export let includeDeleteAll = false
|
|
49
|
+
/** Show a confirmation message before clearing all selections */
|
|
50
|
+
export let confirmDelete: string | undefined = undefined
|
|
51
|
+
/** A function that can be used to customize the labels on the selected item pills. Default is a function
|
|
52
|
+
* that takes a PopupMenuItem and returns its label, if available, or its value when no label is provided. */
|
|
53
|
+
export let selectedItemLabel: (item: PopupMenuItem) => string = item => item.label || item.value
|
|
54
|
+
|
|
55
|
+
let menushown: boolean
|
|
56
|
+
let loading = false
|
|
57
|
+
let options: PopupMenuItem[] = []
|
|
58
|
+
let hilitedpill: string | undefined
|
|
59
|
+
let popupvalue: string | undefined = undefined
|
|
60
|
+
let inputelement: HTMLInputElement
|
|
61
|
+
let menuid: string
|
|
62
|
+
let announcement = ''
|
|
63
|
+
const instructionid = randomid()
|
|
64
|
+
const dispatch = createEventDispatcher()
|
|
65
|
+
|
|
66
|
+
const optionsCache = new Cache(async (ipt: string) => await getOptions(ipt), { freshseconds: 5 })
|
|
67
|
+
|
|
68
|
+
const selectedStore = new SafeStore(selected)
|
|
69
|
+
$: $selectedStore = selected
|
|
70
|
+
$: selectedSet = new Set($selectedStore.map(s => s.value))
|
|
71
|
+
// Stop showing our menu if the maxSelections limit has been reached.
|
|
72
|
+
$: if (maxSelections > 1 && selected.length >= maxSelections && menushown) menushown = false
|
|
73
|
+
let optionsTimer: number | undefined
|
|
74
|
+
async function reactToInput (..._: any) {
|
|
75
|
+
loading = true
|
|
76
|
+
clearTimeout(optionsTimer)
|
|
72
77
|
setTimeout(async () => {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
function removeSelection(opt, idx, nextfocus = 1) {
|
|
78
|
+
const saveval = inputelement?.value
|
|
79
|
+
const rawOptions = await optionsCache.get(saveval)
|
|
80
|
+
if (inputelement?.value !== saveval) return // ignore any results that are out of date
|
|
81
|
+
options = rawOptions.filter(o => !('value' in o) || !selectedSet.has(o.value))
|
|
82
|
+
if (typeof document !== 'undefined' && inputelement === document.activeElement && (options.length || inputelement?.value.length) && !menushown) menushown = true
|
|
83
|
+
loading = false
|
|
84
|
+
}, 250)
|
|
85
|
+
}
|
|
86
|
+
$: void reactToInput(getOptions, selectedSet)
|
|
87
|
+
function addSelection (e: CustomEvent & { detail: PopupMenuItem }) {
|
|
88
|
+
inputelement.value = ''
|
|
89
|
+
const opt = { ...e.detail, label: e.detail.label || e.detail.value }
|
|
90
|
+
selected = maxSelections === 1 ? [opt] : [...selected, opt]
|
|
91
|
+
popupvalue = undefined
|
|
92
|
+
announcement = `${maxSelections === 1 ? 'selected' : 'added'} ${opt.label}`
|
|
93
|
+
dispatch('change', selected)
|
|
94
|
+
}
|
|
95
|
+
function removeSelection (opt: typeof selected[number], idx: number, nextfocus = 1) {
|
|
93
96
|
if (hilitedpill === opt.value) {
|
|
94
|
-
|
|
97
|
+
hilitedpill = selected[idx + nextfocus]?.value
|
|
95
98
|
}
|
|
96
|
-
selected = selected.filter(s => s.value !== opt.value)
|
|
97
|
-
if (document.activeElement !== inputelement)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
function inputkeydown(e) {
|
|
103
|
-
if (modifierKey(e))
|
|
104
|
-
return;
|
|
99
|
+
selected = selected.filter(s => s.value !== opt.value)
|
|
100
|
+
if (document.activeElement !== inputelement) inputelement.focus()
|
|
101
|
+
announcement = `removed ${opt.label ?? opt.value}`
|
|
102
|
+
dispatch('change', selected)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function inputkeydown (e: KeyboardEvent) {
|
|
106
|
+
if (modifierKey(e)) return
|
|
105
107
|
if (e.key === 'ArrowLeft' && selectionIsLeft(inputelement)) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
else if (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
e.preventDefault();
|
|
126
|
-
}
|
|
108
|
+
const idx = hilitedpill ? selected.findIndex(s => s.value === hilitedpill) : selected.length
|
|
109
|
+
if (idx !== 0) hilitedpill = selected[idx - 1].value
|
|
110
|
+
} else if (e.key === 'ArrowRight' && selectionIsLeft(inputelement)) {
|
|
111
|
+
if (hilitedpill) e.preventDefault()
|
|
112
|
+
const idx = hilitedpill ? selected.findIndex(s => s.value === hilitedpill) : undefined
|
|
113
|
+
if (idx != null) {
|
|
114
|
+
if (idx === selected.length - 1) hilitedpill = undefined
|
|
115
|
+
else hilitedpill = selected[idx + 1].value
|
|
116
|
+
}
|
|
117
|
+
} else if (['Backspace', 'Delete', 'Clear', ' ', 'Enter'].includes(e.key)) {
|
|
118
|
+
if (hilitedpill) {
|
|
119
|
+
const idx = selected.findIndex(s => s.value === hilitedpill)
|
|
120
|
+
removeSelection(selected[idx], idx, e.key === 'Delete' ? 1 : -1)
|
|
121
|
+
e.preventDefault()
|
|
122
|
+
}
|
|
123
|
+
} else if (e.key !== 'Tab' && maxSelections > 1 && selected.length >= maxSelections) {
|
|
124
|
+
e.preventDefault()
|
|
125
|
+
} else if (e.key === 'ArrowDown') {
|
|
126
|
+
hilitedpill = undefined
|
|
127
127
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
function
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
function
|
|
147
|
-
selected = [];
|
|
148
|
-
dispatch('change', selected);
|
|
149
|
-
}
|
|
150
|
-
function confirmRemoveAllSelections() {
|
|
128
|
+
}
|
|
129
|
+
let active = false
|
|
130
|
+
function inputfocus () {
|
|
131
|
+
void reactToInput()
|
|
132
|
+
active = true
|
|
133
|
+
dispatch('focus')
|
|
134
|
+
}
|
|
135
|
+
function inputblur () {
|
|
136
|
+
active = false
|
|
137
|
+
hilitedpill = undefined
|
|
138
|
+
dispatch('blur')
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function removeAllSelections () {
|
|
142
|
+
selected = []
|
|
143
|
+
dispatch('change', selected)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function confirmRemoveAllSelections () {
|
|
151
147
|
if (confirmDelete) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
removeAllSelections();
|
|
148
|
+
// eslint-disable-next-line no-alert -- just doing something super simple here
|
|
149
|
+
if (window.confirm(confirmDelete)) {
|
|
150
|
+
removeAllSelections()
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
removeAllSelections()
|
|
159
154
|
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
function reactToHilite(..._) {
|
|
165
|
-
if (!inputelement)
|
|
166
|
-
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let popuphilited: number | undefined
|
|
158
|
+
$: if (popuphilited != null) hilitedpill = undefined
|
|
159
|
+
function reactToHilite (..._: any) {
|
|
160
|
+
if (!inputelement) return
|
|
167
161
|
if (hilitedpill) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
$:
|
|
175
|
-
|
|
176
|
-
let gap = 0
|
|
177
|
-
onMount(() => {
|
|
178
|
-
const ul = inputelement.closest('.multiselect-selected')
|
|
179
|
-
if (ul != null)
|
|
180
|
-
|
|
181
|
-
});
|
|
162
|
+
popuphilited = undefined
|
|
163
|
+
inputelement.setAttribute('aria-activedescendant', id + hilitedpill)
|
|
164
|
+
} else inputelement.removeAttribute('aria-activedescendant')
|
|
165
|
+
}
|
|
166
|
+
$: reactToHilite(hilitedpill, id)
|
|
167
|
+
|
|
168
|
+
$: 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`
|
|
169
|
+
|
|
170
|
+
let gap = 0
|
|
171
|
+
onMount(() => {
|
|
172
|
+
const ul = inputelement.closest<HTMLElement>('.multiselect-selected')
|
|
173
|
+
if (ul != null) gap = ((ul.offsetHeight - ul.clientHeight) / 2) + parseInt(window.getComputedStyle(ul).paddingBottom, 10)
|
|
174
|
+
})
|
|
182
175
|
</script>
|
|
183
176
|
|
|
184
177
|
<div class="multiselect" class:disabled role="combobox" aria-controls={menushown ? menuid : undefined} aria-expanded={menushown}>
|