Skip to content

Commit 3bcb198

Browse files
authored
feat: Add missing a11y features (#27)
* feat: Add missing a11y features, #11 * fix: Fix minor issues, #11
1 parent 2571d5f commit 3bcb198

File tree

3 files changed

+120
-22
lines changed

3 files changed

+120
-22
lines changed

README.md

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -110,21 +110,21 @@ yarn add @untemps/svelte-use-tooltip
110110

111111
## API
112112

113-
| Props | Type | Default | Description |
114-
|---------------------------|---------|---------|-----------------------------------------------------------------------------------------------------------------|
115-
| `content` | string | null | Text content to display in the tooltip. |
116-
| `contentSelector` | string | null | Selector of the content to display in the tooltip. |
117-
| `contentClone` | boolean | null | Flag to clone the content to display in the tooltip. If false, the content is removed from its previous parent. |
118-
| `contentActions` | object | null | Configuration of the tooltip actions (see [Content Actions](#content-actions)). |
119-
| `containerClassName` | string | null | Class name to apply to the tooltip container. |
120-
| `position` | string | 'top' | Position of the tooltip. Available values: 'top', 'bottom', 'left', 'right' |
121-
| `animated` | boolean | false | Flag to animate tooltip transitions. |
122-
| `animationEnterClassName` | string | null | Class name to apply to the tooltip enter transition. |
123-
| `animationLeaveClassName` | string | null | Class name to apply to the tooltip leave transition. |
124-
| `enterDelay` | number | 0 | Delay before showing the tooltip in milliseconds. |
125-
| `leaveDelay` | number | 0 | Delay before hiding the tooltip in milliseconds. |
126-
| `offset` | number | 10 | Distance between the tooltip and the target in pixels. |
127-
| `disabled` | boolean | false | Flag to disable the tooltip content. |
113+
| Props | Type | Default | Description |
114+
|---------------------------|---------|-------------------|-----------------------------------------------------------------------------------------------------------------|
115+
| `content` | string | null | Text content to display in the tooltip. |
116+
| `contentSelector` | string | null | Selector of the content to display in the tooltip. |
117+
| `contentClone` | boolean | false | Flag to clone the content to display in the tooltip. If false, the content is removed from its previous parent. |
118+
| `contentActions` | object | null | Configuration of the tooltip actions (see [Content Actions](#content-actions)). |
119+
| `containerClassName` | string | '__tooltip' | Class name to apply to the tooltip container. |
120+
| `position` | string | 'top' | Position of the tooltip. Available values: 'top', 'bottom', 'left', 'right' |
121+
| `animated` | boolean | false | Flag to animate tooltip transitions. |
122+
| `animationEnterClassName` | string | '__tooltip-enter' | Class name to apply to the tooltip enter transition. |
123+
| `animationLeaveClassName` | string | '__tooltip-leave' | Class name to apply to the tooltip leave transition. |
124+
| `enterDelay` | number | 0 | Delay before showing the tooltip in milliseconds. |
125+
| `leaveDelay` | number | 0 | Delay before hiding the tooltip in milliseconds. |
126+
| `offset` | number | 10 | Distance between the tooltip and the target in pixels. |
127+
| `disabled` | boolean | false | Flag to disable the tooltip content. |
128128

129129
### Content Actions
130130

src/Tooltip.js

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class Tooltip {
1313

1414
#boundEnterHandler = null
1515
#boundLeaveHandler = null
16+
#boundKeyDownHandler = null
1617

1718
#target = null
1819
#content = null
@@ -65,11 +66,11 @@ class Tooltip {
6566

6667
this.#observer = new DOMObserver()
6768

69+
this.#createTooltip()
70+
6871
this.#target.title = ''
6972
this.#target.setAttribute('style', 'position: relative')
70-
71-
this.#createTooltip()
72-
this.#tooltip.setAttribute('class', this.#containerClassName || `__tooltip __tooltip-${this.#position}`)
73+
this.#target.setAttribute('aria-describedby', 'tooltip')
7374

7475
disabled ? this.#disableTarget() : this.#enableTarget()
7576

@@ -141,21 +142,32 @@ class Tooltip {
141142
#enableTarget() {
142143
this.#boundEnterHandler = this.#onTargetEnter.bind(this)
143144
this.#boundLeaveHandler = this.#onTargetLeave.bind(this)
145+
this.#boundKeyDownHandler = this.#onTargetKeyDown.bind(this)
144146

145147
this.#target.addEventListener('mouseenter', this.#boundEnterHandler)
146148
this.#target.addEventListener('mouseleave', this.#boundLeaveHandler)
149+
this.#target.addEventListener('focusin', this.#boundEnterHandler)
150+
this.#target.addEventListener('focusout', this.#boundLeaveHandler)
151+
window.addEventListener('keydown', this.#boundKeyDownHandler)
147152
}
148153

149154
#disableTarget() {
150155
this.#target.removeEventListener('mouseenter', this.#boundEnterHandler)
151156
this.#target.removeEventListener('mouseleave', this.#boundLeaveHandler)
157+
this.#target.removeEventListener('focusin', this.#boundEnterHandler)
158+
this.#target.removeEventListener('focusout', this.#boundLeaveHandler)
159+
window.removeEventListener('keydown', this.#boundKeyDownHandler)
152160

153161
this.#boundEnterHandler = null
154162
this.#boundLeaveHandler = null
163+
this.#boundKeyDownHandler = null
155164
}
156165

157166
#createTooltip() {
158167
this.#tooltip = document.createElement('div')
168+
this.#tooltip.setAttribute('id', 'tooltip')
169+
this.#tooltip.setAttribute('class', this.#containerClassName || `__tooltip __tooltip-${this.#position}`)
170+
this.#tooltip.setAttribute('role', 'tooltip')
159171

160172
if (this.#contentSelector) {
161173
this.#observer
@@ -332,6 +344,12 @@ class Tooltip {
332344
await this.#waitForDelay(this.#leaveDelay)
333345
await this.#removeTooltipFromTarget()
334346
}
347+
348+
async #onTargetKeyDown(e) {
349+
if (e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27) {
350+
await this.#onTargetLeave()
351+
}
352+
}
335353
}
336354

337355
export default Tooltip

src/__tests__/useTooltip.test.js

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ describe('useTooltip', () => {
1717
new Promise(async (resolve) => {
1818
await fireEvent.mouseOver(target) // fireEvent.mouseEnter only works if mouseOver is triggered before
1919
await fireEvent.mouseEnter(target)
20-
await _sleep(10)
20+
await _sleep(1)
2121
resolve()
2222
})
2323

2424
const _leave = async () =>
2525
new Promise(async (resolve) => {
2626
await fireEvent.mouseLeave(target)
27-
await _sleep(10)
27+
await _sleep(1)
2828
resolve()
2929
})
3030

@@ -35,6 +35,34 @@ describe('useTooltip', () => {
3535
resolve()
3636
})
3737

38+
const _focus = async () =>
39+
new Promise(async (resolve) => {
40+
await fireEvent.focusIn(target)
41+
await _sleep(1)
42+
resolve()
43+
})
44+
45+
const _blur = async () =>
46+
new Promise(async (resolve) => {
47+
await fireEvent.focusOut(target)
48+
await _sleep(1)
49+
resolve()
50+
})
51+
52+
const _focusAndBlur = async () =>
53+
new Promise(async (resolve) => {
54+
await _focus()
55+
await _blur()
56+
resolve()
57+
})
58+
59+
const _keyDown = async (key) =>
60+
new Promise(async (resolve) => {
61+
await fireEvent.keyDown(target, key || { key: 'Escape', code: 'Escape', charCode: 27 })
62+
await _sleep(1)
63+
resolve()
64+
})
65+
3866
beforeEach(() => {
3967
target = _createElement('target', { class: 'bar' })
4068
template = _createElement('template')
@@ -78,6 +106,13 @@ describe('useTooltip', () => {
78106
await _enterAndLeave()
79107
expect(template).not.toBeInTheDocument()
80108
})
109+
110+
it('Hides tooltip on escape key down', async () => {
111+
action = useTooltip(target, options)
112+
await _enter()
113+
await _keyDown()
114+
expect(template).not.toBeInTheDocument()
115+
})
81116
})
82117

83118
describe('update', () => {
@@ -95,6 +130,27 @@ describe('useTooltip', () => {
95130
expect(newTemplate).toBeInTheDocument()
96131
})
97132
})
133+
134+
describe('focus', () => {
135+
it('Shows tooltip on focus in', async () => {
136+
action = useTooltip(target, options)
137+
await _focus()
138+
expect(template).toBeInTheDocument()
139+
})
140+
141+
it('Hides tooltip on focus out', async () => {
142+
action = useTooltip(target, options)
143+
await _focusAndBlur()
144+
expect(template).not.toBeInTheDocument()
145+
})
146+
147+
it('Hides tooltip on escape key down', async () => {
148+
action = useTooltip(target, options)
149+
await _focus()
150+
await _keyDown()
151+
expect(template).not.toBeInTheDocument()
152+
})
153+
})
98154
})
99155

100156
describe('useTooltip lifecycle', () => {
@@ -109,20 +165,44 @@ describe('useTooltip', () => {
109165
describe('useTooltip props: content', () => {
110166
it('Displays text content', async () => {
111167
const content = 'Foo'
112-
action = useTooltip(target, { ...options, contentSelector: null, content })
168+
action = useTooltip(target, {
169+
...options,
170+
contentSelector: null,
171+
content,
172+
})
113173
await _enter()
114174
expect(target).toHaveTextContent(content)
115175
})
116176

117177
it('Displays content element over text', async () => {
118178
const content = 'Foo'
119-
action = useTooltip(target, { ...options, content })
179+
action = useTooltip(target, {
180+
...options,
181+
content,
182+
})
120183
await _enter()
121184
expect(target).not.toHaveTextContent(content)
122185
expect(template).toBeInTheDocument()
123186
})
124187
})
125188

189+
describe('useTooltip props: contentClone', () => {
190+
it('Does not clone content element', async () => {
191+
action = useTooltip(target, options)
192+
await _sleep(1)
193+
expect(template).not.toBeVisible()
194+
})
195+
196+
it('Clones content element', async () => {
197+
action = useTooltip(target, {
198+
...options,
199+
contentClone: true,
200+
})
201+
await _sleep(1)
202+
expect(template).toBeVisible()
203+
})
204+
})
205+
126206
describe('useTooltip props: contentActions', () => {
127207
it('Triggers callback on tooltip click', async () => {
128208
action = useTooltip(target, options)

0 commit comments

Comments
 (0)