Improve :focus accessibility & Polish animations

This commit is contained in:
venashial
2022-06-10 19:13:28 -07:00
parent 3243cbf039
commit 62f1830197
4 changed files with 56 additions and 60 deletions

View File

@@ -1,9 +1,8 @@
<script lang="ts"> <script lang="ts">
import IconChevronDown from 'virtual:icons/lucide/chevron-down' import IconChevronDown from 'virtual:icons/lucide/chevron-down'
import IconCheck from 'virtual:icons/heroicons-outline/check' import IconCheck from 'virtual:icons/heroicons-outline/check'
import { debounce } from 'throttle-debounce'
import { clickOutside } from 'svelte-use-click-outside' import { clickOutside } from 'svelte-use-click-outside'
import { onMount } from 'svelte' import { fade } from 'svelte/transition'
interface Option { interface Option {
label: string label: string
@@ -54,43 +53,29 @@
...options.map((it) => getTextWidth(String(it.label || it.value), '16px Inter')) ...options.map((it) => getTextWidth(String(it.label || it.value), '16px Inter'))
) )
let shouldOpenUp = false
let element: HTMLElement let element: HTMLElement
const checkShouldOpenUp = debounce(100, false, () => { function selectOption(option: Option) {
if (element) { selected = option
const bounding = element.getBoundingClientRect() open = false
element.focus()
shouldOpenUp = }
bounding.bottom + 32 * options.length + 16 >
(window.innerHeight || document.documentElement.clientHeight)
}
})
onMount(() => {
checkShouldOpenUp()
window.addEventListener('resize', checkShouldOpenUp)
})
function keydown(event: KeyboardEvent) { function keydown(event: KeyboardEvent) {
if ((event.key === ' ' || event.key === 'Enter') && !open) { if (event.key === ' ' || event.key === 'Enter') {
open = true event.preventDefault()
} else if (event.key === 'ArrowUp') {
if (selected) { if (!open) {
const index = options.findIndex((option) => option.value === selected.value) open = true
if (index > 0) { // Needs delay before trying to move focus
selected = options[index - 1] setTimeout(() => element.children[1].children[0].focus(), 0)
} } else {
const option = options.find(
({ label }) => label === document.activeElement.innerHTML.trim()
)
selectOption(option)
open = false
} }
} else if (event.key === 'ArrowDown') {
if (selected) {
const index = options.findIndex((option) => option.value === selected.value)
if (index < options.length - 1) {
selected = options[index + 1]
}
}
} else if ((event.key === 'Escape' || event.key === 'Enter') && open) {
open = false
} }
} }
</script> </script>
@@ -108,12 +93,12 @@
<div <div
class="select__input" class="select__input"
on:click={() => { on:click={() => {
open = !open open = true
}}> }}>
{#if icon} {#if icon}
<svelte:component this={icon} /> <svelte:component this={icon} />
{/if} {/if}
<span class="select__input__value" style:min-width="{minWidth + 16 + 8}px"> <span class="select__input__value" style:min-width="{minWidth}px">
{label || selected?.label || value || 'Choose...'} {label || selected?.label || value || 'Choose...'}
</span> </span>
<div class="select__input__arrow"> <div class="select__input__arrow">
@@ -123,21 +108,23 @@
</div> </div>
</div> </div>
{#if open} {#if open}
<ul class="select__options" style:--selected-index={options.indexOf(selected)}> <div
transition:fade={{ duration: 70 }}
class="select__options"
style:--selected-index={options.indexOf(selected)}>
{#each options as option (option.value)} {#each options as option (option.value)}
<li {@const isSelected = selected?.value === option.value}
on:click={() => { <button
selected = option on:click={() => selectOption(option)}
open = false class:is-selected={isSelected}
}} tabindex={isSelected ? -1 : 0}>
class:is-selected={selected?.value === option.value}>
{option.label || option.value} {option.label || option.value}
{#if selected?.value === option.value} {#if selected?.value === option.value}
<IconCheck /> <IconCheck />
{/if} {/if}
</li> </button>
{/each} {/each}
</ul> </div>
{/if} {/if}
</div> </div>
@@ -174,29 +161,29 @@
top: calc(100% * -1 * var(--selected-index)); top: calc(100% * -1 * var(--selected-index));
background-color: var(--color-button-bg); background-color: var(--color-button-bg);
border-radius: var(--rounded); border-radius: var(--rounded);
box-shadow: var(--shadow-inset-sm), var(--shadow-floating); box-shadow: var(--shadow-inset-sm), var(--shadow-floating), 0 0 0 1px var(--color-tertiary);
border: var(--border-width) solid var(--color-tertiary); /* border: var(--border-width) solid var(--color-tertiary); */
overflow: hidden; overflow: hidden;
z-index: 5; z-index: 5;
li { button {
padding: 0.25rem 1rem; padding: 0.25rem 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
&:hover { &:hover,
&:focus {
background-color: var(--color-brand-dark); background-color: var(--color-brand-dark);
color: var(--color-brand-dark-contrast); color: var(--color-brand-dark-contrast);
outline: none;
border-radius: 0;
} }
&.is-selected { &.is-selected {
background-color: var(--color-brand-light); background-color: var(--color-brand-light);
color: var(--color-text); color: var(--color-text);
cursor: default; cursor: default;
display: flex;
align-items: center;
gap: 0.5rem;
:global(.icon) {
}
} }
} }
} }

View File

@@ -39,7 +39,6 @@
box-shadow: var(--shadow-inset-sm); box-shadow: var(--shadow-inset-sm);
background-color: var(--color-button-bg); background-color: var(--color-button-bg);
border: none; border: none;
padding: 0.25rem 1rem;
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
@@ -48,8 +47,13 @@
} }
} }
input {
padding: 0.25rem 1rem;
}
textarea { textarea {
min-height: 2.5rem; min-height: 2.5rem;
padding: 0.5rem 1rem;
} }
:global(.icon) { :global(.icon) {

View File

@@ -26,18 +26,23 @@ button:focus-visible,
a:focus-visible, a:focus-visible,
input:focus-visible, input:focus-visible,
[tabindex='0']:focus-visible { [tabindex='0']:focus-visible {
outline: 0.25rem solid hsla(290, 100%, 40%, 0.5); outline: 0.25rem solid hsla(290, 100%, 75%, 1);
border-radius: 0.25rem;
}
:where(a) {
border-radius: 0.25rem; border-radius: 0.25rem;
} }
input, input,
button, button,
a { a {
outline: 0.25rem solid hsla(155, 58%, 44%, 0); outline: 0 solid hsla(290, 100%, 40%, 0);
transition: outline 0.2s ease-in-out; transition: outline 0.2s ease-in-out;
} }
input[type='text']:focus-visible { input[type='text']:focus-visible,
input[type='textarea']:focus-visible {
outline: 0.25rem solid hsla(155, 58%, 44%, 0.7); outline: 0.25rem solid hsla(155, 58%, 44%, 0.7);
} }

View File

@@ -6,7 +6,7 @@ title: Writing CSS
### Avoid inconsistent CSS units ### Avoid inconsistent CSS units
Prefer using `rem` units, using only whole and half units, eg. `2rem` or `1.5rem`. If you need a specific pixel (`px`) measurement, use `px` and add comment explaining why you used it. The one exception is that `0.25` is allowed. Prefer using `rem` units, using only whole and half units, eg. `2rem` or `1.5rem`. If you need a specific pixel (`px`) measurement, use `px` and add comment explaining why you used it. The one exception is that `0.25rem` is allowed.
> Using `rem` units lets you change the scale of the UI by simply changing the body font size. > Using `rem` units lets you change the scale of the UI by simply changing the body font size.