mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-20 02:42:11 +00:00
Label alert schedule toggles by section
This commit is contained in:
@@ -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) {
|
||||
<div class="px-3 py-3 sm:px-6 sm:py-4 border-b border-border bg-surface-alt">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||
<div class="flex min-w-0 flex-col gap-1 flex-1">
|
||||
<h2 class="text-sm sm:text-base tracking-tight font-semibold text-base-content dark:text-slate-100">
|
||||
<h2
|
||||
id={local.titleId}
|
||||
class="text-sm sm:text-base tracking-tight font-semibold text-base-content dark:text-slate-100"
|
||||
>
|
||||
{local.title}
|
||||
</h2>
|
||||
<Show when={local.description}>
|
||||
<p class="text-xs sm:text-sm text-muted dark:text-slate-200">
|
||||
{local.description}
|
||||
</p>
|
||||
<p class="text-xs sm:text-sm text-muted dark:text-slate-200">{local.description}</p>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={local.action}>
|
||||
|
||||
@@ -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(() => (
|
||||
<>
|
||||
<span id="schedule-section-label">Quiet hours</span>
|
||||
<Toggle
|
||||
checked={false}
|
||||
ariaLabelledBy="schedule-section-label"
|
||||
label={<span>Disabled</span>}
|
||||
/>
|
||||
</>
|
||||
));
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
<div class={getToggleContainerClass(props.containerClass)}>
|
||||
<TogglePrimitive
|
||||
{...props}
|
||||
size={size}
|
||||
ariaLabelledBy={props.label ? labelId : props.ariaLabelledBy}
|
||||
ariaDescribedBy={props.description ? descriptionId : props.ariaDescribedBy}
|
||||
ariaLabelledBy={labelledBy()}
|
||||
ariaDescribedBy={describedBy()}
|
||||
/>
|
||||
{(props.label || props.description) && (
|
||||
<span class={getToggleLabelClass()}>
|
||||
|
||||
@@ -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 (
|
||||
<SettingsPanel
|
||||
titleId={fieldIds.title}
|
||||
title={ALERT_CONFIG_COOLDOWN_TITLE}
|
||||
description={ALERT_CONFIG_COOLDOWN_DESCRIPTION}
|
||||
action={
|
||||
@@ -40,6 +42,7 @@ export function AlertCooldownSection(props: AlertCooldownSectionProps) {
|
||||
checked={props.cooldown.enabled}
|
||||
onChange={(event) => props.setCooldownEnabled(event.currentTarget.checked)}
|
||||
containerClass="sm:self-start"
|
||||
ariaLabelledBy={fieldIds.title}
|
||||
label={
|
||||
<span class="text-xs font-medium text-muted">
|
||||
{getAlertConfigToggleStatusLabel(props.cooldown.enabled)}
|
||||
|
||||
@@ -30,9 +30,11 @@ interface AlertEscalationSectionProps {
|
||||
|
||||
export function AlertEscalationSection(props: AlertEscalationSectionProps) {
|
||||
const fieldIdPrefix = `alert-escalation-${createUniqueId()}`;
|
||||
const titleId = `${fieldIdPrefix}-title`;
|
||||
|
||||
return (
|
||||
<SettingsPanel
|
||||
titleId={titleId}
|
||||
title={ALERT_CONFIG_ESCALATION_TITLE}
|
||||
description={ALERT_CONFIG_ESCALATION_DESCRIPTION}
|
||||
action={
|
||||
@@ -40,6 +42,7 @@ export function AlertEscalationSection(props: AlertEscalationSectionProps) {
|
||||
checked={props.escalation.enabled}
|
||||
onChange={(event) => props.setEscalationEnabled(event.currentTarget.checked)}
|
||||
containerClass="sm:self-start"
|
||||
ariaLabelledBy={titleId}
|
||||
label={
|
||||
<span class="text-xs font-medium text-muted">
|
||||
{getAlertConfigToggleStatusLabel(props.escalation.enabled)}
|
||||
|
||||
@@ -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 (
|
||||
<SettingsPanel
|
||||
titleId={fieldIds.title}
|
||||
title={ALERT_CONFIG_GROUPING_TITLE}
|
||||
description={ALERT_CONFIG_GROUPING_DESCRIPTION}
|
||||
action={
|
||||
@@ -43,6 +45,7 @@ export function AlertGroupingSection(props: AlertGroupingSectionProps) {
|
||||
checked={props.grouping.enabled}
|
||||
onChange={(event) => props.setGroupingEnabled(event.currentTarget.checked)}
|
||||
containerClass="sm:self-start"
|
||||
ariaLabelledBy={fieldIds.title}
|
||||
label={
|
||||
<span class="text-xs font-medium text-muted">
|
||||
{getAlertConfigToggleStatusLabel(props.grouping.enabled)}
|
||||
|
||||
@@ -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 (
|
||||
<SettingsPanel
|
||||
titleId={fieldIds.title}
|
||||
title={ALERT_CONFIG_QUIET_HOURS_TITLE}
|
||||
description={ALERT_CONFIG_QUIET_HOURS_DESCRIPTION}
|
||||
action={
|
||||
@@ -55,9 +58,10 @@ export function AlertQuietHoursSection(props: AlertQuietHoursSectionProps) {
|
||||
checked={props.quietHours.enabled}
|
||||
onChange={(event) => props.setQuietHoursEnabled(event.currentTarget.checked)}
|
||||
containerClass="sm:self-start"
|
||||
ariaLabelledBy={fieldIds.title}
|
||||
label={
|
||||
<span class="text-xs font-medium text-muted">
|
||||
{props.quietHours.enabled ? 'Enabled' : 'Disabled'}
|
||||
{getAlertConfigToggleStatusLabel(props.quietHours.enabled)}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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 (
|
||||
<SettingsPanel
|
||||
titleId={titleId}
|
||||
title={ALERT_CONFIG_RECOVERY_TITLE}
|
||||
description={ALERT_CONFIG_RECOVERY_DESCRIPTION}
|
||||
action={
|
||||
@@ -23,6 +28,7 @@ export function AlertRecoverySection(props: AlertRecoverySectionProps) {
|
||||
checked={props.notifyOnResolve}
|
||||
onChange={(event) => props.setNotifyOnResolveEnabled(event.currentTarget.checked)}
|
||||
containerClass="sm:self-start"
|
||||
ariaLabelledBy={titleId}
|
||||
label={
|
||||
<span class="text-xs font-medium text-muted">
|
||||
{getAlertConfigToggleStatusLabel(props.notifyOnResolve)}
|
||||
|
||||
@@ -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> = {}): 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(() => (
|
||||
<AlertRecoverySection notifyOnResolve={false} setNotifyOnResolveEnabled={vi.fn()} />
|
||||
));
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Recovery notifications Disabled' })).toHaveAttribute(
|
||||
'aria-pressed',
|
||||
'false',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user