Skip to content
This repository was archived by the owner on Feb 21, 2025. It is now read-only.

Commit c2c7da4

Browse files
committed
focus improvements
1 parent a47d3e1 commit c2c7da4

File tree

6 files changed

+71
-59
lines changed

6 files changed

+71
-59
lines changed

src/lib/components/Button.svelte

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,16 @@
11

22
<script lang="ts">
33
4-
// Svelte imports
5-
import { createEventDispatcher } from "svelte"
4+
// Internal dependencies
5+
import { preventFocus } from "$scripts/hocusfocus"
66
77
// Exports
88
export let href: string | undefined = undefined
99
export let submit: boolean = false
1010
export let disabled: boolean = false
1111
export let dangerous: boolean = false
1212
13-
function click() {
14-
button.blur()
15-
dispatch('click')
16-
}
17-
1813
// Variables
19-
const dispatch = createEventDispatcher()
2014
let button: HTMLButtonElement | HTMLAnchorElement
2115
2216
// Property validation
@@ -37,10 +31,11 @@
3731
class="button"
3832
class:disabled
3933
class:dangerous
40-
tabindex={disabled ? -1 : 0}
34+
tabindex="-1"
4135
type={submit ? 'submit' : 'button'}
42-
on:click={click}
36+
use:preventFocus
4337
bind:this={button}
38+
on:click
4439
>
4540
<slot />
4641
</button>
@@ -52,9 +47,10 @@
5247
class="button"
5348
class:disabled
5449
class:dangerous
55-
tabindex={disabled ? -1 : 0}
56-
on:click={click}
50+
tabindex="-1"
51+
use:preventFocus
5752
bind:this={button}
53+
on:click
5854
>
5955
<slot />
6056
</a>
@@ -71,18 +67,16 @@
7167
@use "$styles/palette.sass" as *
7268
7369
.button
70+
position: relative
7471
display: inline-flex
7572
flex-flow: row nowrap
7673
align-items: center
7774
7875
padding: $input-thin-padding $input-thick-padding
79-
80-
border: 1px solid transparent
8176
border-radius: $border-radius
8277
8378
color: $white
8479
background: $purple
85-
overflow: hidden
8680
white-space: nowrap
8781
8882
cursor: pointer
@@ -104,11 +98,9 @@
10498
margin-right: $input-thin-padding
10599
106100
filter: $white-filter
107-
transform-origin: center
108101
transition: all $default-transition
109-
110-
&:hover, &:focus
111102
103+
&:hover
112104
background: $dark-purple
113105
114106
&.dangerous

