fix(web): settings and bell use accessible checkboxes, not slide toggles

@pi/ui's slide toggle hides its input with display: none and gives it
no name: neither the Tab key nor a screen reader could reach the
switches in settings (visibility, the public's proposals, the
definition of done, the link for clients) or the bell's (e-mail copy,
notices on this device). They are pi-checkbox now, each with a name
saying what it turns on: reachable by keyboard, toggled with the space
bar and announced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-16 20:20:06 +02:00
parent e10c9b84bf
commit 4969fb3f6d
6 changed files with 46 additions and 80 deletions

View File

@@ -17,12 +17,7 @@ import {
untracked,
} from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';
import {
PiCheckboxComponent,
PiIconComponent,
PiNoteComponent,
PiSlideToggleComponent,
} from '@pi/ui';
import { PiCheckboxComponent, PiIconComponent, PiNoteComponent } from '@pi/ui';
import { GitIntegrationComponent } from '../ui/git-integration';
import { WebhooksComponent } from '../ui/webhooks';
import { TransferComponent } from '../ui/transfer';
@@ -156,7 +151,6 @@ function idFrom(name: string): string {
NgTemplateOutlet,
PiIconComponent,
PiNoteComponent,
PiSlideToggleComponent,
],
template: `
<div class="ts-settings-layout">
@@ -215,15 +209,13 @@ function idFrom(name: string): string {
}
</p>
</div>
<div class="ts-toggle">
<pi-slide-toggle
label="Proyecto público"
[checked]="project.isPublic"
[disabled]="!facade.canManage()"
(checkedChange)="facade.setPublic($event)"
/>
<span aria-hidden="true">{{ project.isPublic ? 'Público' : 'Privado' }}</span>
</div>
<pi-checkbox
class="ts-toggle"
label="Proyecto público"
[checked]="project.isPublic"
[disabled]="!facade.canManage()"
(checkedChange)="facade.setPublic($event)"
/>
</section>
<section
@@ -249,15 +241,13 @@ function idFrom(name: string): string {
}
</p>
</div>
<div class="ts-toggle">
<pi-slide-toggle
label="Propuestas del público abiertas"
[checked]="project.isPublic && project.acceptsProposals"
[disabled]="!facade.canManage() || !project.isPublic"
(checkedChange)="facade.setAcceptsProposals($event)"
/>
<span aria-hidden="true">{{ project.isPublic && project.acceptsProposals ? 'Abierto' : 'Cerrado' }}</span>
</div>
<pi-checkbox
class="ts-toggle"
label="Formulario abierto"
[checked]="project.isPublic && project.acceptsProposals"
[disabled]="!facade.canManage() || !project.isPublic"
(checkedChange)="facade.setAcceptsProposals($event)"
/>
</section>
@if (facade.canManage()) {
@@ -464,14 +454,12 @@ function idFrom(name: string): string {
medias. El propietario y los admins sí pueden cerrarlo.
</p>
</div>
<div class="ts-toggle">
<pi-slide-toggle
label="Definición de «Hecho» activada"
[checked]="!!d.checklistBeforeDone"
(checkedChange)="setChecklistBeforeDone($event)"
/>
<span aria-hidden="true">{{ d.checklistBeforeDone ? 'Activada' : 'Desactivada' }}</span>
</div>
<pi-checkbox
class="ts-toggle"
label="Exigir la checklist completa"
[checked]="!!d.checklistBeforeDone"
(checkedChange)="setChecklistBeforeDone($event)"
/>
</header>
</section>
@@ -1103,10 +1091,7 @@ function idFrom(name: string): string {
}
}
.ts-toggle {
display: inline-flex;
align-items: center;
gap: 0.625rem;
font-size: 0.875rem;
flex: none;
font-weight: 600;
}
.ts-danger__confirm {

View File

@@ -7,7 +7,7 @@ import {
input,
untracked,
} from '@angular/core';
import { PiCheckboxComponent, PiSlideToggleComponent } from '@pi/ui';
import { PiCheckboxComponent } from '@pi/ui';
import { ToastService } from '@task-stream/web/shared/ui';
import {
provideSharingFacade,
@@ -37,7 +37,7 @@ const DEFAULT_BLOCKS: SharedBlock[] = ['flow', 'sprint', 'releases'];
@Component({
selector: 'ts-client-share',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [PiCheckboxComponent, PiSlideToggleComponent],
imports: [PiCheckboxComponent],
providers: [provideSharingFacade()],
host: { class: 'ts-panel', role: 'region', 'aria-labelledby': 'share-title' },
template: `
@@ -49,17 +49,13 @@ const DEFAULT_BLOCKS: SharedBlock[] = ['flow', 'sprint', 'releases'];
enseña personas ni tickets sueltos (salvo las épicas).
</p>
</div>
<div class="ts-toggle">
<pi-slide-toggle
label="Panel para clientes compartido"
[checked]="!!sharing.share()"
[disabled]="!sharing.loaded() || sharing.busy()"
(checkedChange)="toggle($event)"
/>
<span aria-hidden="true">{{
sharing.share() ? 'Compartido' : 'Sin compartir'
}}</span>
</div>
<pi-checkbox
class="ts-toggle"
label="Compartir con clientes"
[checked]="!!sharing.share()"
[disabled]="!sharing.loaded() || sharing.busy()"
(checkedChange)="toggle($event)"
/>
</div>
@if (sharing.share(); as share) {
@@ -187,10 +183,7 @@ const DEFAULT_BLOCKS: SharedBlock[] = ['flow', 'sprint', 'releases'];
font-size: 0.875rem;
}
.ts-toggle {
display: inline-flex;
align-items: center;
gap: 0.625rem;
font-size: 0.875rem;
flex: none;
font-weight: 600;
}
`,

View File

@@ -11,7 +11,7 @@ import {
PiBadgeComponent,
PiIconButtonComponent,
PiIconComponent,
PiSlideToggleComponent,
PiCheckboxComponent,
} from '@pi/ui';
import { DrawerService } from '@task-stream/web/shared/ui';
import { InboxFacade, PushFacade } from '@task-stream/web/tracker/application';
@@ -46,7 +46,7 @@ const ICON: Record<NotificationKind, string> = {
PiBadgeComponent,
PiIconButtonComponent,
PiIconComponent,
PiSlideToggleComponent,
PiCheckboxComponent,
],
template: `
<pi-badge
@@ -81,25 +81,19 @@ const ICON: Record<NotificationKind, string> = {
(execute)="inbox.markRead()"
/>
@if (inbox.settings()?.emailAvailable) {
<div class="ts-inbox__switch">
<pi-slide-toggle
label="Copia por correo"
[checked]="inbox.settings()?.email ?? false"
(checkedChange)="inbox.setEmail($event)"
/>
<span aria-hidden="true">Copia por correo</span>
</div>
<pi-checkbox
label="Copia por correo"
[checked]="inbox.settings()?.email ?? false"
(checkedChange)="inbox.setEmail($event)"
/>
}
@if (inbox.settings()?.pushPublicKey && push.supported) {
<div class="ts-inbox__switch">
<pi-slide-toggle
label="Avisos en este dispositivo"
[checked]="push.on()"
[disabled]="push.busy()"
(checkedChange)="togglePush($event)"
/>
<span aria-hidden="true">Avisos en este dispositivo</span>
</div>
<pi-checkbox
label="Avisos en este dispositivo"
[checked]="push.on()"
[disabled]="push.busy()"
(checkedChange)="togglePush($event)"
/>
}
</div>
@if (push.error(); as error) {
@@ -156,12 +150,6 @@ const ICON: Record<NotificationKind, string> = {
justify-content: space-between;
gap: 0.75rem;
}
.ts-inbox__switch {
display: inline-flex;
gap: 0.5rem;
align-items: center;
font-size: 0.875rem;
}
.ts-inbox__error {
margin: 0;
color: var(--ts-c-red, var(--ts-ink));