feat: enhance a11y (#546)

This commit is contained in:
pengzhanbo 2025-03-30 03:08:01 +08:00 committed by GitHub
parent 52d04631d9
commit b28112efc2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 95 additions and 43 deletions

View File

@ -1,10 +1,11 @@
<script setup lang="ts">
import { shallowRef } from 'vue'
import { shallowRef, useId } from 'vue'
import { useCaniuse, useCaniuseFeaturesSearch, useCaniuseVersionSelect } from '../composables/caniuse.js'
import CodeViewer from './CodeViewer.vue'
const listEl = shallowRef<HTMLUListElement | null>(null)
const inputEl = shallowRef<HTMLInputElement | null>(null)
const id = useId()
const { feature, featureList, onSelect, isFocus } = useCaniuseFeaturesSearch(inputEl, listEl)
const { past, pastList, future, futureList, embedType, embedTypeList } = useCaniuseVersionSelect()
@ -14,10 +15,11 @@ const { output, rendered } = useCaniuse({ feature, embedType, past, future })
<template>
<div class="caniuse-config-wrapper">
<form>
<div class="caniuse-form-item">
<label for="feature">选择特性</label>
<label class="caniuse-form-item" :for="`caniuse-feature-input-${id}`">
<span>选择特性</span>
<div class="feature-input">
<input
:id="`caniuse-feature-input-${id}`"
ref="inputEl"
class="feature-input__input"
type="text"
@ -29,18 +31,28 @@ const { output, rendered } = useCaniuse({ feature, embedType, past, future })
<li
v-for="item in featureList"
:key="item.value"
@click="onSelect(item)"
>
{{ item.label }}
<button
type="button"
class="feature-list-item"
@click="onSelect(item)"
@keydown.enter="onSelect(item)"
>
{{ item.label }}
</button>
</li>
</ul>
</div>
</div>
</label>
<div class="caniuse-form-item">
<label for="embedType">嵌入方式</label>
<span>嵌入方式</span>
<div class="caniuse-embed-type">
<label v-for="item in embedTypeList" :key="item.label">
<input v-model="embedType" type="radio" name="embedType" :value="item.value">
<label
v-for="(item, index) in embedTypeList"
:key="item.label"
:for="`caniuse-embed-${id}-${index}`"
>
<input :id="`caniuse-embed-${id}-${index}`" v-model="embedType" type="radio" name="embedType" :value="item.value">
<span>{{ item.label }}</span>
<Badge v-if="item.value === 'image'" type="warning" text="不推荐" />
</label>
@ -49,17 +61,21 @@ const { output, rendered } = useCaniuse({ feature, embedType, past, future })
<div v-if="!embedType" class="caniuse-form-item">
<span>浏览器版本</span>
<div class="caniuse-browser-version">
<select v-model="past" name="past">
<option v-for="item in pastList" :key="item.value" :value="item.value">
{{ item.label }}
</option>
</select>
<label :for="`caniuse-past-${id}`">
<select :id="`caniuse-past-${id}`" v-model="past" name="past">
<option v-for="item in pastList" :key="item.value" :value="item.value">
{{ item.label }}
</option>
</select>
</label>
<span>-</span>
<select v-model="future" name="future">
<option v-for="item in futureList" :key="item.value" :value="item.value">
{{ item.label }}
</option>
</select>
<label :for="`caniuse-future-${id}`">
<select :id="`caniuse-future-${id}`" v-model="future" name="future">
<option v-for="item in futureList" :key="item.value" :value="item.value">
{{ item.label }}
</option>
</select>
</label>
</div>
</div>
</form>
@ -174,7 +190,7 @@ const { output, rendered } = useCaniuse({ feature, embedType, past, future })
display: none;
}
.caniuse-browser-version select {
.caniuse-browser-version label {
flex: 1 2;
width: 100%;
padding: 3px 16px;

View File

@ -27,9 +27,9 @@ defineProps<{
<span class="title">
<a :href="demo.url" target="_blank" rel="noopener noreferrer" :aria-label="demo.name" :title="demo.name">{{ demo.name }}</a>
</span>
<a v-if="demo.repo" :href="demo.repo" class="github" target="_blank" rel="noopener noreferrer"><span
class="vpi-social-github"
/></a>
<a v-if="demo.repo" :href="demo.repo" class="github" target="_blank" rel="noopener noreferrer" :aria-label="`Link to GitHub: ${demo.name}`">
<span class="vpi-social-github" />
</a>
</h3>
<p :title="demo.desc">
{{ demo.desc }}

View File

@ -78,17 +78,20 @@ const output = computed(() => {
</DemoWrapper>
</div>
<p>
<label>
<input v-model="isDark" type="checkbox"> 深色模式
<label for="tint-plate-is-dark">
<input id="tint-plate-is-dark" v-model="isDark" type="checkbox"> 深色模式
</label>
</p>
<div class="mode-content">
<div
v-for="item in modeList" :key="item.value" class="mode" :class="{ active: mode === item.value }"
<button
v-for="item in modeList"
:key="item.value"
class="mode" :class="{ active: mode === item.value }"
type="button"
@click="mode = item.value"
>
{{ item.label }}
</div>
</button>
</div>
<div class="tint-plate-">
<SingleTintPlate v-if="mode === 'single'" v-model="singleTintPlate" />

View File

@ -1,4 +1,6 @@
<script setup lang="ts">
import { useId } from 'vue'
interface Props {
min?: number
max: number
@ -9,25 +11,36 @@ const props = withDefaults(defineProps<Props>(), {
min: 0,
step: 1,
})
const value = defineModel<number>({
required: true,
set(v) {
return Math.min(Math.max(v, props.min), props.max)
},
})
const id = useId()
</script>
<template>
<input v-model="value" type="range" :min="min" :max="max" :step="step">
<input v-model="value" type="number" :min="min" :max="max" :step="step">
<label :for="`range-${id}`" class="input-range">
<input :id="`range-${id}`" v-model="value" type="range" :min="min" :max="max" :step="step">
</label>
<label :for="`range-number-${id}`" class="input-range-number">
<input :id="`range-number-${id}`" v-model="value" type="number" :min="min" :max="max" :step="step">
</label>
</template>
<style scoped>
input[type="range"] {
.input-range {
flex: 1 2;
}
input[type="number"] {
.input-range input {
width: 100%;
}
.input-range-number {
width: 50px;
margin-left: 10px;
text-align: center;

View File

@ -2,6 +2,9 @@ import config from '@pengzhanbo/eslint-config-vue'
export default config({
pnpm: true,
vue: {
a11y: true,
},
ignores: [
'lib',
'docs/notes/theme/snippet/code-block.snippet.md',
@ -20,6 +23,8 @@ export default config({
files: ['**/*.vue'],
rules: {
'vue/no-v-text-v-html-on-component': 'off',
'vue-a11y/click-events-have-key-events': 'off',
'vue-a11y/no-static-element-interactions': 'off',
},
}, {
files: ['**/*.md/*.{js,ts}'],

View File

@ -32,7 +32,13 @@ watch(show, () => nextTick(() => {
</script>
<template>
<span class="vp-abbr" @mouseenter="show = true" @mouseleave="show = false">
<span
class="vp-abbr"
@mouseenter="show = true"
@mouseleave="show = false"
@focus="show = true"
@blur="show = false"
>
<slot />
<ClientOnly>
<Transition name="fade">

View File

@ -81,6 +81,7 @@ onUnmounted(() => {
<template>
<div ref="editorEl" class="code-repl-editor">
<slot />
<!-- eslint-disable-next-line vue-a11y/form-control-has-label -->
<textarea ref="textAreaEl" v-model="input" class="code-repl-input" />
</div>
</template>

View File

@ -37,6 +37,7 @@ const data = useFence(
<iframe
:id="`VPDemoNormalDraw${id}`"
ref="draw"
:title="title || 'Demo'"
class="draw-iframe"
allow="accelerometer *; bluetooth *; camera *; encrypted-media *; display-capture *; geolocation *; gyroscope *; microphone *; midi *; clipboard-read *; clipboard-write *; web-share *; serial *; xr-spatial-tracking *"
allowfullscreen="true"

View File

@ -311,6 +311,7 @@ function selectedClick(e: MouseEvent, p: SearchResult & Result) {
@pointerup="onSearchBarClick($event)"
@submit.prevent=""
>
<!-- eslint-disable vue-a11y/label-has-for -->
<label
id="localsearch-label"
:title="buttonText"

View File

@ -36,16 +36,19 @@ async function onSubmit() {
<div class="vp-encrypt-form">
<p class="encrypt-text" v-html="info ?? 'Only Password can access this site'" />
<p class="encrypt-input-wrapper">
<span class="vpi-lock icon-lock" />
<input
v-model="password"
class="encrypt-input"
:class="{ error: errorCode === 1 }"
type="password"
:placeholder="theme.encryptPlaceholder ?? 'Enter Password'"
@keyup.enter="onSubmit"
@input="password && (errorCode = 0)"
>
<label for="encrypt-input">
<span class="vpi-lock icon-lock" />
<input
id="encrypt-input"
v-model="password"
class="encrypt-input"
:class="{ error: errorCode === 1 }"
type="password"
:placeholder="theme.encryptPlaceholder ?? 'Enter Password'"
@keyup.enter="onSubmit"
@input="password && (errorCode = 0)"
>
</label>
</p>
<button class="encrypt-button" :class="{ unlocking }" @click="onSubmit">
<span v-if="!unlocking">{{ theme.encryptButtonText ?? 'Confirm' }}</span>

View File

@ -28,6 +28,8 @@ function onBlur() {
class="vp-flyout"
@mouseenter="open = true"
@mouseleave="open = false"
@focus="open = true"
@blur="open = false"
>
<button
type="button"

View File

@ -1,4 +1,5 @@
<template>
<!-- eslint-disable-next-line vue-a11y/role-has-required-aria-props -->
<button class="vp-switch" type="button" role="switch">
<span class="check">
<span v-if="$slots.default" class="icon">