Skip to content

Commit

Permalink
feat(checkbox): support w3c WAI-ARIA (a11y) pattern (#1130)
Browse files Browse the repository at this point in the history
  • Loading branch information
mlmoravek authored Nov 25, 2024
1 parent 1561718 commit 6d189fe
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 57 deletions.
15 changes: 7 additions & 8 deletions packages/docs/components/Checkbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,13 @@

### Events

| Event name | Properties | Description |
| -------------------- | ----------------------------------------------------------------------- | ---------------------------------- |
| update:model-value | **value** `T \| T[]` - updated modelValue prop | modelValue prop two-way binding |
| input | **value** `T \| T[]` - input value<br/>**event** `Event` - native event | on input change event |
| update:indeterminate | **value** `boolean` - updated indeterminate prop | indeterminate prop two-way binding |
| focus | **event** `Event` - native event | on input focus event |
| blur | **event** `Event` - native event | on input blur event |
| invalid | **event** `Event` - native event | on input invalid event |
| Event name | Properties | Description |
| ------------------ | ----------------------------------------------------------------------- | ------------------------------- |
| update:model-value | **value** `T \| T[]` - updated modelValue prop | modelValue prop two-way binding |
| input | **value** `T \| T[]` - input value<br/>**event** `Event` - native event | on input change event |
| focus | **event** `Event` - native event | on input focus event |
| blur | **event** `Event` - native event | on input blur event |
| invalid | **event** `Event` - native event | on input invalid event |

### Slots

Expand Down
74 changes: 32 additions & 42 deletions packages/oruga/src/components/checkbox/Checkbox.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts" generic="T">
import { computed, useAttrs, useId, useTemplateRef } from "vue";
import { computed, useAttrs, useId, useSlots, useTemplateRef } from "vue";
import { getDefault } from "@/utils/config";
import { defineClasses, useInputHandler } from "@/composables";
Expand All @@ -23,18 +23,18 @@ defineOptions({
const props = withDefaults(defineProps<CheckboxProps<T>>(), {
override: undefined,
modelValue: undefined,
id: () => useId(),
variant: () => getDefault("checkbox.variant"),
size: () => getDefault("checkbox.size"),
label: undefined,
indeterminate: false,
nativeValue: undefined,
disabled: false,
required: false,
disabled: false,
name: undefined,
nativeValue: undefined,
trueValue: undefined,
falseValue: undefined,
autocomplete: () => getDefault("checkbox.autocomplete", "off"),
id: () => useId(),
useHtml5Validation: () => getDefault("useHtml5Validation", true),
customValidity: "",
});
Expand All @@ -51,11 +51,6 @@ const emits = defineEmits<{
* @param event {Event} native event
*/
input: [value: T | T[], event: Event];
/**
* indeterminate prop two-way binding
* @param value {boolean} updated indeterminate prop
*/
"update:indeterminate": [value: boolean];
/**
* on input focus event
* @param event {Event} native event
Expand Down Expand Up @@ -85,23 +80,20 @@ const { onBlur, onFocus, onInvalid, setFocus } = useInputHandler(
// inject parent field component if used inside one
const { parentField } = injectField();
const vmodel = defineModel<T | T[]>({ default: undefined });
// if not `label` is given and `id` is given set as `for` property on o-field wrapper
if (!props.label && props.id) parentField?.value?.setInputId(props.id);
// set field labelId or create a unique label id if a label is given
const labelId =
!!parentField.value || !!props.label || !!useSlots().label
? parentField.value?.labelId || useId()
: undefined;
const isIndeterminate = defineModel<boolean>("indeterminate", {
default: false,
});
// if no `label` is given and `id` is given set as `for` property on o-field wrapper
if (!props.label && props.id) parentField.value?.setInputId(props.id);
const _trueValue =
typeof props.trueValue === "undefined" ? true : props.trueValue;
const _falseValue =
typeof props.falseValue === "undefined" ? false : props.falseValue;
const vmodel = defineModel<T | T[]>({ default: undefined });
const isChecked = computed(
() =>
vmodel.value === _trueValue ||
vmodel.value === (props.trueValue ?? true) ||
(Array.isArray(vmodel.value) &&
vmodel.value.includes(props.nativeValue as T)),
);
Expand All @@ -115,7 +107,7 @@ function onInput(event: Event): void {
const attrs = useAttrs();
const inputBind = computed(() => ({
...parentField?.value?.inputAttrs,
...parentField.value?.inputAttrs,
...attrs,
}));
Expand Down Expand Up @@ -144,7 +136,7 @@ const inputClasses = defineClasses(
"indeterminateClass",
"o-chk__input--indeterminate",
null,
isIndeterminate,
computed(() => props.indeterminate),
],
);
Expand All @@ -157,14 +149,7 @@ defineExpose({ focus: setFocus, value: vmodel });
</script>

<template>
<label
ref="label"
:class="rootClasses"
data-oruga="checkbox"
role="checkbox"
:aria-checked="isChecked"
@click.stop="setFocus"
@keydown.prevent.enter="setFocus">
<div :class="rootClasses" data-oruga="checkbox">
<input
v-bind="inputBind"
:id="id"
Expand All @@ -173,25 +158,30 @@ defineExpose({ focus: setFocus, value: vmodel });
type="checkbox"
data-oruga-input="checkbox"
:class="inputClasses"
:disabled="disabled"
:required="required"
:name="name"
:autocomplete="autocomplete"
:value="nativeValue"
:indeterminate.prop="indeterminate"
:true-value="_trueValue"
:false-value="_falseValue"
@click.stop
:true-value="trueValue ?? true"
:false-value="falseValue ?? false"
:required="required"
:indeterminate="indeterminate"
:disabled="disabled"
:autocomplete="autocomplete"
:aria-checked="indeterminate ? 'mixed' : isChecked"
:aria-labelledby="labelId"
@blur="onBlur"
@focus="onFocus"
@invalid="onInvalid"
@input="onInput" />
@change="onInput" />

<span v-if="label || $slots.default" :class="labelClasses">
<label
v-if="label || $slots.default"
:id="labelId"
:for="id"
:class="labelClasses">
<!--
@slot Content slot, default is label prop
-->
<slot>{{ label }}</slot>
</span>
</label>
</label>
</div>
</template>
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`OCheckbox tests > render correctly 1`] = `
"<label class="o-chk" data-oruga="checkbox" role="checkbox" aria-checked="false"><input id="v-0" type="checkbox" data-oruga-input="checkbox" class="o-chk__input" autocomplete="off" true-value="true" false-value="false">
<!--v-if-->
</label>"
"<div class="o-chk" data-oruga="checkbox"><input id="v-0" type="checkbox" data-oruga-input="checkbox" class="o-chk__input" true-value="true" false-value="false" autocomplete="off" aria-checked="false" aria-labelledby="v-1"><label id="v-1" for="v-0" class="o-chk__label">
<!--
@slot Content slot, default is label prop
-->My Input
</label></div>"
`;
49 changes: 49 additions & 0 deletions packages/oruga/src/components/checkbox/tests/checkbox.axe.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { afterEach, describe, expect, test } from "vitest";
import { enableAutoUnmount, mount } from "@vue/test-utils";
import { axe } from "jest-axe";

import OCheckbox from "../Checkbox.vue";

describe("Checkbox axe tests", () => {
enableAutoUnmount(afterEach);

const a11yCases = [
{
title: "axe checkbox - base case",
props: { label: "Checkbox Label" },
},
{
title: "axe checkbox - indeterminate case",
props: { label: "Checkbox Label", indeterminate: true },
},
{
title: "axe checkbox - id case",
props: { label: "Checkbox Label", id: "my-id" },
},
{
title: "axe checkbox - variant case",
props: { label: "Checkbox Label", variant: "success" },
},
{
title: "axe checkbox - size case",
props: { label: "Checkbox Label", size: "large" },
},
{
title: "axe checkbox - required case",
props: { label: "Checkbox Label", required: true },
},
{
title: "axe checkbox - disabled case",
props: { label: "Checkbox Label", disabled: true },
},
];

test.each(a11yCases)("$title", async ({ props }) => {
const wrapper = mount(OCheckbox, {
props: { ...props },
attachTo: document.body,
});

expect(await axe(wrapper.element)).toHaveNoViolations();
});
});
6 changes: 2 additions & 4 deletions packages/oruga/src/components/checkbox/tests/checkbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,11 @@ describe("OCheckbox tests", () => {
enableAutoUnmount(afterEach);

test("render correctly", () => {
const wrapper = mount(OCheckbox);
const wrapper = mount(OCheckbox, { props: { label: "My Input" } });
expect(!!wrapper.vm).toBeTruthy();
expect(wrapper.exists()).toBeTruthy();
expect(wrapper.attributes("data-oruga")).toBe("checkbox");
expect(
wrapper.find("label input[type=checkbox]").exists(),
).toBeTruthy(); // has an input checkbox
expect(wrapper.find("input[type=checkbox]").exists()).toBeTruthy();
expect(wrapper.html()).toMatchSnapshot();
});

Expand Down

0 comments on commit 6d189fe

Please sign in to comment.