src/lib/components/Dropdown.svelte

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import { Severity } from '$scripts/validation'
99
import { clickoutside } from '$scripts/clickoutside'
1010
import { scrollintoview } from '$scripts/scrollintoview'
11-
import { focusthis, loopfocus, focusonhover, losefocus, focusfirst } from '$scripts/hocusfocus'
11+
import { focusOnLoad, loopFocus, focusOnHover, focusFirstChild, focusOnKeydown } from '$scripts/hocusfocus'
1212
1313
import type { DropdownOption } from '$scripts/types'
1414
@@ -37,7 +37,7 @@
3737
}
3838
3939
function set(new_value: T | null) {
40-
focusthis(header)
40+
focusOnLoad(header)
4141
visibility(false)
4242
4343
if (value !== new_value) {
@@ -75,7 +75,6 @@
7575
<div
7676
class="wrapper"
7777
class:visible
78-
use:losefocus={() => visibility(false)}
7978
use:clickoutside={() => visibility(false)}
8079
bind:this={wrapper}
8180
>
@@ -101,10 +100,10 @@
101100
</button>
102101

103102
{#if visible}
104-
<div class="options" use:focusfirst use:loopfocus use:scrollintoview>
103+
<div class="options" use:focusFirstChild use:loopFocus use:scrollintoview>
105104
{#if options.length >= 5}
106105
<div class="option searchbar">
107-
<input type="search" placeholder="Search..." bind:value={query}>
106+
<input type="search" placeholder="Search..." bind:value={query} use:focusOnKeydown>
108107
<img src={searchIcon} alt="Searchbar" />
109108
</div>
110109
{/if}
@@ -114,9 +113,10 @@
114113
<button
115114
type="button"
116115
class="option"
116+
tabindex={option.validation.okay() ? 0 : -1}
117117
disabled={!option.validation.okay()}
118118
on:click={() => set(option.value)}
119-
use:focusonhover
119+
use:focusOnHover
120120
>
121121
{#if option.label.trim() === ''}
122122
<i> Unnamed option </i>
@@ -144,7 +144,7 @@
144144
{/if}
145145

146146
{#if value !== null}
147-
<button type="button" class="option grayed" on:click={() => set(null)} use:focusonhover>
147+
<button type="button" class="option grayed" on:click={() => set(null)} use:focusOnHover>
148148
<i> Remove choice </i>
149149
</button>
150150
{/if}

src/lib/components/LinkButton.svelte

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
type={submit ? 'submit' : 'button'}
2525
class="link-button"
2626
class:disabled
27+
tabindex="-1"
2728
on:click
2829
>
2930
<slot />
@@ -34,7 +35,8 @@
3435
<a
3536
href={href}
3637
class="link-button"
37-
class:disabled
38+
class:disabled
39+
tabindex="-1"
3840
on:click
3941
>
4042
<slot />

src/lib/components/Modal.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<script lang="ts">
33
44
// Internal imports
5-
import { focusfirst } from '$scripts/hocusfocus'
5+
import { focusFirstChild } from '$scripts/hocusfocus'
66
77
// Assets
88
import plusIcon from '$assets/plus-icon.svg'
@@ -35,7 +35,7 @@
3535
<img src={plusIcon} alt="Exit icon" class="icon" />
3636
</button>
3737
</header>
38-
<section use:focusfirst>
38+
<section use:focusFirstChild>
3939
<slot />
4040
</section>
4141
<footer>

src/lib/scripts/hocusfocus.ts

Lines changed: 48 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,26 @@
11

2+
function getFocusableChildren(element: HTMLElement) {
3+
return Array.from(element.querySelectorAll<HTMLElement>('button:not([tabindex="-1"]), [href]:not([tabindex="-1"]), input:not([tabindex="-1"]), select:not([tabindex="-1"]), textarea:not([tabindex="-1"]), [tabindex]:not([tabindex="-1"])'))
4+
}
5+
26
// Exports
3-
export function focusfirst(element: HTMLElement) {
4-
const focusable = element.querySelectorAll<HTMLElement>('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')
5-
if (focusable.length) focusable[0].focus()
6-
return {}
7+
export function focusFirstChild(element: HTMLElement) {
8+
const focusable = getFocusableChildren(element)
9+
if (focusable.length) {
10+
focusable[0].focus()
11+
console.log('FocusFirstChild focused')
12+
}
713
}
814

9-
export function focusthis(element: HTMLElement) {
15+
export function focusOnLoad(element: HTMLElement) {
1016
element.focus()
11-
return {}
17+
console.log('FocusOnLoad focused')
1218
}
1319

14-
export function focusonhover(element: HTMLElement) {
20+
export function focusOnHover(element: HTMLElement) {
1521
function onHover() {
1622
element.focus()
23+
console.log('FocusOnHover focused')
1724
}
1825

1926
element.addEventListener('mouseenter', onHover)
@@ -25,30 +32,24 @@ export function focusonhover(element: HTMLElement) {
2532
}
2633
}
2734

28-
export function losefocus(element: HTMLElement, callback: () => void) {
29-
function checkFocus() {
30-
let had_focus = has_focus
31-
has_focus = element.contains(document.activeElement) ||
32-
element === document.activeElement ||
33-
document.activeElement === document.body
34-
if (had_focus && !has_focus) callback()
35+
export function focusOnKeydown(element: HTMLElement, key?: string) {
36+
function onKeyDown(event: KeyboardEvent) {
37+
if (event.key === 'Tab' || event.key === 'Enter' || key && event.key !== key) return
38+
element.focus()
39+
console.log('FocusOnKeydown focused')
3540
}
3641

37-
let has_focus = element.contains(document.activeElement)
38-
document.addEventListener('focusin', checkFocus)
39-
document.addEventListener('focusout', checkFocus)
42+
document.addEventListener('keydown', onKeyDown)
4043

4144
return {
4245
destroy() {
43-
document.removeEventListener('focusin', checkFocus)
44-
document.removeEventListener('focusout', checkFocus)
46+
document.removeEventListener('keydown', onKeyDown)
4547
}
4648
}
4749
}
4850

49-
export function loopfocus(element: HTMLElement) {
50-
const focusable = Array.from(element.querySelectorAll<HTMLElement>('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'))
51-
if (!focusable.length) return {}
51+
export function loopFocus(element: HTMLElement) {
52+
let focusable = getFocusableChildren(element)
5253
let index = focusable.findIndex(e => e === document.activeElement)
5354

5455
function onFocusIn(event: FocusEvent) {
@@ -61,15 +62,12 @@ export function loopfocus(element: HTMLElement) {
6162

6263
event.preventDefault()
6364

64-
// Go to the next focusable element that still exists in the dom
65-
let next = (index + (event.shiftKey ? -1 : 1) + focusable.length) % focusable.length
66-
while (!element.contains(focusable[next])) {
67-
next = (next + (event.shiftKey ? -1 : 1) + focusable.length) % focusable.length
68-
}
69-
70-
// Focus
71-
index = next
65+
// Go to the next focusable element
66+
index = (index + (event.shiftKey ? -1 : 1) + focusable.length) % focusable.length
67+
while (!element.contains(focusable[index]))
68+
index = (index + (event.shiftKey ? -1 : 1) + focusable.length) % focusable.length
7269
focusable[index].focus()
70+
console.log('LoopFocus focused')
7371
}
7472
}
7573

@@ -83,3 +81,23 @@ export function loopfocus(element: HTMLElement) {
8381
}
8482
}
8583
}
84+
85+
export function preventFocus(element: HTMLElement) {
86+
let last_focus = document.activeElement as HTMLElement | null
87+
88+
function onFocus(event: FocusEvent) {
89+
if (element === event.target || element.contains(event.target as HTMLElement)) {
90+
last_focus?.focus()
91+
} else {
92+
last_focus = document.activeElement as HTMLElement | null
93+
}
94+
}
95+
96+
document.addEventListener('focus', onFocus)
97+
98+
return {
99+
destroy() {
100+
document.removeEventListener('focus', onFocus)
101+
}
102+
}
103+
}

src/lib/styles/variables.sass

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
$border-radius: 0.3em
55
$shadow: 3px 3px 3px 0px rgba(0,0,0,0.2)
66

7-
$scale-on-hover: 1.1
7+
$scale-on-hover: 1.15
88
$rotate-on-hover: 90deg
99
$default-transition: ease-in 0.15s
1010

0 commit comments

Comments
 (0)