@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.
@@ -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">import { createEventDispatcher, onMount } from 'svelte';
10
- import { randomid, Cache, isNotBlank } from 'txstate-utils';
11
- import ScreenReaderOnly from './ScreenReaderOnly.svelte';
12
- import DefaultPopupMenu from './PopupMenu.svelte';
13
- import { modifierKey, selectionIsLeft } from '../util';
14
- import { SafeStore } from '@txstate-mws/svelte-store';
15
- export let name;
16
- /**
17
- * Function to pass to the component that tells it how to use the text in the text input to determine what
18
- * `PopupMenuItem[]` should be displayed in the `PopupMenu`. Items already 'selected' from the popup menu will
19
- * be tracked and automatically filtered from the popup if returned as one of the `PopupMenuItem[]` by `getOptions`. */
20
- export let getOptions;
21
- export let id = randomid();
22
- export let disabled = false;
23
- export let menuContainerClass = '';
24
- export let menuClass = '';
25
- export let menuItemClass = '';
26
- export let menuItemHilitedClass = '';
27
- export let inputClass = '';
28
- export let menuCategoryClass = '';
29
- /** The maximum number of selections allowed before making new selections is disabled. Default of 0 is unlimited. */
30
- export let maxSelections = 0;
31
- /** Consuming components may need to make decisions about what to display or return as options in the popup based
32
- * on what's already selected. This component property can be used to both pass an initial set of selections but
33
- * it can also be bound to provide a means to inspect what's selected. */
34
- export let selected = [];
35
- /** The text to be displayed in the input text box for contextual feedback on what the input text is expecting. */
36
- export let placeholder = '';
37
- /** When there are no items (e.g. it's a filtered search and there were no results), we still display one
38
- disabled item in the menu to let the user know what is going on. Use this prop to specify the message. */
39
- export let emptyText = undefined;
40
- export let usePortal = undefined;
41
- export let descid = undefined;
42
- /** You can define your own PopupMenu and pass for that to be used or accept DefaultPopupMenu. */
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;
51
- let menushown;
52
- let loading = false;
53
- let options = [];
54
- let hilitedpill;
55
- let popupvalue = undefined;
56
- let inputelement;
57
- let menuid;
58
- let announcement = '';
59
- const instructionid = randomid();
60
- const dispatch = createEventDispatcher();
61
- const optionsCache = new Cache(async (ipt) => {
62
- return await getOptions(ipt);
63
- }, { freshseconds: 5 });
64
- const selectedStore = new SafeStore(selected);
65
- $: $selectedStore = selected;
66
- $: selectedSet = new Set($selectedStore.map(s => s.value));
67
- // Stop showing our menu if the maxSelections limit has been reached.
68
- $: if (maxSelections > 1 && selected.length >= maxSelections && menushown)
69
- menushown = false;
70
- let optionsTimer;
71
- async function reactToInput(..._) {
72
- loading = true;
73
- clearTimeout(optionsTimer);
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)
74
77
  setTimeout(async () => {
75
- const saveval = inputelement?.value;
76
- const rawOptions = await optionsCache.get(saveval);
77
- if (inputelement?.value !== saveval)
78
- return; // ignore any results that are out of date
79
- options = rawOptions.filter(o => !('value' in o) || !selectedSet.has(o.value));
80
- if (typeof document !== 'undefined' && inputelement === document.activeElement && (options.length || inputelement?.value.length) && !menushown)
81
- menushown = true;
82
- loading = false;
83
- }, 250);
84
- }
85
- $: void reactToInput(getOptions, selectedSet);
86
- function addSelection(e) {
87
- inputelement.value = '';
88
- const opt = { ...e.detail, label: e.detail.label || e.detail.value };
89
- selected = maxSelections === 1 ? [opt] : [...selected, opt];
90
- popupvalue = undefined;
91
- announcement = `${maxSelections === 1 ? 'selected' : 'added'} ${opt.label}`;
92
- dispatch('change', selected);
93
- }
94
- 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) {
95
96
  if (hilitedpill === opt.value) {
96
- hilitedpill = selected[idx + nextfocus]?.value;
97
+ hilitedpill = selected[idx + nextfocus]?.value
97
98
  }
98
- selected = selected.filter(s => s.value !== opt.value);
99
- if (document.activeElement !== inputelement)
100
- inputelement.focus();
101
- announcement = `removed ${opt.label ?? opt.value}`;
102
- dispatch('change', selected);
103
- }
104
- function inputkeydown(e) {
105
- if (modifierKey(e))
106
- 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
107
107
  if (e.key === 'ArrowLeft' && selectionIsLeft(inputelement)) {
108
- const idx = hilitedpill ? selected.findIndex(s => s.value === hilitedpill) : selected.length;
109
- if (idx !== 0)
110
- hilitedpill = selected[idx - 1].value;
111
- }
112
- else if (e.key === 'ArrowRight' && selectionIsLeft(inputelement)) {
113
- if (hilitedpill)
114
- e.preventDefault();
115
- const idx = hilitedpill ? selected.findIndex(s => s.value === hilitedpill) : undefined;
116
- if (idx != null) {
117
- if (idx === selected.length - 1)
118
- hilitedpill = undefined;
119
- else
120
- hilitedpill = selected[idx + 1].value;
121
- }
122
- }
123
- else if (['Backspace', 'Delete', 'Clear', ' ', 'Enter'].includes(e.key)) {
124
- if (hilitedpill) {
125
- const idx = selected.findIndex(s => s.value === hilitedpill);
126
- removeSelection(selected[idx], idx, e.key === 'Delete' ? 1 : -1);
127
- e.preventDefault();
128
- }
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
129
127
  }
130
- else if (e.key !== 'Tab' && maxSelections > 1 && selected.length >= maxSelections) {
131
- e.preventDefault();
132
- }
133
- else if (e.key === 'ArrowDown') {
134
- hilitedpill = undefined;
135
- }
136
- }
137
- let active = false;
138
- function inputfocus() {
139
- void reactToInput();
140
- active = true;
141
- dispatch('focus');
142
- }
143
- function inputblur() {
144
- active = false;
145
- dispatch('blur');
146
- }
147
- function removeAllSelections() {
148
- selected = [];
149
- dispatch('change', selected);
150
- }
151
- 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 () {
152
147
  if (confirmDelete) {
153
- if (confirm(confirmDelete)) {
154
- removeAllSelections();
155
- }
156
- }
157
- else {
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
- let popuphilited;
162
- $: if (popuphilited != null)
163
- hilitedpill = undefined;
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
- popuphilited = undefined;
169
- inputelement.setAttribute('aria-activedescendant', id + hilitedpill);
170
- }
171
- else
172
- inputelement.removeAttribute('aria-activedescendant');
173
- }
174
- $: reactToHilite(hilitedpill, id);
175
- $: 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`;
176
- let gap = 0;
177
- onMount(() => {
178
- const ul = inputelement.closest('.multiselect-selected');
179
- if (ul != null)
180
- gap = ((ul.offsetHeight - ul.clientHeight) / 2) + parseInt(window.getComputedStyle(ul).paddingBottom);
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}>
@@ -203,7 +196,6 @@ onMount(() => {
203
196
  {/each}
204
197
  </div>
205
198
  {/if}
206
- <!-- svelte-ignore a11y-role-has-required-aria-props -->
207
199
  <input type="text" class={inputClass} {id} {name} {disabled} {placeholder}
208
200
  bind:this={inputelement} on:input={reactToInput} on:blur={inputblur}
209
201
  on:focus={inputfocus} on:keydown={inputkeydown}