@txstate-mws/svelte-components 1.6.12 → 1.6.13
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/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
|
@@ -2,294 +2,293 @@
|
|
|
2
2
|
The purpose of [`PopupMenu`](https://github.com/txstate-etc/svelte-components/blob/main/docs/PopupMenu.md) is to display a menu of options to the user, activated by clicking any element
|
|
3
3
|
bound to `buttonelement`. It can also be controlled from the parent, by binding the `menushown` boolean.
|
|
4
4
|
-->
|
|
5
|
-
<script lang="ts" generics="MenuItem extends PopupMenuItem = PopupMenuItem">
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
export let
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
export let
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
export let
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
export let
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
export let
|
|
63
|
-
|
|
64
|
-
export let
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
export let
|
|
68
|
-
let
|
|
69
|
-
|
|
70
|
-
let
|
|
71
|
-
let
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
let
|
|
78
|
-
|
|
5
|
+
<script lang="ts" generics="MenuItem extends PopupMenuItem = PopupMenuItem">
|
|
6
|
+
import { createEventDispatcher, onDestroy, tick } from 'svelte'
|
|
7
|
+
import { groupby, isNull, randomid } from 'txstate-utils'
|
|
8
|
+
import { Store } from '@txstate-mws/svelte-store'
|
|
9
|
+
import { glue, portal } from '../actions'
|
|
10
|
+
import type { GlueAlignOpts, GlueAlignStore } from '../actions'
|
|
11
|
+
import type { PopupMenuItem, PopupMenuDivider } from '../types'
|
|
12
|
+
import ScreenReaderOnly from './ScreenReaderOnly.svelte'
|
|
13
|
+
import { getScrollParents, modifierKey } from '../util'
|
|
14
|
+
const dispatch = createEventDispatcher()
|
|
15
|
+
|
|
16
|
+
type MenuTypes = MenuItem | PopupMenuDivider
|
|
17
|
+
|
|
18
|
+
interface $$Events {
|
|
19
|
+
change: CustomEvent<MenuItem>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The DOM element that will act as the "button" for this menu. The menu will be placed next to the
|
|
23
|
+
button and be controlled by the button. Keyboard access will be enabled when the button has focus.
|
|
24
|
+
This component adds all appropriate attributes (tabindex, roles, and aria) automatically. */
|
|
25
|
+
export let buttonelement: HTMLElement | undefined
|
|
26
|
+
/** The list of menu items to be shown. Parent may change this at any time based on user activity. */
|
|
27
|
+
export let items: MenuItem[] = []
|
|
28
|
+
export let menushown = false
|
|
29
|
+
export let value: string | undefined = undefined
|
|
30
|
+
/** Control where the menu will appear. Default is to use the current viewport to make a decision to
|
|
31
|
+
maximize potential for menu growth. */
|
|
32
|
+
export let align: GlueAlignOpts = 'auto'
|
|
33
|
+
/** When the menu is active, should it cover the button or be rendered above/below it? */
|
|
34
|
+
export let cover = false
|
|
35
|
+
/** When an item is currently selected, should it appear in the menu (with a visual indicator), or
|
|
36
|
+
simply be removed from the list (`showSelected: false`). */
|
|
37
|
+
export let showSelected = true
|
|
38
|
+
/** When defined, this width will be passed through to the menu's CSS width. Use a valid CSS dimension
|
|
39
|
+
such as 33em or 159px. Generally this will be used when you need to match the menu width to something
|
|
40
|
+
else, like the button. If the width is static like 100% a simple CSS rule is likely more efficient. */
|
|
41
|
+
export let width: string | undefined = undefined
|
|
42
|
+
/** Bind this prop to receive a store that is updated each time a menu placement decision is made. Example
|
|
43
|
+
of when this is useful is when you are trying to round corners of your button and need to know whether the
|
|
44
|
+
menu is above or below your button so you know which corners to round and which to leave square. Note that
|
|
45
|
+
the values do not update when the menu is hidden, so you may need to rely on `menushown` as well. */
|
|
46
|
+
export let computedalign = new Store<GlueAlignStore>({ valign: 'bottom', halign: 'left' })
|
|
47
|
+
/** Create some spacing between the specified button element and the menu, in pixels */
|
|
48
|
+
export let gap = 0
|
|
49
|
+
/** Set to `true` if you want the parent element's styling to increase its minHeight when necessary. Useful
|
|
50
|
+
for parent elements not styled for variable size children and which you want override the the size of. */
|
|
51
|
+
export let adjustparentheight = false
|
|
52
|
+
/** If the menu would be clipped by an `overflow: hidden`, you can set this prop and it will be placed in
|
|
53
|
+
the specified container, or the `document.body` if you simply say `true`. Placement will still be calculated
|
|
54
|
+
correctly since it uses positon fixed for placement. */
|
|
55
|
+
export let usePortal: HTMLElement | true | undefined = undefined
|
|
56
|
+
/** Useful for when your `items` need to be fetched but you want the associated element shown. Set to loading
|
|
57
|
+
until they're ready to be displayed and the popup menu will not be shown until the `loading` bind is `true`. */
|
|
58
|
+
export let loading = false
|
|
59
|
+
/** A bindable value for inspecting which item in `items` is currently highlighted as the item currently
|
|
60
|
+
active in the list. This is not the same as `value` and the highlighted item may or may not be selected as
|
|
61
|
+
for the `value` of the field. */
|
|
62
|
+
export let hilited: number | undefined = undefined
|
|
63
|
+
/** The id of the <ul> element that is the displayed list of items. */
|
|
64
|
+
export let menuid = randomid()
|
|
65
|
+
/** When there are no items (e.g. it's a filtered search and there were no results), we still display one
|
|
66
|
+
disabled item in the menu to let the user know what is going on. Use this prop to specify the message. */
|
|
67
|
+
export let emptyText: string | undefined = undefined
|
|
68
|
+
export let menuContainerClass = ''
|
|
69
|
+
export let menuClass = ''
|
|
70
|
+
export let menuItemClass = ''
|
|
71
|
+
export let menuItemHilitedClass = ''
|
|
72
|
+
export let menuItemSelectedClass = ''
|
|
73
|
+
export let menuCategoryClass = ''
|
|
74
|
+
export let hideSelectedIndicator = false
|
|
75
|
+
export let hideEmptyText = false
|
|
76
|
+
/** If true, the role for the popup menu will be 'menu' instead of the default 'listbox' value */
|
|
77
|
+
export let usemenurole = false
|
|
78
|
+
|
|
79
|
+
let menuelement: HTMLElement | undefined
|
|
80
|
+
const itemelements: HTMLElement[] = []
|
|
81
|
+
let firstactive = 0
|
|
82
|
+
let lastactive = items.length - 1
|
|
83
|
+
$: hasMeaningfulItems = showSelected ? !!items.length : !!items.filter(itm => itm.value !== value).length
|
|
84
|
+
|
|
85
|
+
function hiddenItem (item: MenuItem) {
|
|
86
|
+
return !!item && !showSelected && item.value === value
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
$: grouped = groupby(items, item => item.group ?? 'default-group')
|
|
90
|
+
let itemsOrderedByGroup: MenuItem[] = []
|
|
91
|
+
|
|
92
|
+
function getItemIndex (item: MenuItem) {
|
|
79
93
|
return itemsOrderedByGroup.findIndex(itm => {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return item.group === itm.group;
|
|
87
|
-
}
|
|
94
|
+
// need to handle default group options
|
|
95
|
+
if (itm.value === item.value) {
|
|
96
|
+
if (item.group === 'default-group') {
|
|
97
|
+
return isNull(itm.group)
|
|
98
|
+
} else {
|
|
99
|
+
return item.group === itm.group
|
|
88
100
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
}
|
|
102
|
+
return false
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function reactToItems (..._: any[]) {
|
|
107
|
+
itemsOrderedByGroup = Object.values(grouped).flat()
|
|
108
|
+
firstactive = itemsOrderedByGroup.findIndex(itm => 'value' in itm && !itm.disabled && !hiddenItem(itm))
|
|
109
|
+
lastactive = itemsOrderedByGroup.length - [...itemsOrderedByGroup].reverse().findIndex(itm => 'value' in itm && !itm.disabled && !hiddenItem(itm)) - 1
|
|
110
|
+
if (hilited && (itemsOrderedByGroup[hilited])?.disabled) hilited = firstactive
|
|
111
|
+
}
|
|
112
|
+
$: void reactToItems(items, value)
|
|
113
|
+
|
|
114
|
+
async function reactToMenuShown (_: boolean) {
|
|
101
115
|
if (!buttonelement) {
|
|
102
|
-
|
|
103
|
-
|
|
116
|
+
hilited = undefined
|
|
117
|
+
return
|
|
104
118
|
}
|
|
105
|
-
await tick()
|
|
119
|
+
await tick()
|
|
106
120
|
if (!menushown) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
121
|
+
buttonelement.removeAttribute('aria-controls')
|
|
122
|
+
buttonelement.removeAttribute('aria-owns')
|
|
123
|
+
buttonelement.setAttribute('aria-expanded', 'false')
|
|
124
|
+
if (hilited) buttonelement.removeAttribute('aria-activedescendant')
|
|
125
|
+
hilited = undefined
|
|
126
|
+
} else {
|
|
127
|
+
if (buttonelement instanceof HTMLInputElement) buttonelement.setAttribute('aria-owns', menuid)
|
|
128
|
+
else buttonelement.setAttribute('aria-controls', menuid)
|
|
129
|
+
buttonelement.setAttribute('aria-expanded', 'true')
|
|
130
|
+
if (buttonelement !== document.activeElement) buttonelement.focus()
|
|
113
131
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
itemelements[hilited].scrollIntoView({ block: 'center' });
|
|
132
|
-
buttonelement?.setAttribute('aria-activedescendant', `${menuid}-${hilited}`);
|
|
133
|
-
}
|
|
134
|
-
function isSelectable(itm) {
|
|
135
|
-
return 'value' in itm && !itm.disabled && !hiddenItem(itm);
|
|
136
|
-
}
|
|
137
|
-
function onkeydown(e) {
|
|
138
|
-
if (modifierKey(e))
|
|
139
|
-
return;
|
|
132
|
+
}
|
|
133
|
+
$: void reactToMenuShown(menushown)
|
|
134
|
+
|
|
135
|
+
function move (idx: number) {
|
|
136
|
+
if (!menushown) return
|
|
137
|
+
if (itemsOrderedByGroup[idx] == null || (itemsOrderedByGroup[idx])?.disabled) return
|
|
138
|
+
hilited = Math.max(firstactive, Math.min(lastactive, idx))
|
|
139
|
+
itemelements[hilited].scrollIntoView({ block: 'center' })
|
|
140
|
+
buttonelement?.setAttribute('aria-activedescendant', `${menuid}-${hilited}`)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function isSelectable (itm: MenuTypes): itm is MenuItem {
|
|
144
|
+
return 'value' in itm && !itm.disabled && !hiddenItem(itm)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function onkeydown (e: KeyboardEvent) {
|
|
148
|
+
if (modifierKey(e)) return
|
|
140
149
|
if (e.key === 'ArrowDown') {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
150
|
+
e.preventDefault()
|
|
151
|
+
if (menushown) {
|
|
152
|
+
let i = (hilited ?? firstactive - 1) + 1
|
|
153
|
+
while (itemsOrderedByGroup[i] && !isSelectable(itemsOrderedByGroup[i])) i += 1
|
|
154
|
+
move(i)
|
|
155
|
+
} else {
|
|
156
|
+
menushown = true
|
|
157
|
+
tick().then(() => { move(firstactive) }).catch(console.error)
|
|
158
|
+
}
|
|
159
|
+
} else if (e.key === 'ArrowUp') {
|
|
160
|
+
e.preventDefault()
|
|
161
|
+
if (menushown) {
|
|
162
|
+
let i = (hilited ?? lastactive + 1) - 1
|
|
163
|
+
while (itemsOrderedByGroup[i] && !isSelectable(itemsOrderedByGroup[i])) i -= 1
|
|
164
|
+
move(i)
|
|
165
|
+
} else {
|
|
166
|
+
menushown = true
|
|
167
|
+
tick().then(() => { move(lastactive) }).catch(console.error)
|
|
168
|
+
}
|
|
169
|
+
} else if (e.key === 'Home') {
|
|
170
|
+
e.preventDefault()
|
|
171
|
+
if (menushown) {
|
|
172
|
+
move(firstactive)
|
|
173
|
+
}
|
|
174
|
+
} else if (e.key === 'End') {
|
|
175
|
+
e.preventDefault()
|
|
176
|
+
if (menushown) {
|
|
177
|
+
move(lastactive)
|
|
178
|
+
}
|
|
179
|
+
} else if (e.key === 'Enter') {
|
|
180
|
+
e.preventDefault()
|
|
181
|
+
if (menushown) {
|
|
182
|
+
menushown = false
|
|
183
|
+
if (typeof hilited !== 'undefined') {
|
|
184
|
+
value = (itemsOrderedByGroup[hilited])?.value
|
|
185
|
+
dispatch('change', itemsOrderedByGroup[hilited])
|
|
147
186
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
187
|
+
} else {
|
|
188
|
+
menushown = true
|
|
189
|
+
tick().then(() => { move(firstactive) }).catch(console.error)
|
|
190
|
+
}
|
|
191
|
+
} else if (e.key === ' ') {
|
|
192
|
+
// buttonelement might be a text input if this popup is searchable,
|
|
193
|
+
// so we want to allow spaces to be entered without changing the menu situation
|
|
194
|
+
if (menushown) {
|
|
195
|
+
if (typeof hilited !== 'undefined') {
|
|
196
|
+
e.preventDefault()
|
|
197
|
+
menushown = false
|
|
198
|
+
value = (itemsOrderedByGroup[hilited])?.value
|
|
199
|
+
dispatch('change', itemsOrderedByGroup[hilited])
|
|
200
|
+
} else if (buttonelement?.tagName !== 'INPUT') {
|
|
201
|
+
e.preventDefault()
|
|
202
|
+
menushown = false
|
|
151
203
|
}
|
|
204
|
+
} else if (buttonelement?.tagName !== 'INPUT') {
|
|
205
|
+
e.preventDefault()
|
|
206
|
+
menushown = true
|
|
207
|
+
tick().then(() => { move(firstactive) }).catch(console.error)
|
|
208
|
+
}
|
|
209
|
+
} else if (menushown && e.key === 'Escape') {
|
|
210
|
+
e.preventDefault()
|
|
211
|
+
e.stopPropagation()
|
|
212
|
+
menushown = false
|
|
152
213
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
tick().then(() => { move(lastactive); }).catch(console.error);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
else if (e.key === 'Home') {
|
|
167
|
-
e.preventDefault();
|
|
168
|
-
if (menushown) {
|
|
169
|
-
move(firstactive);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
else if (e.key === 'End') {
|
|
173
|
-
e.preventDefault();
|
|
174
|
-
if (menushown) {
|
|
175
|
-
move(lastactive);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
else if (e.key === 'Enter') {
|
|
179
|
-
e.preventDefault();
|
|
180
|
-
if (menushown) {
|
|
181
|
-
menushown = false;
|
|
182
|
-
if (typeof hilited !== 'undefined') {
|
|
183
|
-
value = (itemsOrderedByGroup[hilited])?.value;
|
|
184
|
-
dispatch('change', itemsOrderedByGroup[hilited]);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
else {
|
|
188
|
-
menushown = true;
|
|
189
|
-
tick().then(() => { move(firstactive); }).catch(console.error);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
else if (e.key === ' ') {
|
|
193
|
-
// buttonelement might be a text input if this popup is searchable,
|
|
194
|
-
// so we want to allow spaces to be entered without changing the menu situation
|
|
195
|
-
if (menushown) {
|
|
196
|
-
if (typeof hilited !== 'undefined') {
|
|
197
|
-
e.preventDefault();
|
|
198
|
-
menushown = false;
|
|
199
|
-
value = (itemsOrderedByGroup[hilited])?.value;
|
|
200
|
-
dispatch('change', itemsOrderedByGroup[hilited]);
|
|
201
|
-
}
|
|
202
|
-
else if (buttonelement?.tagName !== 'INPUT') {
|
|
203
|
-
e.preventDefault();
|
|
204
|
-
menushown = false;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
else if (buttonelement?.tagName !== 'INPUT') {
|
|
208
|
-
e.preventDefault();
|
|
209
|
-
menushown = true;
|
|
210
|
-
tick().then(() => { move(firstactive); }).catch(console.error);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
else if (menushown && e.key === 'Escape') {
|
|
214
|
-
e.preventDefault();
|
|
215
|
-
e.stopPropagation();
|
|
216
|
-
menushown = false;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
let blurTimer;
|
|
220
|
-
function onbuttonclick(e) {
|
|
221
|
-
e.preventDefault();
|
|
222
|
-
cancelAnimationFrame(blurTimer);
|
|
223
|
-
menushown = !menushown;
|
|
224
|
-
}
|
|
225
|
-
function onblur(e) {
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let blurTimer: number
|
|
217
|
+
function onbuttonclick (e: MouseEvent) {
|
|
218
|
+
e.preventDefault()
|
|
219
|
+
cancelAnimationFrame(blurTimer)
|
|
220
|
+
menushown = !menushown
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function onblur (e: FocusEvent) {
|
|
226
224
|
// tabindex=-1 on our menu elements means e.relatedTarget will be set
|
|
227
225
|
if (!(e.relatedTarget instanceof HTMLElement && menuelement?.contains(e.relatedTarget))) {
|
|
228
|
-
|
|
229
|
-
menushown = false; });
|
|
226
|
+
blurTimer = requestAnimationFrame(() => { if (buttonelement !== document.activeElement) menushown = false })
|
|
230
227
|
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
observers = []
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function oninput (e: InputEvent) {
|
|
231
|
+
if (document.activeElement === buttonelement) menushown = true
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function cleanup (element: HTMLElement | undefined) {
|
|
235
|
+
for (const observer of observers) observer.disconnect()
|
|
236
|
+
observers = []
|
|
240
237
|
if (element) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
238
|
+
element.removeEventListener('click', onbuttonclick)
|
|
239
|
+
element.removeEventListener('keydown', onkeydown)
|
|
240
|
+
element.removeEventListener('blur', onblur)
|
|
241
|
+
element.removeEventListener('input', oninput)
|
|
242
|
+
element.removeAttribute('aria-disabled')
|
|
243
|
+
element.removeAttribute('aria-haspopup')
|
|
244
|
+
element.removeAttribute('aria-expanded')
|
|
245
|
+
element.removeAttribute('aria-controls')
|
|
246
|
+
element.removeAttribute('aria-activedescendant')
|
|
250
247
|
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
onDestroy(() => { cleanup(buttonelement) })
|
|
251
|
+
|
|
252
|
+
// if buttonelement changes we need to handle listeners and aria
|
|
253
|
+
let lastbuttonelement: HTMLElement | undefined
|
|
254
|
+
let observers: IntersectionObserver[] = []
|
|
255
|
+
function reactToButtonElement (buttonelement: HTMLElement | undefined) {
|
|
256
|
+
cleanup(lastbuttonelement)
|
|
257
|
+
lastbuttonelement = buttonelement
|
|
259
258
|
if (buttonelement) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
259
|
+
buttonelement.setAttribute('aria-haspopup', (usemenurole ? 'true' : 'listbox'))
|
|
260
|
+
buttonelement.setAttribute('aria-expanded', menushown ? 'true' : 'false')
|
|
261
|
+
if (!usemenurole) {
|
|
262
|
+
buttonelement.setAttribute('role', 'combobox')
|
|
263
|
+
}
|
|
264
|
+
buttonelement.addEventListener('click', onbuttonclick)
|
|
265
|
+
buttonelement.addEventListener('keydown', onkeydown)
|
|
266
|
+
buttonelement.addEventListener('blur', onblur)
|
|
267
|
+
buttonelement.addEventListener('input', oninput)
|
|
268
|
+
const parents = getScrollParents(buttonelement)
|
|
269
|
+
for (const parent of [...parents, undefined]) {
|
|
270
|
+
const observer = new IntersectionObserver(entries => {
|
|
271
|
+
for (const entry of entries) {
|
|
272
|
+
if (entry.intersectionRatio < 0.1) menushown = false
|
|
273
|
+
}
|
|
274
|
+
}, { root: parent, threshold: 0.1 })
|
|
275
|
+
observer.observe(buttonelement)
|
|
276
|
+
observers.push(observer)
|
|
277
|
+
}
|
|
280
278
|
}
|
|
281
|
-
}
|
|
282
|
-
$: reactToButtonElement(buttonelement)
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
e.
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
menushown = false
|
|
289
|
-
value = item.value
|
|
290
|
-
dispatch('change', item)
|
|
291
|
-
}
|
|
292
|
-
|
|
279
|
+
}
|
|
280
|
+
$: reactToButtonElement(buttonelement)
|
|
281
|
+
|
|
282
|
+
const onclick = (item: MenuItem) => (e: MouseEvent) => {
|
|
283
|
+
e.stopPropagation()
|
|
284
|
+
e.preventDefault()
|
|
285
|
+
if (item.disabled) return
|
|
286
|
+
menushown = false
|
|
287
|
+
value = item.value
|
|
288
|
+
dispatch('change', item)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
$: hasSelected = showSelected && !hideSelectedIndicator && items.some(itm => 'value' in itm && itm.value === value)
|
|
293
292
|
</script>
|
|
294
293
|
|
|
295
294
|
{#if menushown && !loading && (hasMeaningfulItems || !hideEmptyText)}
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
This component is for adding text that can be read by screen readers while remaining invisible to regular users.
|
|
3
3
|
Useful for when aria-label isn't supported with the drawback that it can show up in copy/paste selections.
|
|
4
4
|
-->
|
|
5
|
-
<script lang="ts"
|
|
6
|
-
|
|
7
|
-
export let
|
|
8
|
-
export let
|
|
5
|
+
<script lang="ts">
|
|
6
|
+
/** An id to bind with the `<span>` used to implement this component. */
|
|
7
|
+
export let id: string | undefined = undefined
|
|
8
|
+
export let arialive: 'off' | 'assertive' | 'polite' | undefined = undefined
|
|
9
|
+
export let ariaatomic: boolean | 'false' | 'true' | undefined = undefined
|
|
9
10
|
</script>
|
|
10
11
|
|
|
11
12
|
<style>
|
package/dist/stores/toasts.d.ts
CHANGED
|
@@ -24,8 +24,8 @@ export interface ToastStorage {
|
|
|
24
24
|
suspended: boolean;
|
|
25
25
|
}
|
|
26
26
|
export declare class ToastStore extends Store<ToastStorage[]> {
|
|
27
|
-
add(toast: Toast):
|
|
28
|
-
add(msg: string, type?: Toast['type'], ttlMs?: number):
|
|
27
|
+
add(toast: Toast): void;
|
|
28
|
+
add(msg: string, type?: Toast['type'], ttlMs?: number): void;
|
|
29
29
|
close(id: string): void;
|
|
30
30
|
suspend(id: string): void;
|
|
31
31
|
resume(id: string): void;
|
package/dist/util/util.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare function classes(...classNames: (string | undefined)[]): string;
|
|
2
2
|
export declare function debounced(fn: (..._: any[]) => any, timeout: number): (...args: any[]) => void;
|
|
3
|
-
export declare function modifierKey(e:
|
|
3
|
+
export declare function modifierKey(e: KeyboardEvent): boolean;
|
|
4
4
|
export declare function selectionIsLeft(ele: HTMLInputElement | HTMLTextAreaElement): boolean;
|
|
5
|
-
export declare function getScrollParents(element: HTMLElement):
|
|
5
|
+
export declare function getScrollParents(element: HTMLElement): HTMLElement[];
|