@txstate-mws/svelte-components 1.6.11 → 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.
@@ -2,304 +2,299 @@
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">import { createEventDispatcher, onDestroy, tick } from 'svelte';
6
- import { groupby, isNull, randomid } from 'txstate-utils';
7
- import { Store } from '@txstate-mws/svelte-store';
8
- import { glue, portal } from '../actions';
9
- import ScreenReaderOnly from './ScreenReaderOnly.svelte';
10
- import { getScrollParents, modifierKey } from '../util';
11
- const dispatch = createEventDispatcher();
12
- /** The DOM element that will act as the "button" for this menu. The menu will be placed next to the
13
- button and be controlled by the button. Keyboard access will be enabled when the button has focus.
14
- This component adds all appropriate attributes (tabindex, roles, and aria) automatically. */
15
- export let buttonelement;
16
- /** The list of menu items to be shown. Parent may change this at any time based on user activity. */
17
- export let items = [];
18
- export let menushown = false;
19
- export let value = undefined;
20
- /** Control where the menu will appear. Default is to use the current viewport to make a decision to
21
- maximize potential for menu growth. */
22
- export let align = 'auto';
23
- /** When the menu is active, should it cover the button or be rendered above/below it? */
24
- export let cover = false;
25
- /** When an item is currently selected, should it appear in the menu (with a visual indicator), or
26
- simply be removed from the list (`showSelected: false`). */
27
- export let showSelected = true;
28
- /** When defined, this width will be passed through to the menu's CSS width. Use a valid CSS dimension
29
- such as 33em or 159px. Generally this will be used when you need to match the menu width to something
30
- else, like the button. If the width is static like 100% a simple CSS rule is likely more efficient. */
31
- export let width = undefined;
32
- /** Bind this prop to receive a store that is updated each time a menu placement decision is made. Example
33
- of when this is useful is when you are trying to round corners of your button and need to know whether the
34
- menu is above or below your button so you know which corners to round and which to leave square. Note that
35
- the values do not update when the menu is hidden, so you may need to rely on `menushown` as well. */
36
- export let computedalign = new Store({ valign: 'bottom', halign: 'left' });
37
- /** Create some spacing between the specified button element and the menu, in pixels */
38
- export let gap = 0;
39
- /** Set to `true` if you want the parent element's styling to increase its minHeight when necessary. Useful
40
- for parent elements not styled for variable size children and which you want override the the size of. */
41
- export let adjustparentheight = false;
42
- /** If the menu would be clipped by an `overflow: hidden`, you can set this prop and it will be placed in
43
- the specified container, or the `document.body` if you simply say `true`. Placement will still be calculated
44
- correctly since it uses positon fixed for placement. */
45
- export let usePortal = undefined;
46
- /** Useful for when your `items` need to be fetched but you want the associated element shown. Set to loading
47
- until they're ready to be displayed and the popup menu will not be shown until the `loading` bind is `true`. */
48
- export let loading = false;
49
- /** A bindable value for inspecting which item in `items` is currently highlighted as the item currently
50
- active in the list. This is not the same as `value` and the highlighted item may or may not be selected as
51
- for the `value` of the field. */
52
- export let hilited = undefined;
53
- /** The id of the <ul> element that is the displayed list of items. */
54
- export let menuid = randomid();
55
- /** When there are no items (e.g. it's a filtered search and there were no results), we still display one
56
- disabled item in the menu to let the user know what is going on. Use this prop to specify the message. */
57
- export let emptyText = undefined;
58
- export let menuContainerClass = '';
59
- export let menuClass = '';
60
- export let menuItemClass = '';
61
- export let menuItemHilitedClass = '';
62
- export let menuItemSelectedClass = '';
63
- export let menuCategoryClass = '';
64
- export let hideSelectedIndicator = false;
65
- export let hideEmptyText = false;
66
- /** If true, the role for the popup menu will be 'menu' instead of the default 'listbox' value */
67
- export let usemenurole = false;
68
- let menuelement;
69
- const itemelements = [];
70
- let firstactive = 0;
71
- let lastactive = items.length - 1;
72
- $: hasMeaningfulItems = showSelected ? !!items.length : !!items.filter(itm => itm.value !== value).length;
73
- function hiddenItem(item) {
74
- return !!item && !showSelected && item.value === value;
75
- }
76
- $: grouped = groupby(items, item => item.group ?? 'default-group');
77
- let itemsOrderedByGroup = [];
78
- function getItemIndex(item) {
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
- // 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
- }
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
- return false;
90
- });
91
- }
92
- async function reactToItems(..._) {
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)
97
- hilited = firstactive;
98
- }
99
- $: void reactToItems(items, value);
100
- async function reactToMenuShown(_) {
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
- hilited = undefined;
103
- return;
116
+ hilited = undefined
117
+ return
104
118
  }
