fix(a11y): add sr-only DialogTitle/Description to record/export/timetravel modals
Radix Dialog warns when DialogContent отсутствует accessible name/description. RecordDrawer (Sheet) уже имел Title, добавили sr-only Description. ConflictDiffModal/ExportModal/TimeTravelModal — кастомные visible headers, теперь имеют sr-only Title + Description для screen readers + чтобы заглушить dev warnings.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Dialog, DialogContent } from '@/ui'
|
import { Dialog, DialogContent, DialogTitle, DialogDescription } from '@/ui'
|
||||||
import type { DictionaryDetail, FlattenedRecord } from '@/api/client'
|
import type { DictionaryDetail, FlattenedRecord } from '@/api/client'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
@@ -175,6 +175,16 @@ export function ExportModal({
|
|||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
||||||
<DialogContent size="xl" className="!p-0 gap-0">
|
<DialogContent size="xl" className="!p-0 gap-0">
|
||||||
|
{/* Radix a11y: visually-hidden Title + Description. Visible heading
|
||||||
|
* рендерится ниже как кастомный layout. */}
|
||||||
|
<DialogTitle className="sr-only">
|
||||||
|
{t('export.title', { defaultValue: 'Экспорт' })} — {detail.name}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="sr-only">
|
||||||
|
{t('export.description', {
|
||||||
|
defaultValue: 'Выбор формата, объёма и колонок для выгрузки.',
|
||||||
|
})}
|
||||||
|
</DialogDescription>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="px-6 py-4 border-b border-line">
|
<div className="px-6 py-4 border-b border-line">
|
||||||
<div className="text-cap text-mute tracking-[0.18em] uppercase">
|
<div className="text-cap text-mute tracking-[0.18em] uppercase">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { ArrowsClockwiseIcon, WarningIcon } from '@phosphor-icons/react'
|
import { ArrowsClockwiseIcon, WarningIcon } from '@phosphor-icons/react'
|
||||||
import { Dialog, DialogContent } from '@/ui'
|
import { Dialog, DialogContent, DialogTitle, DialogDescription } from '@/ui'
|
||||||
import { useRecordRaw } from '@/api/queries'
|
import { useRecordRaw } from '@/api/queries'
|
||||||
import type { CreateRecordRequest } from '@/api/client'
|
import type { CreateRecordRequest } from '@/api/client'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
@@ -75,6 +75,17 @@ export function ConflictDiffModal({
|
|||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
||||||
<DialogContent size="lg" className="!p-0 gap-0 max-h-[85vh]">
|
<DialogContent size="lg" className="!p-0 gap-0 max-h-[85vh]">
|
||||||
|
{/* Radix a11y: visually-hidden Title + Description satisfy
|
||||||
|
* DialogContent requirements. Visible header — custom layout ниже. */}
|
||||||
|
<DialogTitle className="sr-only">
|
||||||
|
{t('conflict.title', { defaultValue: 'Запись изменена другим пользователем' })}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="sr-only">
|
||||||
|
{t('conflict.description', {
|
||||||
|
defaultValue:
|
||||||
|
'Сравнение локальных правок с актуальным состоянием на сервере.',
|
||||||
|
})}
|
||||||
|
</DialogDescription>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-start gap-3 px-6 py-4 border-b border-line">
|
<div className="flex items-start gap-3 px-6 py-4 border-b border-line">
|
||||||
<div className="size-8 rounded-md bg-warn-bg inline-flex items-center justify-center shrink-0">
|
<div className="size-8 rounded-md bg-warn-bg inline-flex items-center justify-center shrink-0">
|
||||||
|
|||||||
@@ -156,6 +156,12 @@ export function RecordDrawer({
|
|||||||
<DialogPrimitive.Title id={titleId} className="text-cap text-mute truncate">
|
<DialogPrimitive.Title id={titleId} className="text-cap text-mute truncate">
|
||||||
{caption}
|
{caption}
|
||||||
</DialogPrimitive.Title>
|
</DialogPrimitive.Title>
|
||||||
|
{/* sr-only Description чтобы Radix не ругался на missing
|
||||||
|
* `aria-describedby` (a11y warning). Visible description нам не
|
||||||
|
* нужен — caption + recordId уже описывают drawer. */}
|
||||||
|
<DialogPrimitive.Description className="sr-only">
|
||||||
|
{caption}{recordId ? ` (${recordId})` : ''}
|
||||||
|
</DialogPrimitive.Description>
|
||||||
{recordId && (
|
{recordId && (
|
||||||
<div className="text-mono text-ink-2 truncate mt-0.5">{recordId}</div>
|
<div className="text-mono text-ink-2 truncate mt-0.5">{recordId}</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { ClockCounterClockwiseIcon, CaretLeftIcon, CaretRightIcon, XIcon } from '@phosphor-icons/react'
|
import { ClockCounterClockwiseIcon, CaretLeftIcon, CaretRightIcon, XIcon } from '@phosphor-icons/react'
|
||||||
import { Dialog, DialogContent } from '@/ui'
|
import { Dialog, DialogContent, DialogTitle, DialogDescription } from '@/ui'
|
||||||
import type { DictionaryDetail } from '@/api/client'
|
import type { DictionaryDetail } from '@/api/client'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
@@ -127,6 +127,16 @@ export function TimeTravelModal({
|
|||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
||||||
<DialogContent size="full" className="!p-0 gap-0 max-h-[95vh]" hideClose>
|
<DialogContent size="full" className="!p-0 gap-0 max-h-[95vh]" hideClose>
|
||||||
|
{/* Radix a11y: sr-only Title + Description. Visible header — custom layout ниже. */}
|
||||||
|
<DialogTitle className="sr-only">
|
||||||
|
{t('timeTravel.title', { defaultValue: 'Состояние справочника на момент времени' })}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="sr-only">
|
||||||
|
{t('timeTravel.description', {
|
||||||
|
defaultValue:
|
||||||
|
'Сравнение текущего состояния справочника с выбранной точкой во времени.',
|
||||||
|
})}
|
||||||
|
</DialogDescription>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-start gap-3 px-6 py-4 border-b border-line">
|
<div className="flex items-start gap-3 px-6 py-4 border-b border-line">
|
||||||
<div className="size-8 rounded-md bg-surface-2 inline-flex items-center justify-center shrink-0">
|
<div className="size-8 rounded-md bg-surface-2 inline-flex items-center justify-center shrink-0">
|
||||||
|
|||||||
Reference in New Issue
Block a user