mirror of
https://github.com/pengzhanbo/vuepress-theme-plume.git
synced 2026-04-23 10:58:13 +08:00
feat: enhance a11y (#546)
This commit is contained in:
parent
52d04631d9
commit
b28112efc2
@ -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;
|
||||
|
||||
@ -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 }}
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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}'],
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -28,6 +28,8 @@ function onBlur() {
|
||||
class="vp-flyout"
|
||||
@mouseenter="open = true"
|
||||
@mouseleave="open = false"
|
||||
@focus="open = true"
|
||||
@blur="open = false"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@ -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">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user