mirror of
https://github.com/element-plus/element-plus.git
synced 2025-02-23 11:59:34 +08:00
improvement(theme-chalk): visual enhancement (#7563)
* improvement(theme-chalk): visual enhancement - Add visual enhancement for keyboard nagivation on form items. * Fix linter issue * Fix switch active text issue * Fix bordered radio demo issue
This commit is contained in:
parent
7a45b88783
commit
f3a8856c63
@ -1,23 +1,27 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-radio v-model="radio1" label="1" size="large">Option 1</el-radio>
|
||||
<el-radio v-model="radio1" label="2" size="large">Option 2</el-radio>
|
||||
<div class="mb-2 flex items-center text-sm">
|
||||
<el-radio-group v-model="radio1" class="ml-4">
|
||||
<el-radio label="1" size="large">Option 1</el-radio>
|
||||
<el-radio label="2" size="large">Option 2</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div>
|
||||
<el-radio v-model="radio2" label="1">Option 1</el-radio>
|
||||
<el-radio v-model="radio2" label="2">Option 2</el-radio>
|
||||
<div class="my-2 flex items-center text-sm">
|
||||
<el-radio-group v-model="radio2" class="ml-4">
|
||||
<el-radio label="1">Option 1</el-radio>
|
||||
<el-radio label="2">Option 2</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div>
|
||||
<el-radio v-model="radio3" label="1" size="small">Option 1</el-radio>
|
||||
<el-radio v-model="radio3" label="2" size="small">Option 2</el-radio>
|
||||
<div class="my-4 flex items-center text-sm">
|
||||
<el-radio-group v-model="radio3" class="ml-4">
|
||||
<el-radio label="1" size="small">Option 1</el-radio>
|
||||
<el-radio label="2" size="small">Option 2</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div>
|
||||
<el-radio v-model="radio3" label="1" size="small" disabled
|
||||
>Option 1</el-radio
|
||||
>
|
||||
<el-radio v-model="radio3" label="2" size="small" disabled
|
||||
>Option 2</el-radio
|
||||
>
|
||||
<div class="mb-2 flex items-center text-sm">
|
||||
<el-radio-group v-model="radio3" disabled class="ml-4">
|
||||
<el-radio label="1" size="small">Option 1</el-radio>
|
||||
<el-radio label="2" size="small">Option 2</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -1,11 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-radio v-model="radio1" label="1" size="large" border>Option A</el-radio>
|
||||
<el-radio v-model="radio1" label="2" size="large" border>Option B</el-radio>
|
||||
<el-radio-group v-model="radio1">
|
||||
<el-radio label="1" size="large" border>Option A</el-radio>
|
||||
<el-radio label="2" size="large" border>Option B</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div style="margin-top: 20px">
|
||||
<el-radio v-model="radio2" label="1" border>Option A</el-radio>
|
||||
<el-radio v-model="radio2" label="2" border>Option B</el-radio>
|
||||
<el-radio-group v-model="radio2">
|
||||
<el-radio label="1" border>Option A</el-radio>
|
||||
<el-radio label="2" border>Option B</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div style="margin-top: 20px">
|
||||
<el-radio-group v-model="radio3" size="small">
|
||||
|
@ -23,7 +23,6 @@
|
||||
:role="indeterminate ? 'checkbox' : undefined"
|
||||
:aria-checked="indeterminate ? 'mixed' : undefined"
|
||||
>
|
||||
<span :class="ns.e('inner')" />
|
||||
<input
|
||||
v-if="trueLabel || falseLabel"
|
||||
:id="inputId"
|
||||
@ -55,6 +54,7 @@
|
||||
@focus="focus = true"
|
||||
@blur="focus = false"
|
||||
/>
|
||||
<span :class="ns.e('inner')" />
|
||||
</span>
|
||||
<span v-if="hasOwnLabel" :class="ns.e('label')">
|
||||
<slot />
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { nextTick } from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, test } from 'vitest'
|
||||
import { EVENT_CODE } from '@element-plus/constants'
|
||||
import { ElFormItem } from '@element-plus/components/form'
|
||||
import Radio from '../src/radio.vue'
|
||||
import RadioGroup from '../src/radio-group.vue'
|
||||
@ -328,45 +327,6 @@ describe('Radio Button', () => {
|
||||
)
|
||||
expect(wrapper.findAll('.el-radio-button--large').length).toBe(3)
|
||||
})
|
||||
it('keyboard event', async () => {
|
||||
const wrapper = _mount(
|
||||
` <el-radio-group v-model="radio">
|
||||
<el-radio-button ref="radio1" :label="3">3</el-radio-button>
|
||||
<el-radio-button ref="radio2" :label="6">6</el-radio-button>
|
||||
<el-radio-button ref="radio3" :label="9">9</el-radio-button>
|
||||
</el-radio-group>`,
|
||||
() => ({
|
||||
radio: 6,
|
||||
})
|
||||
)
|
||||
const radio1 = wrapper.findComponent({ ref: 'radio1' })
|
||||
const radio2 = wrapper.findComponent({ ref: 'radio2' })
|
||||
const radio3 = wrapper.findComponent({ ref: 'radio3' })
|
||||
const vm = wrapper.vm as any
|
||||
expect(vm.radio).toEqual(6)
|
||||
radio2.trigger('keydown', {
|
||||
code: EVENT_CODE.left,
|
||||
})
|
||||
expect(vm.radio).toEqual(3)
|
||||
radio1.trigger('keydown', {
|
||||
code: EVENT_CODE.left,
|
||||
})
|
||||
expect(vm.radio).toEqual(9)
|
||||
await nextTick()
|
||||
radio3.trigger('keydown', {
|
||||
code: EVENT_CODE.right,
|
||||
})
|
||||
expect(vm.radio).toEqual(3)
|
||||
radio1.trigger('keydown', {
|
||||
code: EVENT_CODE.right,
|
||||
})
|
||||
expect(vm.radio).toEqual(6)
|
||||
await nextTick()
|
||||
radio1.trigger('keydown', {
|
||||
code: EVENT_CODE.enter,
|
||||
})
|
||||
expect(vm.radio).toEqual(6)
|
||||
})
|
||||
|
||||
describe('form item accessibility integration', () => {
|
||||
test('single radio group in form item', async () => {
|
||||
|
@ -7,11 +7,6 @@
|
||||
ns.is('focus', focus),
|
||||
ns.bm('button', size),
|
||||
]"
|
||||
role="radio"
|
||||
:aria-checked="modelValue === label"
|
||||
:aria-disabled="disabled"
|
||||
:tabindex="tabIndex"
|
||||
@keydown.space.stop.prevent="modelValue = disabled ? modelValue : label"
|
||||
>
|
||||
<input
|
||||
ref="radioRef"
|
||||
@ -19,9 +14,8 @@
|
||||
:class="ns.be('button', 'original-radio')"
|
||||
:value="label"
|
||||
type="radio"
|
||||
:name="name"
|
||||
:name="name || radioGroup?.name"
|
||||
:disabled="disabled"
|
||||
tabindex="-1"
|
||||
@focus="focus = true"
|
||||
@blur="focus = false"
|
||||
/>
|
||||
@ -79,6 +73,7 @@ export default defineComponent({
|
||||
focus,
|
||||
activeStyle,
|
||||
radioRef,
|
||||
radioGroup,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
@ -26,6 +26,10 @@ export const radioGroupProps = buildProps({
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
} as const)
|
||||
export type RadioGroupProps = ExtractPropTypes<typeof radioGroupProps>
|
||||
|
||||
|
@ -6,7 +6,6 @@
|
||||
role="radiogroup"
|
||||
:aria-label="!isLabeledByFormItem ? label || 'radio-group' : undefined"
|
||||
:aria-labelledby="isLabeledByFormItem ? formItem.labelId : undefined"
|
||||
@keydown="handleKeydown"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
@ -14,6 +13,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
nextTick,
|
||||
onMounted,
|
||||
@ -23,7 +23,7 @@ import {
|
||||
toRefs,
|
||||
watch,
|
||||
} from 'vue'
|
||||
import { EVENT_CODE, UPDATE_MODEL_EVENT } from '@element-plus/constants'
|
||||
import { UPDATE_MODEL_EVENT } from '@element-plus/constants'
|
||||
import { radioGroupKey } from '@element-plus/tokens'
|
||||
import {
|
||||
useFormItem,
|
||||
@ -34,6 +34,7 @@ import { debugWarn } from '@element-plus/utils'
|
||||
import { radioGroupEmits, radioGroupProps } from './radio-group'
|
||||
import type { RadioGroupProps } from '..'
|
||||
|
||||
let id = 1
|
||||
export default defineComponent({
|
||||
name: 'ElRadioGroup',
|
||||
props: radioGroupProps,
|
||||
@ -55,42 +56,6 @@ export default defineComponent({
|
||||
nextTick(() => ctx.emit('change', value))
|
||||
}
|
||||
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
if (!radioGroupRef.value) return
|
||||
|
||||
// 左右上下按键 可以在 radio 组内切换不同选项
|
||||
const target = e.target as HTMLInputElement
|
||||
const className =
|
||||
target.nodeName === 'INPUT' ? '[type=radio]' : '[role=radio]'
|
||||
const radios =
|
||||
radioGroupRef.value.querySelectorAll<HTMLInputElement>(className)
|
||||
const length = radios.length
|
||||
const index = Array.from(radios).indexOf(target)
|
||||
const roleRadios =
|
||||
radioGroupRef.value.querySelectorAll<HTMLInputElement>('[role=radio]')
|
||||
|
||||
let nextIndex: number | null = null
|
||||
switch (e.code) {
|
||||
case EVENT_CODE.left:
|
||||
case EVENT_CODE.up:
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
nextIndex = index === 0 ? length - 1 : index - 1
|
||||
break
|
||||
case EVENT_CODE.right:
|
||||
case EVENT_CODE.down:
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
nextIndex = index === length - 1 ? 0 : index + 1
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
if (nextIndex === null) return
|
||||
roleRadios[nextIndex].click()
|
||||
roleRadios[nextIndex].focus()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const radios =
|
||||
radioGroupRef.value!.querySelectorAll<HTMLInputElement>('[type=radio]')
|
||||
@ -100,11 +65,18 @@ export default defineComponent({
|
||||
}
|
||||
})
|
||||
|
||||
const randomName = `el-radio-group-${id++}`
|
||||
|
||||
const name = computed(() => {
|
||||
return props.name || randomName
|
||||
})
|
||||
|
||||
provide(
|
||||
radioGroupKey,
|
||||
reactive({
|
||||
...toRefs(props),
|
||||
changeEvent,
|
||||
name,
|
||||
})
|
||||
)
|
||||
|
||||
@ -119,7 +91,6 @@ export default defineComponent({
|
||||
formItem,
|
||||
groupId,
|
||||
isLabeledByFormItem,
|
||||
handleKeydown,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
@ -8,7 +8,6 @@
|
||||
ns.is('checked', modelValue === label),
|
||||
ns.m(size),
|
||||
]"
|
||||
@keydown.space.stop.prevent="modelValue = disabled ? modelValue : label"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
@ -17,20 +16,19 @@
|
||||
ns.is('checked', modelValue === label),
|
||||
]"
|
||||
>
|
||||
<span :class="ns.e('inner')" />
|
||||
<input
|
||||
ref="radioRef"
|
||||
v-model="modelValue"
|
||||
:class="ns.e('original')"
|
||||
:value="label"
|
||||
type="radio"
|
||||
:name="name"
|
||||
:name="name || radioGroup?.name"
|
||||
:disabled="disabled"
|
||||
tabindex="tabIndex"
|
||||
type="radio"
|
||||
@focus="focus = true"
|
||||
@blur="focus = false"
|
||||
@change="handleChange"
|
||||
/>
|
||||
<span :class="ns.e('inner')" />
|
||||
</span>
|
||||
<span :class="ns.e('label')" @keydown.stop>
|
||||
<slot>
|
||||
@ -52,8 +50,16 @@ export default defineComponent({
|
||||
|
||||
setup(props, { emit }) {
|
||||
const ns = useNamespace('radio')
|
||||
const { radioRef, isGroup, focus, size, disabled, tabIndex, modelValue } =
|
||||
useRadio(props, emit)
|
||||
const {
|
||||
radioRef,
|
||||
isGroup,
|
||||
radioGroup,
|
||||
focus,
|
||||
size,
|
||||
disabled,
|
||||
tabIndex,
|
||||
modelValue,
|
||||
} = useRadio(props, emit)
|
||||
|
||||
function handleChange() {
|
||||
nextTick(() => emit('change', modelValue.value))
|
||||
@ -63,6 +69,7 @@ export default defineComponent({
|
||||
ns,
|
||||
focus,
|
||||
isGroup,
|
||||
radioGroup,
|
||||
modelValue,
|
||||
tabIndex,
|
||||
size,
|
||||
|
@ -59,6 +59,12 @@ $button-icon-span-gap: map.merge(
|
||||
background-color: getCssVar('button', 'active', 'bg-color');
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
border-color: transparent;
|
||||
outline: 2px solid getCssVar('button', 'border-color');
|
||||
outline-offset: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
> span {
|
||||
@ -166,6 +172,12 @@ $button-icon-span-gap: map.merge(
|
||||
background-color: getCssVar('fill-color', 'light');
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
border-color: transparent;
|
||||
outline: 2px solid getCssVar('button', 'border-color');
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: getCssVar('fill-color');
|
||||
}
|
||||
|
@ -97,6 +97,14 @@ $checkbox-bordered-input-width: map.merge(
|
||||
}
|
||||
}
|
||||
|
||||
input:focus-visible {
|
||||
& + .#{$namespace}-checkbox__inner {
|
||||
outline: 2px solid getCssVar('checkbox-input-border-color-hover');
|
||||
outline-offset: 1px;
|
||||
border-radius: getCssVar('checkbox-border-radius');
|
||||
}
|
||||
}
|
||||
|
||||
@include e(input) {
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
@ -169,8 +177,10 @@ $checkbox-bordered-input-width: map.merge(
|
||||
}
|
||||
@include when(focus) {
|
||||
// Visually distinguish when focus
|
||||
.#{$namespace}-checkbox__inner {
|
||||
border-color: getCssVar('checkbox-input-border-color-hover');
|
||||
&:not(.is-checked) {
|
||||
.#{$namespace}-checkbox__original:not(:focus-visible) {
|
||||
border-color: getCssVar('checkbox-input-border-color-hover');
|
||||
}
|
||||
}
|
||||
}
|
||||
@include when(indeterminate) {
|
||||
@ -207,7 +217,8 @@ $checkbox-bordered-input-width: map.merge(
|
||||
background-color: getCssVar('checkbox-bg-color');
|
||||
z-index: getCssVar('index-normal');
|
||||
transition: border-color 0.25s cubic-bezier(0.71, -0.46, 0.29, 1.46),
|
||||
background-color 0.25s cubic-bezier(0.71, -0.46, 0.29, 1.46);
|
||||
background-color 0.25s cubic-bezier(0.71, -0.46, 0.29, 1.46),
|
||||
outline 0.25s cubic-bezier(0.71, -0.46, 0.29, 1.46);
|
||||
|
||||
&:hover {
|
||||
border-color: getCssVar('checkbox-input-border-color-hover');
|
||||
|
@ -101,6 +101,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
& + .#{$namespace}-radio-button__inner {
|
||||
border-left: getCssVar('border');
|
||||
border-left-color: getCssVarWithDefault(
|
||||
'radio-button-checked-border-color',
|
||||
map.get($radio-button, 'checked-border-color')
|
||||
);
|
||||
outline: 2px solid getCssVar('radio-button-checked-border-color');
|
||||
outline-offset: 1px;
|
||||
z-index: 2;
|
||||
border-radius: getCssVar('border-radius-base');
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
& + .#{$namespace}-radio-button__inner {
|
||||
color: getCssVar('disabled-text-color');
|
||||
@ -147,9 +162,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:focus:not(.is-focus):not(:active):not(.is-disabled) {
|
||||
/*获得焦点时 样式提醒*/
|
||||
box-shadow: 0 0 2px 2px getCssVar('radio-button-checked-border-color');
|
||||
}
|
||||
}
|
||||
|
@ -179,9 +179,17 @@ $radio-font-size: map.merge(
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: 0;
|
||||
|
||||
&:focus-visible {
|
||||
& + .#{$namespace}-radio__inner {
|
||||
outline: 2px solid getCssVar('radio-input-border-color-hover');
|
||||
outline-offset: 1px;
|
||||
border-radius: getCssVar('radio-input-border-radius');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:focus:not(.is-focus):not(:active):not(.is-disabled) {
|
||||
&:focus:not(:focus-visible):not(.is-focus):not(:active):not(.is-disabled) {
|
||||
/*获得焦点时 样式提醒*/
|
||||
.#{$namespace}-radio__inner {
|
||||
box-shadow: 0 0 2px 2px getCssVar('radio-input-border-color-hover');
|
||||
|
@ -76,6 +76,7 @@ $switch-button-size: map.merge(
|
||||
line-height: map.get($switch-core-height, 'default');
|
||||
height: map.get($switch-height, 'default');
|
||||
vertical-align: middle;
|
||||
|
||||
@include when(disabled) {
|
||||
& .#{$namespace}-switch__core,
|
||||
& .#{$namespace}-switch__label {
|
||||
@ -123,6 +124,13 @@ $switch-button-size: map.merge(
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
margin: 0;
|
||||
|
||||
&:focus-visible {
|
||||
& ~ .#{$namespace}-switch__core {
|
||||
outline: 2px solid getCssVar('switch-on-color');
|
||||
outline-offset: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include e(core) {
|
||||
|
Loading…
Reference in New Issue
Block a user