105
- await tick();
119
+ await tick()
106
120
  if (!menushown) {
107
- buttonelement.removeAttribute('aria-controls');
108
- buttonelement.removeAttribute('aria-owns');
109
- buttonelement.setAttribute('aria-expanded', 'false');
110
- if (hilited)
111
- buttonelement.removeAttribute('aria-activedescendant');
112
- hilited = undefined;
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
- else {
115
- if (buttonelement instanceof HTMLInputElement)
116
- buttonelement.setAttribute('aria-owns', menuid);
117
- else
118
- buttonelement.setAttribute('aria-controls', menuid);
119
- buttonelement.setAttribute('aria-expanded', 'true');
120
- if (buttonelement !== document.activeElement)
121
- buttonelement.focus();
122
- }
123
- }
124
- $: void reactToMenuShown(menushown);
125
- function move(idx) {
126
- if (!menushown)
127
- return;
128
- if (itemsOrderedByGroup[idx] == null || (itemsOrderedByGroup[idx])?.disabled)
129
- return;
130
- hilited = Math.max(firstactive, Math.min(lastactive, idx));
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
- e.preventDefault();
142
- if (menushown) {
143
- let i = (hilited ?? firstactive - 1) + 1;
144
- while (itemsOrderedByGroup[i] && !isSelectable(itemsOrderedByGroup[i]))
145
- i++;
146
- move(i);
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
- else {
149
- menushown = true;
150
- tick().then(() => { move(firstactive); }).catch(console.error);
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
- else if (e.key === 'ArrowUp') {
154
- e.preventDefault();
155
- if (menushown) {
156
- let i = (hilited ?? lastactive + 1) - 1;
157
- while (itemsOrderedByGroup[i] && !isSelectable(itemsOrderedByGroup[i]))
158
- i--;
159
- move(i);
160
- }
161
- else {
162
- menushown = true;
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 {
203
- if (buttonelement.tagName !== 'INPUT') {
204
- e.preventDefault();
205
- menushown = false;
206
- }
207
- }
208
- }
209
- else {
210
- if (buttonelement.tagName !== 'INPUT') {
211
- e.preventDefault();
212
- menushown = true;
213
- tick().then(() => { move(firstactive); }).catch(console.error);
214
- }
215
- }
216
- }
217
- else if (menushown && e.key === 'Escape') {
218
- e.preventDefault();
219
- e.stopPropagation();
220
- menushown = false;
221
- }
222
- }
223
- let blurTimer;
224
- function onbuttonclick(e) {
225
- e.preventDefault();
226
- cancelAnimationFrame(blurTimer);
227
- menushown = !menushown;
228
- }
229
- 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) {
230
224
  // tabindex=-1 on our menu elements means e.relatedTarget will be set
231
225
  if (!(e.relatedTarget instanceof HTMLElement && menuelement?.contains(e.relatedTarget))) {
232
- blurTimer = requestAnimationFrame(() => { if (buttonelement !== document.activeElement)
233
- menushown = false; });
226
+ blurTimer = requestAnimationFrame(() => { if (buttonelement !== document.activeElement) menushown = false })
234
227
  }
235
- }
236
- function oninput(e) {
237
- if (document.activeElement === buttonelement)
238
- menushown = true;
239
- }
240
- function cleanup(element) {
241
- for (const observer of observers)
242
- observer.disconnect();
243
- 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 = []
244
237
  if (element) {
245
- element.removeEventListener('click', onbuttonclick);
246
- element.removeEventListener('keydown', onkeydown);
247
- element.removeEventListener('blur', onblur);
248
- element.removeEventListener('input', oninput);
249
- element.removeAttribute('aria-disabled');
250
- element.removeAttribute('aria-haspopup');
251
- element.removeAttribute('aria-expanded');
252
- element.removeAttribute('aria-controls');
253
- element.removeAttribute('aria-activedescendant');
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')
254
247
  }
255
- }
256
- onDestroy(() => { cleanup(buttonelement); });
257
- // if buttonelement changes we need to handle listeners and aria
258
- let lastbuttonelement;
259
- let observers = [];
260
- function reactToButtonElement(buttonelement) {
261
- cleanup(lastbuttonelement);
262
- lastbuttonelement = buttonelement;
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
263
258
  if (buttonelement) {
264
- buttonelement.setAttribute('aria-haspopup', (usemenurole ? 'true' : 'listbox'));
265
- buttonelement.setAttribute('aria-expanded', menushown ? 'true' : 'false');
266
- if (!usemenurole) {
267
- buttonelement.setAttribute('role', 'combobox');
268
- }
269
- buttonelement.addEventListener('click', onbuttonclick);
270
- buttonelement.addEventListener('keydown', onkeydown);
271
- buttonelement.addEventListener('blur', onblur);
272
- buttonelement.addEventListener('input', oninput);
273
- const parents = getScrollParents(buttonelement);
274
- for (const parent of [...parents, undefined]) {
275
- const observer = new IntersectionObserver((entries) => {
276
- for (const entry of entries) {
277
- if (entry.intersectionRatio < 0.1)
278
- menushown = false;
279
- }
280
- }, { root: parent, threshold: 0.1 });
281
- observer.observe(buttonelement);
282
- observers.push(observer);
283
- }
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
+ }
284
278
  }
285
- }
286
- $: reactToButtonElement(buttonelement);
287
- const onclick = (item) => (e) => {
288
- e.stopPropagation();
289
- e.preventDefault();
290
- if (item.disabled)
291
- return;
292
- menushown = false;
293
- value = item.value;
294
- dispatch('change', item);
295
- };
296
- $: hasSelected = showSelected && !hideSelectedIndicator && items.some(itm => 'value' in itm && itm.value === value);
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)
297
292
  </script>
298
293
 
299
294
  {#if menushown && !loading && (hasMeaningfulItems || !hideEmptyText)}
300
- <div use:portal={usePortal === true ? undefined : (usePortal || null)} use:glue={{ target: buttonelement, align, cover, gap, adjustparentheight, store: computedalign }} class={menuContainerClass}>
295
+ <div use:portal={usePortal === true ? undefined : (usePortal ?? null)} use:glue={{ target: buttonelement, align, cover, gap, adjustparentheight, store: computedalign }} class={menuContainerClass}>
301
296
  <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]}
297
+ {#each Object.entries(grouped) as [group, groupitems] (group)}
303
298
  {#if group === 'default-group'}
304
299
  {#each groupitems as item, i (item.value)}
305
300
  {@const itemIndex = getItemIndex(item)}