You've already forked AstralRinth
forked from didirus/AstralRinth
Improve :focus accessibility & Polish animations
This commit is contained in:
@@ -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) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user