fix(a11y): label associations for AdminNewsCreate fields

- htmlFor/id pairs for title, slug, read-time, excerpt and featured-image inputs
- add an ariaLabel prop to ColoredItemCombobox (applied to the trigger button) and
  pass the category/tag labels so the comboboxes have an accessible name (WCAG 1.3.1/4.1.2)
This commit is contained in:
c0mrade
2026-05-26 00:20:30 +03:00
parent 1b40c3856d
commit fefc52fb20
2 changed files with 25 additions and 5 deletions

View File

@@ -27,6 +27,7 @@ interface ColoredItemComboboxProps {
onCreateNew: (name: string, color: string) => Promise<ColoredItem>; onCreateNew: (name: string, color: string) => Promise<ColoredItem>;
onDelete?: (item: ColoredItem) => Promise<void>; onDelete?: (item: ColoredItem) => Promise<void>;
placeholder?: string; placeholder?: string;
ariaLabel?: string;
isLoading?: boolean; isLoading?: boolean;
colors?: string[]; colors?: string[];
className?: string; className?: string;
@@ -39,6 +40,7 @@ export function ColoredItemCombobox({
onCreateNew, onCreateNew,
onDelete, onDelete,
placeholder, placeholder,
ariaLabel,
isLoading = false, isLoading = false,
colors = DEFAULT_COLORS, colors = DEFAULT_COLORS,
className, className,
@@ -190,6 +192,7 @@ export function ColoredItemCombobox({
isOpen ? 'border-accent-500/50' : 'border-dark-700 hover:border-dark-600', isOpen ? 'border-accent-500/50' : 'border-dark-700 hover:border-dark-600',
isLoading && 'animate-pulse', isLoading && 'animate-pulse',
)} )}
aria-label={ariaLabel}
aria-expanded={isOpen} aria-expanded={isOpen}
aria-haspopup="listbox" aria-haspopup="listbox"
> >

View File

@@ -652,8 +652,11 @@ export default function AdminNewsCreate() {
<div className="space-y-5"> <div className="space-y-5">
{/* Title */} {/* Title */}
<div> <div>
<label className="label">{t('news.admin.titleLabel')}</label> <label htmlFor="news-title" className="label">
{t('news.admin.titleLabel')}
</label>
<input <input
id="news-title"
type="text" type="text"
value={title} value={title}
onChange={(e) => setTitle(e.target.value)} onChange={(e) => setTitle(e.target.value)}
@@ -664,8 +667,11 @@ export default function AdminNewsCreate() {
{/* Slug */} {/* Slug */}
<div> <div>
<label className="label">{t('news.admin.slugLabel')}</label> <label htmlFor="news-slug" className="label">
{t('news.admin.slugLabel')}
</label>
<input <input
id="news-slug"
type="text" type="text"
value={slug} value={slug}
onChange={(e) => { onChange={(e) => {
@@ -688,6 +694,7 @@ export default function AdminNewsCreate() {
onCreateNew={handleCreateCategory} onCreateNew={handleCreateCategory}
onDelete={handleDeleteCategory} onDelete={handleDeleteCategory}
placeholder={t('news.admin.combobox.selectCategory')} placeholder={t('news.admin.combobox.selectCategory')}
ariaLabel={t('news.admin.categoryLabel')}
/> />
</div> </div>
<div> <div>
@@ -699,14 +706,18 @@ export default function AdminNewsCreate() {
onCreateNew={handleCreateTag} onCreateNew={handleCreateTag}
onDelete={handleDeleteTag} onDelete={handleDeleteTag}
placeholder={t('news.admin.combobox.selectTag')} placeholder={t('news.admin.combobox.selectTag')}
ariaLabel={t('news.admin.tagLabel')}
/> />
</div> </div>
</div> </div>
{/* Read time */} {/* Read time */}
<div> <div>
<label className="label">{t('news.admin.readTimeLabel')}</label> <label htmlFor="news-readtime" className="label">
{t('news.admin.readTimeLabel')}
</label>
<input <input
id="news-readtime"
type="number" type="number"
value={readTimeMinutes} value={readTimeMinutes}
onChange={(e) => setReadTimeMinutes(Number(e.target.value) || 1)} onChange={(e) => setReadTimeMinutes(Number(e.target.value) || 1)}
@@ -718,8 +729,11 @@ export default function AdminNewsCreate() {
{/* Excerpt */} {/* Excerpt */}
<div> <div>
<label className="label">{t('news.admin.excerptLabel')}</label> <label htmlFor="news-excerpt" className="label">
{t('news.admin.excerptLabel')}
</label>
<textarea <textarea
id="news-excerpt"
value={excerpt} value={excerpt}
onChange={(e) => setExcerpt(e.target.value)} onChange={(e) => setExcerpt(e.target.value)}
className="input min-h-[80px] resize-y" className="input min-h-[80px] resize-y"
@@ -729,9 +743,12 @@ export default function AdminNewsCreate() {
{/* Featured Image URL */} {/* Featured Image URL */}
<div> <div>
<label className="label">{t('news.admin.imageLabel')}</label> <label htmlFor="news-image" className="label">
{t('news.admin.imageLabel')}
</label>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input <input
id="news-image"
type="text" type="text"
value={featuredImageUrl} value={featuredImageUrl}
onChange={(e) => setFeaturedImageUrl(e.target.value)} onChange={(e) => setFeaturedImageUrl(e.target.value)}