fix(a11y): Q.DEFER.004 sub-A — DealsTable show-select bulk-checkbox aria-label
VDataTable show-select prop генерировал unlabeled checkbox per row + select-all
header — axe-core критичная label violation (6 nodes на demo seed).
Override через Vuetify 3.12 typed slots:
- header.data-table-select → aria-label='Выбрать все сделки'
- item.data-table-select → aria-label='Выбрать сделку «{{name}}»' (per row)
Test coverage: tests/Frontend/DealsTable.spec.ts (2 specs).
This commit is contained in:
@@ -122,6 +122,23 @@ function formatCost(cost: number): string {
|
||||
<template #[`item.receivedMinutesAgo`]="{ item }: { item: MockDeal }">
|
||||
<span class="num ld-mono-s text-medium-emphasis">{{ formatRelative(item.receivedMinutesAgo) }}</span>
|
||||
</template>
|
||||
|
||||
<template #[`header.data-table-select`]="{ allSelected, selectAll, someSelected }">
|
||||
<v-checkbox-btn
|
||||
:model-value="allSelected"
|
||||
:indeterminate="someSelected && !allSelected"
|
||||
aria-label="Выбрать все сделки"
|
||||
@update:model-value="selectAll"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #[`item.data-table-select`]="{ isSelected, toggleSelect, internalItem, item }">
|
||||
<v-checkbox-btn
|
||||
:model-value="isSelected(internalItem)"
|
||||
:aria-label="`Выбрать сделку «${(item as MockDeal).name}»`"
|
||||
@update:model-value="(v: boolean | null) => toggleSelect(internalItem)"
|
||||
/>
|
||||
</template>
|
||||
</v-data-table>
|
||||
|
||||
<div v-if="deals.length === 0" class="empty-state pa-8 text-center text-medium-emphasis">
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { createVuetify } from 'vuetify';
|
||||
|
||||
import DealsTable from '../../resources/js/components/deals/DealsTable.vue';
|
||||
import type { MockDeal } from '../../resources/js/composables/mockDeals';
|
||||
|
||||
const vuetify = createVuetify();
|
||||
|
||||
const sampleDeals: MockDeal[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Иванов И.',
|
||||
phone: '+7 (916) 100-00-01',
|
||||
statusSlug: 'new',
|
||||
project: 'B1 site',
|
||||
manager: { initials: 'AD', name: 'Admin' },
|
||||
cost: 1000,
|
||||
receivedMinutesAgo: 5,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Петров П.',
|
||||
phone: '+7 (916) 100-00-02',
|
||||
statusSlug: 'new',
|
||||
project: 'B1 call',
|
||||
manager: { initials: 'AD', name: 'Admin' },
|
||||
cost: 1500,
|
||||
receivedMinutesAgo: 30,
|
||||
},
|
||||
];
|
||||
|
||||
describe('DealsTable a11y (Q.DEFER.004 sub-A)', () => {
|
||||
it('select-all header checkbox has aria-label', () => {
|
||||
const wrapper = mount(DealsTable, {
|
||||
props: { deals: sampleDeals, selectedIds: [], statusBySlug: new Map() },
|
||||
global: { plugins: [vuetify] },
|
||||
});
|
||||
const headerCheckbox = wrapper.find(
|
||||
'th .v-selection-control input[type="checkbox"][aria-label="Выбрать все сделки"]',
|
||||
);
|
||||
expect(headerCheckbox.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('each row checkbox has aria-label referencing deal name', () => {
|
||||
const wrapper = mount(DealsTable, {
|
||||
props: { deals: sampleDeals, selectedIds: [], statusBySlug: new Map() },
|
||||
global: { plugins: [vuetify] },
|
||||
});
|
||||
const rowCheckbox1 = wrapper.find(
|
||||
'tbody tr:nth-of-type(1) input[type="checkbox"][aria-label="Выбрать сделку «Иванов И.»"]',
|
||||
);
|
||||
const rowCheckbox2 = wrapper.find(
|
||||
'tbody tr:nth-of-type(2) input[type="checkbox"][aria-label="Выбрать сделку «Петров П.»"]',
|
||||
);
|
||||
expect(rowCheckbox1.exists()).toBe(true);
|
||||
expect(rowCheckbox2.exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user