Skip to content

Commit 78950d4

Browse files
committed
feat: redo autocomplete resetTextWhenSelected option, use event cancelation instead
1 parent f887d0a commit 78950d4

File tree

7 files changed

+147
-71
lines changed

7 files changed

+147
-71
lines changed

MIGRATING.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@ https://github.com/material-components/material-components-web/blob/master/CHANG
1414
- Internally, they are replaced with the `SmuiElement` component exported from `@smui/common`. It takes a `tag` prop and creates an element dynamically with that tag name. You shouldn't ever need to use `SmuiElement` directly.
1515
- The "\*ComponentDev" types (like `MenuComponentDev`) are gone. You can now use the component as its type. Components that can take a `component` or `tag` prop (like `Button`) have required generic arguments that you can get around by using "InstanceType", like `let button: InstanceType<typeof Button>;`.
1616
- If you're using `classAdderBuilder`, you need to use `keyof SmuiElementMap` instead of `string` as its generic argument.
17+
- The `dispatch` function in `@smui/common` will now throw an error if either the `Event` object is not available or the `element` is not provided.
1718

1819
## Changes
1920

2021
### Components
2122

23+
- Autocomplete
24+
- The `SMUIAutocomplete:selected` event is now cancelable.
2225
- Banner
2326
- New `autoClose` prop.
2427
- New `SMUIBanner:actionClicked` event. It is fired when an action is clicked and `autoClose` is `false`.
@@ -32,6 +35,8 @@ https://github.com/material-components/material-components-web/blob/master/CHANG
3235
- List
3336
- New `disabledItemsFocusable` prop.
3437
- New `wrapper` prop on Item. This should be used for items that only act as the container of a nested list.
38+
- Menu
39+
- New `SMUIMenu:closedProgrammatically` event.
3540
- Menu Surface
3641
- New `SMUIMenuSurface:opening` event.
3742
- New `openBottomBias` prop.

packages/autocomplete/src/Autocomplete.svelte

