Skip to content

Commit 4e64b50

Browse files
committed
♿️ Improve Select and Popover accessibility
1 parent c129bcb commit 4e64b50

File tree

14 files changed

+136
-20
lines changed

14 files changed

+136
-20
lines changed

scripts/utilityTypes.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ export type Popover = {
105105
position?: PopoverPosition
106106
offset?: number
107107
closeOnBlur?: boolean
108+
closeOnEsc?: boolean
108109
onOpen?: (args: PopoverCallback) => unknown
109110
onClose?: (args: PopoverCallback) => unknown
110111
}
@@ -139,7 +140,7 @@ declare module 'webcoreui' {
139140
140141
export const get: (selector: string, all: boolean) => Element | NodeListOf<Element> | null
141142
export const on: (
142-
selector: string | HTMLElement,
143+
selector: string | HTMLElement | Document,
143144
event: string,
144145
callback: any,
145146
all?: boolean

src/components/List/list.module.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
@include spacing(m0);
5151

5252
padding-bottom: 0;
53+
user-select: none;
5354

5455
&:not(:first-child) {
5556
@include border(top, primary-50);
@@ -74,7 +75,9 @@
7475

7576
span {
7677
@include typography(md, primary-20);
78+
7779
pointer-events: none;
80+
user-select: none;
7881
}
7982
}
8083
}

src/components/Select/Select.astro

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,21 @@ const inputRestProps = Object.fromEntries(
7676

7777
<script>
7878
import { debounce } from '../../utils/debounce'
79+
import { on } from '../../utils/DOMUtils'
7980
import { dispatch, listen } from '../../utils/event'
8081
import { modal } from '../../utils/modal'
8182
import { closePopover, popover, type PopoverPosition } from '../../utils/popover'
8283

8384
const selects = document.querySelectorAll('[data-id^="w-select"]')
85+
let focusByTab = false
86+
87+
on(document, 'keydown', (event: KeyboardEvent) => {
88+
if (event.key === 'Tab') {
89+
focusByTab = true
90+
}
91+
})
92+
93+
on(document, 'mousedown', () => focusByTab = false)
8494

8595
Array.from(selects).forEach(select => {
8696
const selectElement = select as HTMLElement
@@ -97,6 +107,9 @@ const inputRestProps = Object.fromEntries(
97107
if (search) {
98108
search.focus()
99109
}
110+
},
111+
onClose(event) {
112+
dispatch('selectOnClose', event)
100113
}
101114
})
102115
} else {
@@ -120,10 +133,21 @@ const inputRestProps = Object.fromEntries(
120133

121134
if (search) {
122135
search.focus()
136+
} else {
137+
popover.focus()
123138
}
139+
},
140+
onClose(event) {
141+
dispatch('selectOnClose', event)
124142
}
125143
})
126144
}
145+
146+
on(selectElement, 'focus', (event: Event) => {
147+
if (focusByTab) {
148+
(event.currentTarget as HTMLInputElement).click()
149+
}
150+
})
127151
})
128152

