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',
+ );
+ });
});