Lines changed: 55 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,6 @@
4040
anchor={menu$anchor}
4141
anchorCorner={menu$anchorCorner}
4242
on:SMUIList:mount={handleListAccessor}
43-
on:SMUIMenu:closedProgrammatically={() => {
44-
if (resetTextWhenSelected) {
45-
text = '';
46-
focus();
47-
} else {
48-
hideMenu = true;
49-
}
50-
}}
5143
{...prefixFilter($$restProps, 'menu$')}
5244
>
5345
<List {...prefixFilter($$restProps, 'list$')}>
@@ -170,7 +162,6 @@
170162
export let selectOnExactMatch = true;
171163
export let showMenuWithNoInput = true;
172164
export let noMatchesActionDisabled = true;
173-
export let resetTextWhenSelected = false;
174165
export let search: (input: string) => Promise<any[] | false> = async (
175166
input: string
176167
) => {
@@ -211,72 +202,46 @@
211202
let matches: any[] = [];
212203
let focusedIndex = -1;
213204
let focusedItem: SMUIListItemAccessor | undefined = undefined;
214-
let itemHasBeenSelected: boolean = false;
215-
let hideMenu: boolean = false;
216205
217206
$: menuOpen =
218207
focused &&
219-
!hideMenu &&
220208
(text !== '' || showMenuWithNoInput) &&
221209
(loading ||
222210
(!combobox && !(matches.length === 1 && matches[0] === value)) ||
223211
(combobox &&
224212
!!matches.length &&
225213
!(matches.length === 1 && matches[0] === value)));
226214
227-
let previousText: string | undefined = undefined;
215+
let previousText = text;
228216
$: if (previousText !== text) {
229-
if (!itemHasBeenSelected) {
230-
hideMenu = false;
231-
}
232217
if (!combobox && value != null && getOptionLabel(value) !== text) {
233218
deselectOption(value, false);
234219
}
235220
236-
(async () => {
237-
loading = true;
238-
error = false;
239-
try {
240-
const searchResult = await search(text);
241-
if (searchResult !== false) {
242-
matches = searchResult;
243-
if (selectOnExactMatch) {
244-
const exactMatch = matches.find(
245-
(match) => getOptionLabel(match) === text
246-
);
247-
if (exactMatch && value !== exactMatch) {
248-
selectOption(exactMatch);
249-
}
250-
}
251-
}
252-
} catch (e: any) {
253-
error = true;
254-
}
255-
loading = false;
256-
})();
221+
performSearch();
257222
258-
if (itemHasBeenSelected) {
259-
if (resetTextWhenSelected) {
260-
text = '';
261-
focus();
262-
}
263-
itemHasBeenSelected = false;
264-
}
265223
previousText = text;
266224
}
267225
226+
$: if (options) {
227+
// Set search results on init and refresh search results when `options` is
228+
// changed.
229+
performSearch();
230+
}
231+
268232
let previousValue = value;
269233
$: if (!combobox && previousValue !== value) {
270234
// If the value changes from outside, update the text.
271235
text = getOptionLabel(value);
272236
previousValue = value;
273-
itemHasBeenSelected = true;
274-
if (!resetTextWhenSelected) {
275-
hideMenu = true;
276-
}
277-
} else if (combobox) {
278-
// If the text changes, update value if we're a combobox.
237+
} else if (combobox && previousValue !== value) {
238+
// An update came from the outside.
239+
text = value;
240+
previousValue = value;
241+
} else if (combobox && value !== text) {
242+
// An update came from the user.
279243
value = text;
244+
previousValue = value;
280245
}
281246
282247
let previousFocusedIndex: number | undefined = undefined;
@@ -314,32 +279,70 @@
314279
previousFocusedIndex = focusedIndex;
315280
}
316281
282+
async function performSearch() {
283+
loading = true;
284+
error = false;
285+
try {
286+
const searchResult = await search(text);
287+
if (searchResult !== false) {
288+
matches = searchResult;
289+
if (selectOnExactMatch) {
290+
const exactMatch = matches.find(
291+
(match) => getOptionLabel(match) === text
292+
);
293+
if (exactMatch && value !== exactMatch) {
294+
selectOption(exactMatch);
295+
}
296+
}
297+
}
298+
} catch (e: any) {
299+
error = true;
300+
}
301+
loading = false;
302+
}
303+
317304
function handleListAccessor(event: CustomEvent<SMUIListAccessor>) {
318305
if (!listAccessor) {
319306
listAccessor = event.detail;
320307
}
321308
}
322309
323310
function selectOption(option: any, setText = true) {
311+
const event = dispatch(element, 'SMUIAutocomplete:selected', option, {
312+
bubbles: true,
313+
cancelable: true,
314+
});
315+
316+
if (event.defaultPrevented) {
317+
return;
318+
}
319+
324320
if (setText) {
325321
text = getOptionLabel(option);
326322
}
327323
value = option;
328324
if (!setText) {
329325
previousValue = option;
330326
}
331-
dispatch(element, 'SMUIAutocomplete:selected', option);
332327
}
333328
334329
function deselectOption(option: any, setText = true) {
330+
const event = dispatch(element, 'SMUIAutocomplete:deselected', option, {
331+
bubbles: true,
332+
cancelable: true,
333+
});
334+
335+
if (event.defaultPrevented) {
336+
return;
337+
}
338+
335339
if (setText) {
336340
text = '';
337341
}
338342
value = undefined;
339343
if (!setText) {
340344
previousValue = undefined;
341345
}
342-
dispatch(element, 'SMUIAutocomplete:deselected', option);
343346
}
344347
345348
function toggleOption(option: any) {

packages/common/src/internal/dispatch.ts

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,29 @@ export function dispatch<T extends any = any>(
66
/** This is an internal thing used by SMUI to duplicate some SMUI events as MDC events. */
77
duplicateEventForMDC = false
88
) {
9-
if (typeof Event !== 'undefined' && element) {
10-
const event: CustomEvent<T> = new CustomEvent(eventType, {
11-
...eventInit,
12-
detail,
13-
});
14-
element?.dispatchEvent(event);
15-
if (duplicateEventForMDC && eventType.startsWith('SMUI')) {
16-
const duplicateEvent: CustomEvent<T> = new CustomEvent(
17-
eventType.replace(/^SMUI/g, () => 'MDC'),
18-
{
19-
...eventInit,
20-
detail,
21-
}
22-
);
23-
element?.dispatchEvent(duplicateEvent);
24-
if (duplicateEvent.defaultPrevented) {
25-
event.preventDefault();
9+
if (typeof Event === 'undefined') {
10+
throw new Error('Event not defined.');
11+
}
12+
if (!element) {
13+
throw new Error('Tried to dipatch event without element.');
14+
}
15+
const event: CustomEvent<T> = new CustomEvent(eventType, {
16+
...eventInit,
17+
detail,
18+
});
19+
element?.dispatchEvent(event);
20+
if (duplicateEventForMDC && eventType.startsWith('SMUI')) {
21+
const duplicateEvent: CustomEvent<T> = new CustomEvent(
22+
eventType.replace(/^SMUI/g, () => 'MDC'),
23+
{
24+
...eventInit,
25+
detail,
2626
}
27+
);
28+
element?.dispatchEvent(duplicateEvent);
29+
if (duplicateEvent.defaultPrevented) {
30+
event.preventDefault();
2731
}
28-
return event;
2932
}
33+
return event;
3034
}

packages/menu/src/Menu.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
element.classList.contains(className),
8181
closeSurface: (skipRestoreFocus) => {
8282
menuSurfaceAccessor.closeProgrammatic(skipRestoreFocus);
83-
dispatch(getElement(), 'SMUIMenu:closedProgrammatically', {});
83+
dispatch(getElement(), 'SMUIMenu:closedProgrammatically');
8484
},
8585
getElementIndex: (element) =>
8686
listAccessor

packages/site/src/routes/demo/autocomplete/+page.svelte

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@
3333
Adding entries
3434
</Demo>
3535

36+
<Demo component={AddToList} file="autocomplete/_AddToList.svelte">
37+
Add entries to a list
38+
<svelte:fragment slot="subtitle">
39+
Leave the menu open and don't fill the textbox upon selection.
40+
</svelte:fragment>
41+
</Demo>
42+
3643
<Demo component={Async} file="autocomplete/_Async.svelte">
3744
Async options loading
3845
<svelte:fragment slot="subtitle">
@@ -54,6 +61,7 @@
5461
import Combobox from './_Combobox.svelte';
5562
import Objects from './_Objects.svelte';
5663
import AddEntries from './_AddEntries.svelte';
64+
import AddToList from './_AddToList.svelte';
5765
import Async from './_Async.svelte';
5866
import Manual from './_Manual.svelte';
5967
</script>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<div>
2+
<div class="status">
3+
<pre style="display: inline-block;">Selected:</pre>
4+
<Set style="display: inline-block;" bind:chips={selected} let:chip>
5+
<Chip {chip}>
6+
<Text tabindex={0}>{chip}</Text>
7+
<TrailingAction icon$class="material-icons">cancel</TrailingAction>
8+
</Chip>
9+
</Set>
10+
</div>
11+
12+
<Autocomplete
13+
bind:this={selector}
14+
options={available}
15+
bind:value
16+
label="Fruit"
17+
on:SMUIAutocomplete:selected={handleSelection}
18+
/>
19+
</div>
20+
21+
<script lang="ts">
22+
import Autocomplete from '@smui-extra/autocomplete';
23+
24+
import Chip, { Set, TrailingAction, Text } from '@smui/chips';
25+
26+
let fruits = ['Apple', 'Orange', 'Banana', 'Mango'];
27+
let selected: string[] = [];
28+
29+
$: available = fruits.filter((value) => !selected.includes(value));
30+
31+
let selector: Autocomplete;
32+
let value = '';
33+
34+
function handleSelection(event: CustomEvent<string>) {
35+
// Don't actually select the item.
36+
event.preventDefault();
37+
38+
// You could also set value back to '' here.
39+
selected.push(event.detail);
40+
// Make sure the chips get updated.
41+
selected = selected;
42+
43+
selector.focus();
44+
}
45+
</script>

packages/site/src/routes/demo/autocomplete/_Combobox.svelte

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
<div>
22
<Autocomplete combobox options={fruits} bind:value label="Fruit" />
33
<pre class="status">Selected: {value || ''}</pre>
4+
5+
<div style="margin-top: 1em;">
6+
<div>Programmatically select:</div>
7+
<Button on:click={() => (value = 'Dragonfruit')}>
8+
<Label>Dragonfruit</Label>
9+
</Button>
10+
<Button on:click={() => (value = 'Elderberry')}>
11+
<Label>Elderberry</Label>
12+
</Button>
13+
</div>
414
</div>
515

616
<script lang="ts">
717
import Autocomplete from '@smui-extra/autocomplete';
18+
import Button, { Label } from '@smui/button';
819
920
let fruits = [
1021
'Apple',

0 commit comments

Comments
 (0)