129153
listen('listOnSelect', payload => {

src/components/Select/Select.svelte

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
1010
import { classNames } from '../../utils/classNames'
1111
import { debounce } from '../../utils/debounce'
12+
import { on } from '../../utils/DOMUtils'
1213
import { modal } from '../../utils/modal'
1314
import { closePopover, popover, type PopoverPosition } from '../../utils/popover'
1415
@@ -28,8 +29,11 @@
2829
export let position: SvelteSelectProps['position'] = 'bottom'
2930
export let className: SvelteSelectProps['className'] = ''
3031
export let onChange: SvelteSelectProps['onChange'] = () => {}
32+
export let onClose: SvelteSelectProps['onClose'] = () => {}
3133
3234
let popoverInstance: any
35+
let val: string
36+
let focusByTab = false
3337
3438
const classes = classNames([
3539
styles.select,
@@ -47,6 +51,8 @@
4751
.flat()
4852
.find((item: ListProps['itemGroups'][0]['items'][0]) => item.value === value)?.name
4953
54+
val = (value && inferredValue) ? inferredValue : value
55+
5056
const inputRestProps = Object.fromEntries(
5157
Object.entries($$restProps).filter(([key]) => key.includes('data'))
5258
)
@@ -55,7 +61,7 @@
5561
closePopover(`.w-options-${name}`)
5662
5763
if (updateValue) {
58-
value = event.name
64+
val = event.name
5965
}
6066
6167
onChange?.({
@@ -67,6 +73,14 @@
6773
onMount(() => {
6874
let observer: ResizeObserver | undefined
6975
76+
on(document, 'keydown', (event: KeyboardEvent) => {
77+
if (event.key === 'Tab') {
78+
focusByTab = true
79+
}
80+
})
81+
82+
on(document, 'mousedown', () => focusByTab = false)
83+
7084
if (position === 'modal') {
7185
modal({
7286
trigger: `.w-select-${name}`,
@@ -77,6 +91,9 @@
7791
if (search) {
7892
search.focus()
7993
}
94+
},
95+
onClose(event) {
96+
onClose?.(event)
8097
}
8198
})
8299
} else {
@@ -102,10 +119,19 @@
102119
if (search) {
103120
search.focus()
104121
}
122+
},
123+
onClose(event) {
124+
onClose?.(event)
105125
}
106126
})
107127
}
108128
129+
on(`.w-select-${name}`, 'focus', (event: Event) => {
130+
if (focusByTab) {
131+
(event.currentTarget as HTMLInputElement).click()
132+
}
133+
})
134+
109135
return () => {
110136
popoverInstance?.remove()
111137
observer?.unobserve(document.body)
@@ -115,7 +141,7 @@
115141

116142
<Input
117143
type="text"
118-
value={(value && inferredValue) ? inferredValue : value}
144+
value={val}
119145
readonly={true}
120146
disabled={disabled}
121147
placeholder={placeholder || null}

src/components/Select/Select.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Popover from '../Popover/Popover.tsx'
88

99
import { classNames } from '../../utils/classNames'
1010
import { debounce } from '../../utils/debounce'
11+
import { on } from '../../utils/DOMUtils'
1112
import { modal } from '../../utils/modal'
1213
import { closePopover, popover, type PopoverPosition } from '../../utils/popover'
1314

@@ -28,6 +29,7 @@ const Select = ({
2829
position = 'bottom',
2930
className,
3031
onChange,
32+
onClose,
3133
...rest
3234
}: ReactSelectProps) => {
3335
const inferredValue = rest.itemGroups.map(group => group.items)
@@ -51,6 +53,7 @@ const Select = ({
5153
)
5254

5355
let popoverInstance: any
56+
let focusByTab = false
5457

5558
const select = (event: ListEventType) => {
5659
closePopover(`.w-options-${name}`)
@@ -68,6 +71,14 @@ const Select = ({
6871
useEffect(() => {
6972
let observer: ResizeObserver | undefined
7073

74+
on(document, 'keydown', (event: KeyboardEvent) => {
75+
if (event.key === 'Tab') {
76+
focusByTab = true
77+
}
78+
})
79+
80+
on(document, 'mousedown', () => focusByTab = false)
81+
7182
if (position === 'modal') {
7283
modal({
7384
trigger: `.w-select-${name}`,
@@ -78,6 +89,9 @@ const Select = ({
7889
if (search) {
7990
search.focus()
8091
}
92+
},
93+
onClose(event) {
94+
onClose?.(event)
8195
}
8296
})
8397
} else {
@@ -104,11 +118,20 @@ const Select = ({
104118
if (search) {
105119
search.focus()
106120
}
121+
},
122+
onClose(event) {
123+
onClose?.(event)
107124
}
108125
})
109126
}, 0)
110127
}
111128

129+
on(`.w-select-${name}`, 'focus', (event: Event) => {
130+
if (focusByTab) {
131+
(event.currentTarget as HTMLInputElement).click()
132+
}
133+
})
134+
112135
return () => {
113136
popoverInstance?.remove()
114137
observer?.unobserve(document.body)

src/components/Select/select.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { PopoverPosition } from '../../utils/popover'
1+
import type { ModalCallback } from '../../utils/modal'
2+
import type { PopoverCallback, PopoverPosition } from '../../utils/popover'
23

34
import type { ListEventType, ListProps } from '../List/list'
45

@@ -19,8 +20,10 @@ export type SelectProps = {
1920

2021
export type SvelteSelectProps = {
2122
onChange?: (event: SelectEventType) => void
23+
onClose?: (event: ModalCallback | PopoverCallback) => void
2224
} & SelectProps
2325

2426
export type ReactSelectProps = {
2527
onChange?: (event: SelectEventType) => void
28+
onClose?: (event: ModalCallback | PopoverCallback) => void
2629
} & SelectProps

src/pages/blocks/device-mockup.astro

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,4 @@ const sections = getSections({
132132

133133
max-width: 300px;
134134
}
135-
136-
// .toggle {
137-
// @include typography(md);
138-
// @include spacing(py-xs, px-sm);
139-
// }
140135
</style>

src/pages/components/popover.astro

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,8 @@ const sections = getSections({
245245
trigger: `#popover-btn-${variant}`,
246246
popover: `#popover-${variant}`,
247247
position: button?.dataset.pos as any || 'bottom',
248-
closeOnBlur: !button?.dataset.noClose
248+
closeOnBlur: !button?.dataset.noClose,
249+
closeOnEsc: !button?.dataset.noClose
249250
})
250251

251252
if (removeButton) {

src/pages/components/select.astro

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,9 @@ const sections = getSections({
134134
// eslint-disable-next-line no-console
135135
console.log(event)
136136
})
137+
138+
listen('selectOnClose', event => {
139+
// eslint-disable-next-line no-console
140+
console.log(event)
141+
})
137142
</script>

src/playground/ReactPlayground.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable max-lines */
12
import React, { useState } from 'react'
23

34
import Accordion from '@components/Accordion/Accordion.tsx'
@@ -216,6 +217,10 @@ const ReactPlayground = () => {
216217
searchBarPlaceholder="Filter options"
217218
className={styles.mt}
218219
onChange={payload => setSelect(`${payload.name} (${payload.value})`)}
220+
onClose={event => {
221+
// eslint-disable-next-line no-console
222+
console.log('Select closed', event)
223+
}}
219224
/>
220225

221226
<span className={styles.span}>Selected: {select}</span>

0 commit comments

Comments
 (0)