fix(a11y): add keyboard accessibility to HomeScreen profile list
- Profile list rows are now focusable (tabIndex) and operable via Enter/Space (onKeyDown handler) - Added role=listbox on the <ul> and role=option + aria-selected on non-active rows - Added descriptive aria-label including profile name and active state - Added focus-visible CSS ring matching the design system pattern - Updated test role from 'list' to 'listbox'
This commit is contained in:
parent
f1236e7eb8
commit
e9022b36b9
3 changed files with 24 additions and 2 deletions
|
|
@ -116,11 +116,15 @@ export function HomeScreen({ onNavigate, onCreateProfile }: HomeScreenProps) {
|
||||||
</Button>
|
</Button>
|
||||||
</header>
|
</header>
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<ul className="home-profile-list">
|
<ul className="home-profile-list" role="listbox" aria-label="Profiles">
|
||||||
{state?.profiles.map((profile) => (
|
{state?.profiles.map((profile) => (
|
||||||
<li
|
<li
|
||||||
key={profile.npub}
|
key={profile.npub}
|
||||||
className={`home-profile-row${profile.is_active ? ' is-active' : ''}`}
|
className={`home-profile-row${profile.is_active ? ' is-active' : ''}`}
|
||||||
|
role={profile.is_active ? undefined : 'option'}
|
||||||
|
aria-selected={profile.is_active}
|
||||||
|
tabIndex={profile.is_active ? undefined : 0}
|
||||||
|
aria-label={`${profile.label}${profile.is_active ? ' (active)' : ''} — select profile`}
|
||||||
onClick={
|
onClick={
|
||||||
profile.is_active
|
profile.is_active
|
||||||
? undefined
|
? undefined
|
||||||
|
|
@ -131,6 +135,19 @@ export function HomeScreen({ onNavigate, onCreateProfile }: HomeScreenProps) {
|
||||||
void onSelect(profile.npub);
|
void onSelect(profile.npub);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onKeyDown={
|
||||||
|
profile.is_active
|
||||||
|
? undefined
|
||||||
|
: (event) => {
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
event.preventDefault();
|
||||||
|
if ((event.target as HTMLElement).closest('button, a, input')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void onSelect(profile.npub);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
npub={profile.npub}
|
npub={profile.npub}
|
||||||
|
|
|
||||||
|
|
@ -1557,6 +1557,11 @@ select {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.home-profile-row:not(.is-active):focus-visible {
|
||||||
|
outline: 2px solid var(--focus);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.home-profile-row .active-profile-meta {
|
.home-profile-row .active-profile-meta {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ describe('HomeScreen', () => {
|
||||||
renderWithApp(<HomeScreen onNavigate={vi.fn()} onCreateProfile={vi.fn()} />);
|
renderWithApp(<HomeScreen onNavigate={vi.fn()} onCreateProfile={vi.fn()} />);
|
||||||
|
|
||||||
// The active profile row carries the "Selected" badge; find Alice via the profile list.
|
// The active profile row carries the "Selected" badge; find Alice via the profile list.
|
||||||
const profileList = await screen.findByRole('list');
|
const profileList = await screen.findByRole('listbox');
|
||||||
const aliceRow = within(profileList)
|
const aliceRow = within(profileList)
|
||||||
.getByText('Alice')
|
.getByText('Alice')
|
||||||
.closest('.home-profile-row') as HTMLElement;
|
.closest('.home-profile-row') as HTMLElement;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue