mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
fix(planner): the plan view panel tabs say whether their panel is open and show a focus ring
In the plan view the tabs that collapse and reopen the left and right
panels gave a screen reader no open or closed state and showed no focus
ring when reached with the keyboard. The same tabs in the tour planner
had both, because only there LeftPanelTab got the "announced" option,
and the plan view's right tab was a hand copy of the left one.
LeftPanelTab becomes PanelTab with a side prop and always sets
aria-expanded and the focus ring. The plan view's right tab renders it
mirrored, so every panel tab shares one component. The offsets are
written with literal left and right keys so lint:rtl still counts them.
The page shrinks by 11 lines.
Red test: FE-PAGE-PLANNER-016b in client/src/pages/TripPlannerPage.test.tsx
failed on the old page with
expect(element).toHaveAttribute("aria-expanded", "true").
FE-PAGE-PLANNER-LEFTTAB-005 covers the mirrored right tab.
Changed assertions (they asserted the missing state):
- TripPlannerPage.leftPanelTab.test.tsx FE-PAGE-PLANNER-LEFTTAB-003:
not.toHaveAttribute('aria-expanded') and not.toHaveClass
('focus-visible:outline') are now toHaveAttribute('aria-expanded',
'true') and toHaveClass('focus-visible:outline') (title updated).
- TripPlannerPage.leftPanelTab.test.tsx FE-PAGE-PLANNER-LEFTTAB-004: the
"announced" prop is dropped from both renders (title updated); the
expectations are unchanged. The file renders PanelTab instead of
LeftPanelTab.
This commit is contained in:
@@ -24,7 +24,7 @@
|
||||
"src/mobile/screens/admin/MAdminPluginsPanel.tsx": 1507,
|
||||
"src/pages/JourneyDetailPage.test.tsx": 4030,
|
||||
"src/pages/LoginPage.tsx": 1348,
|
||||
"src/pages/TripPlannerPage.tsx": 1179,
|
||||
"src/pages/TripPlannerPage.tsx": 1168,
|
||||
"src/pages/TripPlannerPage.wiring.test.tsx": 2049,
|
||||
"src/pages/atlas/useAtlas.ts": 1287,
|
||||
"src/pages/tripPlanner/useTripPlanner.roadtrip.test.tsx": 3531,
|
||||
|
||||
@@ -2,14 +2,14 @@ import { fireEvent, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { render } from '../../tests/helpers/render';
|
||||
import { LeftPanelTab } from './TripPlannerPage';
|
||||
import { PanelTab } from './TripPlannerPage';
|
||||
|
||||
// FE-PAGE-PLANNER-LEFTTAB-001 to FE-PAGE-PLANNER-LEFTTAB-004
|
||||
// FE-PAGE-PLANNER-LEFTTAB-001 to FE-PAGE-PLANNER-LEFTTAB-005
|
||||
|
||||
describe('LeftPanelTab', () => {
|
||||
describe('PanelTab', () => {
|
||||
it('FE-PAGE-PLANNER-LEFTTAB-001: an open panel shows a flap on its edge that collapses it', () => {
|
||||
const onToggle = vi.fn();
|
||||
render(<LeftPanelTab hidden={false} label="Collapse" onToggle={onToggle} />);
|
||||
render(<PanelTab hidden={false} label="Collapse" onToggle={onToggle} />);
|
||||
const tab = screen.getByRole('button', { name: 'Collapse' });
|
||||
expect(tab).toHaveClass('text-content-faint');
|
||||
expect(tab.style.position).toBe('absolute');
|
||||
@@ -20,7 +20,7 @@ describe('LeftPanelTab', () => {
|
||||
});
|
||||
|
||||
it('FE-PAGE-PLANNER-LEFTTAB-002: a hidden panel leaves a raised accent tile', () => {
|
||||
render(<LeftPanelTab hidden label="Plan" onToggle={vi.fn()} />);
|
||||
render(<PanelTab hidden label="Plan" onToggle={vi.fn()} />);
|
||||
const tab = screen.getByRole('button', { name: 'Plan' });
|
||||
expect(tab).toHaveClass('bg-accent');
|
||||
expect(tab.style.position).toBe('fixed');
|
||||
@@ -28,21 +28,39 @@ describe('LeftPanelTab', () => {
|
||||
expect(tab.style.borderRadius).toBe('10px');
|
||||
});
|
||||
|
||||
it('FE-PAGE-PLANNER-LEFTTAB-003: says nothing about its state and has no focus ring by default', () => {
|
||||
render(<LeftPanelTab hidden={false} label="Collapse" onToggle={vi.fn()} />);
|
||||
it('FE-PAGE-PLANNER-LEFTTAB-003: an open tab says its panel is open and shows a focus ring', () => {
|
||||
render(<PanelTab hidden={false} label="Collapse" onToggle={vi.fn()} />);
|
||||
const tab = screen.getByRole('button', { name: 'Collapse' });
|
||||
expect(tab).not.toHaveAttribute('aria-expanded');
|
||||
expect(tab).not.toHaveClass('focus-visible:outline');
|
||||
expect(tab).toHaveAttribute('aria-expanded', 'true');
|
||||
expect(tab).toHaveClass('focus-visible:outline');
|
||||
});
|
||||
|
||||
it('FE-PAGE-PLANNER-LEFTTAB-004: an announced tab states whether the panel is open and shows a focus ring', () => {
|
||||
const { rerender } = render(<LeftPanelTab hidden={false} label="Collapse" onToggle={vi.fn()} announced />);
|
||||
it('FE-PAGE-PLANNER-LEFTTAB-004: the tab states whether the panel is open and shows a focus ring', () => {
|
||||
const { rerender } = render(<PanelTab hidden={false} label="Collapse" onToggle={vi.fn()} />);
|
||||
const tab = screen.getByRole('button', { name: 'Collapse' });
|
||||
expect(tab).toHaveAttribute('aria-expanded', 'true');
|
||||
expect(tab).toHaveClass('text-content-faint', 'focus-visible:outline');
|
||||
rerender(<LeftPanelTab hidden label="Plan" onToggle={vi.fn()} announced />);
|
||||
rerender(<PanelTab hidden label="Plan" onToggle={vi.fn()} />);
|
||||
const hiddenTab = screen.getByRole('button', { name: 'Plan' });
|
||||
expect(hiddenTab).toHaveAttribute('aria-expanded', 'false');
|
||||
expect(hiddenTab).toHaveClass('bg-accent', 'focus-visible:outline');
|
||||
});
|
||||
|
||||
it('FE-PAGE-PLANNER-LEFTTAB-005: the right panel tab of the plan view is the same tab, mirrored', () => {
|
||||
const onToggle = vi.fn();
|
||||
const { rerender } = render(<PanelTab side="right" hidden={false} label="Collapse" onToggle={onToggle} />);
|
||||
const tab = screen.getByRole('button', { name: 'Collapse' });
|
||||
expect(tab).toHaveAttribute('aria-expanded', 'true');
|
||||
expect(tab).toHaveClass('text-content-faint', 'focus-visible:outline');
|
||||
expect(tab.style.left).toBe('-28px');
|
||||
expect(tab.style.borderRadius).toBe('10px 0 0 10px');
|
||||
fireEvent.click(tab);
|
||||
expect(onToggle).toHaveBeenCalledTimes(1);
|
||||
rerender(<PanelTab side="right" hidden label="Places" onToggle={onToggle} />);
|
||||
const hiddenTab = screen.getByRole('button', { name: 'Places' });
|
||||
expect(hiddenTab).toHaveAttribute('aria-expanded', 'false');
|
||||
expect(hiddenTab).toHaveClass('bg-accent', 'focus-visible:outline');
|
||||
expect(hiddenTab.style.position).toBe('fixed');
|
||||
expect(hiddenTab.style.right).toBe('10px');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -607,6 +607,30 @@ describe('TripPlannerPage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('FE-PAGE-PLANNER-016b: both plan view panel tabs say whether their panel is open', () => {
|
||||
it('gives the left and right tab aria-expanded and a focus ring, and flips it when a panel closes', async () => {
|
||||
vi.useFakeTimers();
|
||||
seedTripStore({ id: 42 });
|
||||
renderPlannerPage(42);
|
||||
act(() => { vi.runAllTimers(); });
|
||||
vi.useRealTimers();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByRole('button', { name: 'Collapse' })).toHaveLength(2);
|
||||
});
|
||||
for (const tab of screen.getAllByRole('button', { name: 'Collapse' })) {
|
||||
expect(tab).toHaveAttribute('aria-expanded', 'true');
|
||||
expect(tab).toHaveClass('focus-visible:outline');
|
||||
}
|
||||
|
||||
const rightTab = screen.getAllByRole('button', { name: 'Collapse' })[1];
|
||||
fireEvent.click(rightTab);
|
||||
await waitFor(() => expect(rightTab).toHaveAttribute('aria-expanded', 'false'));
|
||||
expect(rightTab).toHaveAccessibleName('Places');
|
||||
expect(rightTab).toHaveClass('focus-visible:outline');
|
||||
});
|
||||
});
|
||||
|
||||
describe('FE-PAGE-PLANNER-017: Trip navigation error redirects to dashboard', () => {
|
||||
it('navigates to /dashboard when loadTrip rejects', async () => {
|
||||
seedStore(useTripStore, {
|
||||
|
||||
@@ -300,7 +300,7 @@ function TripPlannerPageDesktop(): React.ReactElement | null {
|
||||
/>
|
||||
|
||||
<div className="hidden md:block" style={{ position: 'absolute', left: 10, top: 10, bottom: 10, zIndex: 20 }}>
|
||||
<LeftPanelTab hidden={leftHidden} label={leftHidden ? t('trip.mobilePlan') : t('common.collapse')} onToggle={toggleLeft} />
|
||||
<PanelTab hidden={leftHidden} label={leftHidden ? t('trip.mobilePlan') : t('common.collapse')} onToggle={toggleLeft} />
|
||||
|
||||
<div style={{
|
||||
width: leftHidden ? 0 : leftWidth, height: '100%',
|
||||
@@ -424,22 +424,7 @@ function TripPlannerPageDesktop(): React.ReactElement | null {
|
||||
</div>
|
||||
|
||||
<div className="hidden md:block" style={{ position: 'absolute', right: 10, top: 10, bottom: 10, zIndex: 20 }}>
|
||||
{/* The panel's tab: a flap on its edge while open, a raised accent tile once it is tucked away. */}
|
||||
<Tooltip label={rightHidden ? t('trip.mobilePlaces') : t('common.collapse')} placement="left">
|
||||
<button type="button" onClick={toggleRight}
|
||||
aria-label={rightHidden ? t('trip.mobilePlaces') : t('common.collapse')}
|
||||
className={rightHidden ? 'bg-accent text-accent-text shadow-md hover:opacity-90' : 'text-content-faint hover:text-content'}
|
||||
style={{
|
||||
position: rightHidden ? 'fixed' : 'absolute', top: rightHidden ? 'calc(var(--nav-h) + 44px + 14px)' : 14, right: rightHidden ? 10 : undefined, left: rightHidden ? undefined : -28, zIndex: -1,
|
||||
width: 36, height: 36, borderRadius: rightHidden ? 10 : '10px 0 0 10px',
|
||||
background: rightHidden ? undefined : 'var(--sidebar-bg)', backdropFilter: 'blur(20px)', WebkitBackdropFilter: 'blur(20px)',
|
||||
border: 'none',
|
||||
cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
transition: 'color 0.15s',
|
||||
}}>
|
||||
{rightHidden ? <PanelRightOpen size={16} /> : <PanelRightClose size={16} />}
|
||||
</button>
|
||||
</Tooltip>
|
||||
<PanelTab side="right" hidden={rightHidden} label={rightHidden ? t('trip.mobilePlaces') : t('common.collapse')} onToggle={toggleRight} />
|
||||
|
||||
<div style={{
|
||||
width: rightHidden ? 0 : rightWidth, height: '100%',
|
||||
@@ -671,7 +656,7 @@ function TripPlannerPageDesktop(): React.ReactElement | null {
|
||||
onViewBaseLayerChange={tourPlanner.setMapBaseLayer}
|
||||
/>
|
||||
<div className="hidden md:block" style={{ position: 'absolute', left: 10, top: 10, bottom: 10, zIndex: 20 }}>
|
||||
<LeftPanelTab hidden={leftHidden} label={leftHidden ? t('trip.mobilePlan') : t('common.collapse')} onToggle={toggleLeft} announced />
|
||||
<PanelTab hidden={leftHidden} label={leftHidden ? t('trip.mobilePlan') : t('common.collapse')} onToggle={toggleLeft} />
|
||||
<div style={{
|
||||
width: leftHidden ? 0 : leftPanelPx, height: '100%', position: 'relative',
|
||||
background: 'var(--sidebar-bg)', backdropFilter: 'blur(24px) saturate(180%)',
|
||||
@@ -984,31 +969,38 @@ function TripPlannerPageDesktop(): React.ReactElement | null {
|
||||
)
|
||||
}
|
||||
|
||||
interface LeftPanelTabProps {
|
||||
interface PanelTabProps {
|
||||
hidden: boolean
|
||||
label: string
|
||||
onToggle: () => void
|
||||
/** Also states whether the panel is open and shows a focus ring (the tour planner's tab). */
|
||||
announced?: boolean
|
||||
side?: 'left' | 'right'
|
||||
}
|
||||
|
||||
/** The left panel's tab: a flap on its edge while open, a raised accent tile once it is tucked away. */
|
||||
export function LeftPanelTab({ hidden, label, onToggle, announced = false }: LeftPanelTabProps): React.ReactElement {
|
||||
/**
|
||||
* A side panel's tab: a flap on its edge while open, a raised accent tile once it is tucked away.
|
||||
* One component for both panels in every view, so each states whether its panel is open and shows a focus ring.
|
||||
*/
|
||||
export function PanelTab({ hidden, label, onToggle, side = 'left' }: PanelTabProps): React.ReactElement {
|
||||
const look = hidden ? 'bg-accent text-accent-text shadow-md hover:opacity-90' : 'text-content-faint hover:text-content'
|
||||
const left = side === 'left'
|
||||
const [Open, Close] = left ? [PanelLeftOpen, PanelLeftClose] : [PanelRightOpen, PanelRightClose]
|
||||
return (
|
||||
<Tooltip label={label} placement="right">
|
||||
<Tooltip label={label} placement={left ? 'right' : 'left'}>
|
||||
<button type="button" onClick={onToggle}
|
||||
aria-label={label}
|
||||
aria-expanded={announced ? !hidden : undefined}
|
||||
className={announced ? `${look} focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent` : look}
|
||||
aria-expanded={!hidden}
|
||||
className={`${look} focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent`}
|
||||
style={{
|
||||
position: hidden ? 'fixed' : 'absolute', top: hidden ? 'calc(var(--nav-h) + 44px + 14px)' : 14,
|
||||
left: hidden ? 10 : undefined, right: hidden ? undefined : -28, zIndex: -1,
|
||||
width: 36, height: 36, borderRadius: hidden ? 10 : '0 10px 10px 0',
|
||||
...(left
|
||||
? { left: hidden ? 10 : undefined, right: hidden ? undefined : -28 }
|
||||
: { right: hidden ? 10 : undefined, left: hidden ? undefined : -28 }),
|
||||
zIndex: -1,
|
||||
width: 36, height: 36, borderRadius: hidden ? 10 : left ? '0 10px 10px 0' : '10px 0 0 10px',
|
||||
background: hidden ? undefined : 'var(--sidebar-bg)', backdropFilter: 'blur(20px)', WebkitBackdropFilter: 'blur(20px)',
|
||||
border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'color 0.15s',
|
||||
}}>
|
||||
{hidden ? <PanelLeftOpen size={16} /> : <PanelLeftClose size={16} />}
|
||||
{hidden ? <Open size={16} /> : <Close size={16} />}
|
||||
</button>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user