Skip to content

Commit 1bf7083

Browse files
Merge pull request #95 from CaptainCodeman/change-event
Use "change" event when selected item changes
2 parents e058146 + ade2eb6 commit 1bf7083

File tree

20 files changed

+87
-55
lines changed

20 files changed

+87
-55
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ But also:
1717
- ✅ Designed to integrate beautifully with Svelte and SvelteKit
1818
- ✅ Less than 14kB minified / 4kB minified gzipped
1919

20+
## BREAKING CHANGE (v0.0.39)
21+
22+
The latest 0.0.39 release switches to using "change" instead of "select" as the event fired when the selected item changes for semantic correctness and to better match native inputs. Be sure to change listeners from `on:select` to `on:change`!
23+
2024
## Installation
2125

2226
Install using your package manager of choice, e.g.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "svelte-headlessui",
33
"description": "HeadlessUI components for Svelte",
4-
"version": "0.0.38",
4+
"version": "0.0.39",
55
"type": "module",
66
"keywords": [
77
"svelte",

src/docs/03-combobox.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ Comboboxes are the foundation of accessible autocompletes and command palettes f
4747
4848
const combobox = createCombobox({ label: 'Actions', selected: people[2] })
4949
50-
function onSelect(e: Event) {
51-
console.log('select', (e as CustomEvent).detail)
50+
function onChange(e: Event) {
51+
console.log('select', (e as CustomEvent).detail.selected)
5252
}
5353
5454
$: filtered = people.filter((person) =>
@@ -67,7 +67,7 @@ Comboboxes are the foundation of accessible autocompletes and command palettes f
6767
>
6868
<input
6969
use:combobox.input
70-
on:select={onSelect}
70+
on:change={onChange}
7171
class="w-full border-none py-2 pl-3 pr-10 leading-5 text-gray-900 focus:ring-0"
7272
value={$combobox.selected.name}
7373
/>

src/docs/04-combobox-multi.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ Pass an array to the `selected` property of `createCombobox` to trigger multi-se
5252
5353
const combobox = createCombobox({ label: 'People', selected: [people[2], people[3]] })
5454
55-
function onSelect(e: Event) {
55+
function onChange(e: Event) {
5656
console.log('select', (e as CustomEvent).detail)
5757
}
5858
@@ -69,7 +69,7 @@ Pass an array to the `selected` property of `createCombobox` to trigger multi-se
6969
<span class="inline-block w-full rounded-md shadow-sm">
7070
<button
7171
use:combobox.button
72-
on:select={onSelect}
72+
on:change={onChange}
7373
class="focus:shadow-outline-teal relative w-full cursor-default rounded-md border border-gray-300 bg-white py-2 pl-2 pr-10 text-left text-sm transition duration-150 ease-in-out focus:border-teal-300 focus:outline-none sm:leading-5"
7474
>
7575
<div class="flex flex-wrap gap-2">
@@ -85,7 +85,7 @@ Pass an array to the `selected` property of `createCombobox` to trigger multi-se
8585
{/each}
8686
<input
8787
use:combobox.input
88-
on:select={onSelect}
88+
on:change={onChange}
8989
placeholder="Search&hellip;"
9090
class="w-auto border-none py-1 text-sm leading-5 text-gray-900 focus:ring-0"
9191
/>

src/docs/07-listbox.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ Listboxes are a great foundation for building custom, accessible select menus fo
4747
4848
const listbox = createListbox({ label: 'Actions', selected: people[2] })
4949
50-
function onSelect(e: Event) {
50+
function onChange(e: Event) {
5151
console.log('select', (e as CustomEvent).detail)
5252
}
5353
</script>
@@ -57,7 +57,7 @@ Listboxes are a great foundation for building custom, accessible select menus fo
5757
<div class="relative mt-1">
5858
<button
5959
use:listbox.button
60-
on:select={onSelect}
60+
on:change={onChange}
6161
class="relative w-full cursor-default rounded-lg bg-white py-2 pl-3 pr-10 text-left text-sm shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300"
6262
>
6363
<span class="block truncate">{$listbox.selected.name}</span>

src/docs/08-listbox-multi.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ Pass an array to the `selected` property of `createListbox` to trigger multi-sel
5252
5353
const listbox = createListbox({ label: 'Actions', selected: [people[2], people[3]] })
5454
55-
function onSelect(e: Event) {
55+
function onChange(e: Event) {
5656
console.log('select', (e as CustomEvent).detail)
5757
}
5858
</script>
@@ -62,7 +62,7 @@ Pass an array to the `selected` property of `createListbox` to trigger multi-sel
6262
<span class="inline-block w-full rounded-md shadow-sm">
6363
<button
6464
use:listbox.button
65-
on:select={onSelect}
65+
on:change={onChange}
6666
class="focus:shadow-outline-orange relative w-full cursor-default rounded-md border border-gray-300 bg-white py-2 pl-2 pr-10 text-left text-sm transition duration-150 ease-in-out focus:border-orange-300 focus:outline-none sm:leading-5"
6767
>
6868
<div class="flex flex-wrap gap-2">

src/docs/09-menu.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ Menus offer an easy way to build custom, accessible dropdown components with rob
4141
4242
const menu = createMenu({ label: 'Actions' })
4343
44-
function onSelect(e: Event) {
44+
function onChange(e: Event) {
4545
console.log('select', (e as CustomEvent).detail)
4646
}
4747
@@ -64,7 +64,7 @@ Menus offer an easy way to build custom, accessible dropdown components with rob
6464
<div class="relative inline-block text-left">
6565
<button
6666
use:menu.button
67-
on:select={onSelect}
67+
on:change={onChange}
6868
class="inline-flex w-full justify-center rounded-md bg-black bg-opacity-20 px-4 py-2 text-sm font-medium text-white hover:bg-opacity-30 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
6969
>
7070
Options

src/lib/combobox.ts

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import {
3333
removeItem,
3434
type ItemOptions,
3535
type List,
36-
raiseSelectOnChange,
36+
raiseChangeOnSelect,
3737
} from './internal/list'
3838
import { ensureID } from './internal/new-id'
3939
import { onClick } from './internal/on-click'
@@ -52,10 +52,12 @@ import { keyEnter } from './internal/key-enter'
5252
import { keyNavigation } from './internal/key-navigation'
5353
import { noop } from './internal/noop'
5454
import { keyBackspaceAllow } from './internal/key-backspace'
55+
import { onChange } from './internal/on-change'
56+
import { blockDefaultAction } from './internal/events'
5557

5658
// TODO: add "value" selector, to pick text value off list item objects
5759
export interface Combobox extends Labelable, Expandable, Controllable, List, Selectable {
58-
input?: HTMLElement
60+
input?: HTMLInputElement
5961
button?: HTMLElement
6062
filter: string
6163
moved: boolean // whether we have moved active or not (to reset when filtering)
@@ -136,11 +138,21 @@ export function createCombobox(init?: Partial<Combobox>) {
136138

137139
const setFocusToInput = () => state.input?.focus()
138140

141+
const setSelecttionToEnd = () => {
142+
if (state.input) {
143+
state.input.selectionStart = state.input.value.length
144+
state.input.selectionEnd = state.input.value.length
145+
state.input.focus()
146+
}
147+
}
148+
139149
const filter = async (value: string) => {
140150
// current active item
141151
const current = state.active === -1 ? state.selected : state.items[state.active].value
142152

143-
set({ filter: value, expanded: true, opened: true }) // keep expanded or expand if filter is set
153+
// keep expanded or expand if filter is set
154+
// clear selected if input is cleared
155+
set({ filter: value, expanded: true, opened: true, selected: value ? state.selected : null })
144156

145157
await tick()
146158

@@ -172,7 +184,7 @@ export function createCombobox(init?: Partial<Combobox>) {
172184

173185
const select = () => set(selectActive(state))
174186

175-
function input(node: HTMLElement) {
187+
function input(node: HTMLInputElement) {
176188
ensureID(node, prefix)
177189
set({ input: node })
178190

@@ -192,10 +204,11 @@ export function createCombobox(init?: Partial<Combobox>) {
192204
keyBackspaceAllow(del),
193205
),
194206
onInput(filter),
207+
onChange(blockDefaultAction),
195208
// NOTE: button might be a container of the input, or sibling of the input, depending on multi-select
196209
onClick(state.multi ? noop : toggle),
197210
focusOnClose(store),
198-
raiseSelectOnChange(store),
211+
raiseChangeOnSelect(store),
199212
])
200213

201214
return {
@@ -236,7 +249,17 @@ export function createCombobox(init?: Partial<Combobox>) {
236249
setTabIndex(-1),
237250
onClickOutside(() => (state.expanded ? [state.input, state.button, node] : null), close),
238251
onClick(
239-
activate('[role="option"]', focusNode, select, state.multi ? setFocusToInput : close),
252+
activate(
253+
'[role="option"]',
254+
focusNode,
255+
select,
256+
state.multi
257+
? setFocusToInput
258+
: () => {
259+
close()
260+
setSelecttionToEnd()
261+
},
262+
),
240263
),
241264
onPointerMoveChild('[role="option"]', focusNode),
242265
reflectAriaActivedescendent(store),

src/lib/internal/events.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,11 @@ export const listener = <K extends keyof HTMLElementEventMap>(
88
node.addEventListener(type, handler, capture)
99
return () => node.removeEventListener(type, handler, capture)
1010
}
11+
12+
export const blockDefaultAction = (event: Event) => {
13+
if (event.isTrusted) {
14+
event.preventDefault()
15+
event.stopPropagation()
16+
event.stopImmediatePropagation()
17+
}
18+
}

src/lib/internal/key-handler.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
11
import type { Callable } from './callable'
2+
import { blockDefaultAction } from './events'
23

34
export type KeyHandler = (event: KeyboardEvent) => void
45

5-
export const blockDefaultKeyAction = (event: KeyboardEvent) => {
6-
event.preventDefault()
7-
event.stopPropagation()
8-
event.stopImmediatePropagation()
9-
}
10-
116
// eslint-disable-next-line @typescript-eslint/no-unused-vars
127
export const allowDefaultKeyAction = (_event: KeyboardEvent) => {}
138

149
export const keyHandler =
15-
(matches: string[], action: KeyHandler = blockDefaultKeyAction) =>
10+
(matches: string[], action: KeyHandler = blockDefaultAction) =>
1611
(...fns: Callable[]): KeyHandler =>
1712
(event) => {
1813
if (matches.includes(event.key)) {

0 commit comments

Comments
 (0)