From 6ebf0c68864255d74477c0c9683f69f77ac8c46b Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 20 May 2026 14:57:52 +0100 Subject: [PATCH] Label alert schedule toggles by section --- .../src/components/shared/SettingsPanel.tsx | 11 +++++--- .../src/components/shared/Toggle.test.tsx | 18 ++++++++++++ .../src/components/shared/Toggle.tsx | 11 ++++++-- .../features/alerts/AlertCooldownSection.tsx | 3 ++ .../alerts/AlertEscalationSection.tsx | 3 ++ .../features/alerts/AlertGroupingSection.tsx | 3 ++ .../alerts/AlertQuietHoursSection.tsx | 6 +++- .../features/alerts/AlertRecoverySection.tsx | 6 ++++ .../alerts/AlertScheduleSections.test.tsx | 28 +++++++++++++++++++ 9 files changed, 82 insertions(+), 7 deletions(-) diff --git a/frontend-modern/src/components/shared/SettingsPanel.tsx b/frontend-modern/src/components/shared/SettingsPanel.tsx index d33ef570a0..ed20b2f9af 100644 --- a/frontend-modern/src/components/shared/SettingsPanel.tsx +++ b/frontend-modern/src/components/shared/SettingsPanel.tsx @@ -4,6 +4,7 @@ import { Card } from '@/components/shared/Card'; type SettingsPanelProps = { title: JSX.Element; description?: JSX.Element; + titleId?: string; action?: JSX.Element; bodyClass?: string; tone?: 'default' | 'muted' | 'info' | 'success' | 'warning' | 'danger'; @@ -15,6 +16,7 @@ export function SettingsPanel(props: SettingsPanelProps) { const [local, rest] = splitProps(props, [ 'title', 'description', + 'titleId', 'action', 'bodyClass', 'children', @@ -35,13 +37,14 @@ export function SettingsPanel(props: SettingsPanelProps) {
-

+

{local.title}

-

- {local.description} -

+

{local.description}

diff --git a/frontend-modern/src/components/shared/Toggle.test.tsx b/frontend-modern/src/components/shared/Toggle.test.tsx index 59d44609ed..fe8f19d828 100644 --- a/frontend-modern/src/components/shared/Toggle.test.tsx +++ b/frontend-modern/src/components/shared/Toggle.test.tsx @@ -81,4 +81,22 @@ describe('Toggle', () => { expect(button).toHaveAttribute('aria-describedby', 'external-toggle-description'); expect(button).toHaveAccessibleDescription('Run when alerts fire.'); }); + + it('combines external context with its visible status label', () => { + render(() => ( + <> + Quiet hours + Disabled} + /> + + )); + + const button = screen.getByRole('button', { name: 'Quiet hours Disabled' }); + const labelledBy = button.getAttribute('aria-labelledby')?.split(/\s+/) ?? []; + expect(labelledBy).toContain('schedule-section-label'); + expect(labelledBy).toHaveLength(2); + }); }); diff --git a/frontend-modern/src/components/shared/Toggle.tsx b/frontend-modern/src/components/shared/Toggle.tsx index 9eecef11c1..3849f5a2ce 100644 --- a/frontend-modern/src/components/shared/Toggle.tsx +++ b/frontend-modern/src/components/shared/Toggle.tsx @@ -53,14 +53,21 @@ export function Toggle(props: LabeledToggleProps) { const size = resolveToggleSize(props.size, 'md'); const labelId = createUniqueId(); const descriptionId = createUniqueId(); + const labelledBy = () => + [props.ariaLabelledBy, props.label ? labelId : undefined].filter(Boolean).join(' ') || + undefined; + const describedBy = () => + [props.ariaDescribedBy, props.description ? descriptionId : undefined] + .filter(Boolean) + .join(' ') || undefined; return (
{(props.label || props.description) && ( diff --git a/frontend-modern/src/features/alerts/AlertCooldownSection.tsx b/frontend-modern/src/features/alerts/AlertCooldownSection.tsx index 676176a838..12a5155c91 100644 --- a/frontend-modern/src/features/alerts/AlertCooldownSection.tsx +++ b/frontend-modern/src/features/alerts/AlertCooldownSection.tsx @@ -27,12 +27,14 @@ interface AlertCooldownSectionProps { export function AlertCooldownSection(props: AlertCooldownSectionProps) { const fieldIdPrefix = `alert-cooldown-${createUniqueId()}`; const fieldIds = { + title: `${fieldIdPrefix}-title`, minutes: `${fieldIdPrefix}-minutes`, maxAlerts: `${fieldIdPrefix}-max-alerts`, }; return ( props.setCooldownEnabled(event.currentTarget.checked)} containerClass="sm:self-start" + ariaLabelledBy={fieldIds.title} label={ {getAlertConfigToggleStatusLabel(props.cooldown.enabled)} diff --git a/frontend-modern/src/features/alerts/AlertEscalationSection.tsx b/frontend-modern/src/features/alerts/AlertEscalationSection.tsx index d09c0dd907..382fcd43a6 100644 --- a/frontend-modern/src/features/alerts/AlertEscalationSection.tsx +++ b/frontend-modern/src/features/alerts/AlertEscalationSection.tsx @@ -30,9 +30,11 @@ interface AlertEscalationSectionProps { export function AlertEscalationSection(props: AlertEscalationSectionProps) { const fieldIdPrefix = `alert-escalation-${createUniqueId()}`; + const titleId = `${fieldIdPrefix}-title`; return ( props.setEscalationEnabled(event.currentTarget.checked)} containerClass="sm:self-start" + ariaLabelledBy={titleId} label={ {getAlertConfigToggleStatusLabel(props.escalation.enabled)} diff --git a/frontend-modern/src/features/alerts/AlertGroupingSection.tsx b/frontend-modern/src/features/alerts/AlertGroupingSection.tsx index 6cfdf8745d..c5fe318304 100644 --- a/frontend-modern/src/features/alerts/AlertGroupingSection.tsx +++ b/frontend-modern/src/features/alerts/AlertGroupingSection.tsx @@ -31,11 +31,13 @@ interface AlertGroupingSectionProps { export function AlertGroupingSection(props: AlertGroupingSectionProps) { const fieldIdPrefix = `alert-grouping-${createUniqueId()}`; const fieldIds = { + title: `${fieldIdPrefix}-title`, window: `${fieldIdPrefix}-window`, }; return ( props.setGroupingEnabled(event.currentTarget.checked)} containerClass="sm:self-start" + ariaLabelledBy={fieldIds.title} label={ {getAlertConfigToggleStatusLabel(props.grouping.enabled)} diff --git a/frontend-modern/src/features/alerts/AlertQuietHoursSection.tsx b/frontend-modern/src/features/alerts/AlertQuietHoursSection.tsx index 19deb7efec..6bb3724e4d 100644 --- a/frontend-modern/src/features/alerts/AlertQuietHoursSection.tsx +++ b/frontend-modern/src/features/alerts/AlertQuietHoursSection.tsx @@ -10,6 +10,7 @@ import { ALERT_CONFIG_QUIET_HOURS_START_TIME_LABEL, ALERT_CONFIG_QUIET_HOURS_TIMEZONE_LABEL, ALERT_CONFIG_QUIET_HOURS_TITLE, + getAlertConfigToggleStatusLabel, } from '@/utils/alertConfigPresentation'; import { getAlertQuietDayButtonClass, @@ -42,12 +43,14 @@ interface AlertQuietHoursSectionProps { export function AlertQuietHoursSection(props: AlertQuietHoursSectionProps) { const fieldIdPrefix = `alert-quiet-hours-${createUniqueId()}`; const fieldIds = { + title: `${fieldIdPrefix}-title`, start: `${fieldIdPrefix}-start`, end: `${fieldIdPrefix}-end`, }; return ( props.setQuietHoursEnabled(event.currentTarget.checked)} containerClass="sm:self-start" + ariaLabelledBy={fieldIds.title} label={ - {props.quietHours.enabled ? 'Enabled' : 'Disabled'} + {getAlertConfigToggleStatusLabel(props.quietHours.enabled)} } /> diff --git a/frontend-modern/src/features/alerts/AlertRecoverySection.tsx b/frontend-modern/src/features/alerts/AlertRecoverySection.tsx index deac9fa3c6..2d12268a18 100644 --- a/frontend-modern/src/features/alerts/AlertRecoverySection.tsx +++ b/frontend-modern/src/features/alerts/AlertRecoverySection.tsx @@ -1,3 +1,5 @@ +import { createUniqueId } from 'solid-js'; + import { SettingsPanel } from '@/components/shared/SettingsPanel'; import { Toggle } from '@/components/shared/Toggle'; import { formHelpText } from '@/components/shared/Form'; @@ -14,8 +16,11 @@ interface AlertRecoverySectionProps { } export function AlertRecoverySection(props: AlertRecoverySectionProps) { + const titleId = `alert-recovery-${createUniqueId()}-title`; + return ( props.setNotifyOnResolveEnabled(event.currentTarget.checked)} containerClass="sm:self-start" + ariaLabelledBy={titleId} label={ {getAlertConfigToggleStatusLabel(props.notifyOnResolve)} diff --git a/frontend-modern/src/features/alerts/AlertScheduleSections.test.tsx b/frontend-modern/src/features/alerts/AlertScheduleSections.test.tsx index 50aa0a8e50..87ef726eca 100644 --- a/frontend-modern/src/features/alerts/AlertScheduleSections.test.tsx +++ b/frontend-modern/src/features/alerts/AlertScheduleSections.test.tsx @@ -4,6 +4,7 @@ import { AlertCooldownSection } from './AlertCooldownSection'; import { AlertEscalationSection } from './AlertEscalationSection'; import { AlertGroupingSection } from './AlertGroupingSection'; import { AlertQuietHoursSection } from './AlertQuietHoursSection'; +import { AlertRecoverySection } from './AlertRecoverySection'; import type { CooldownConfig, EscalationConfig, GroupingConfig, QuietHoursConfig } from './types'; function makeQuietHours(overrides: Partial = {}): QuietHoursConfig { @@ -55,6 +56,10 @@ describe('Alert schedule sections', () => { /> )); + expect(screen.getByRole('button', { name: 'Quiet hours Enabled' })).toHaveAttribute( + 'aria-pressed', + 'true', + ); expect(screen.getByLabelText('Start time')).toHaveAttribute('type', 'time'); expect(screen.getByLabelText('End time')).toHaveAttribute('type', 'time'); expect(screen.getByRole('combobox', { name: 'Timezone' })).toBeInTheDocument(); @@ -77,6 +82,10 @@ describe('Alert schedule sections', () => { /> )); + expect(screen.getByRole('button', { name: 'Alert cooldown Enabled' })).toHaveAttribute( + 'aria-pressed', + 'true', + ); expect(screen.getByRole('spinbutton', { name: 'Cooldown period' })).toBeInTheDocument(); expect(screen.getByRole('spinbutton', { name: 'Max alerts / hour' })).toBeInTheDocument(); }); @@ -94,6 +103,10 @@ describe('Alert schedule sections', () => { /> )); + expect(screen.getByRole('button', { name: 'Smart grouping Enabled' })).toHaveAttribute( + 'aria-pressed', + 'true', + ); expect(screen.getByRole('slider', { name: 'Grouping window' })).toHaveAttribute( 'aria-valuetext', '10 minutes', @@ -119,8 +132,23 @@ describe('Alert schedule sections', () => { /> )); + expect(screen.getByRole('button', { name: 'Alert escalation Enabled' })).toHaveAttribute( + 'aria-pressed', + 'true', + ); expect(screen.getByRole('spinbutton', { name: 'After' })).toBeInTheDocument(); expect(screen.getByRole('combobox', { name: 'Notify' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Remove escalation level' })).toBeInTheDocument(); }); + + it('associates the visible recovery heading with its status toggle', () => { + render(() => ( + + )); + + expect(screen.getByRole('button', { name: 'Recovery notifications Disabled' })).toHaveAttribute( + 'aria-pressed', + 'false', + ); + }); });