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:
JeremyWuuuuu 2022-05-16 21:01:41 +08:00 committed by GitHub
parent 7a45b88783
commit f3a8856c63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 117 additions and 123 deletions

View File

@ -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>

View File

@ -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">

View File

@ -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 />

View File

@ -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 () => {

View File

@ -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,
}
},
})

View File

@ -26,6 +26,10 @@ export const radioGroupProps = buildProps({
type: String,
default: '',
},
name: {
type: String,
default: undefined,
},
} as const)
export type RadioGroupProps = ExtractPropTypes<typeof radioGroupProps>

View File

@ -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,
}
},
})

View File

@ -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,

View File

@ -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');
}

View File

@ -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');

View File

@ -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');
}
}

View File

@ -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');

View File

@ -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) {