Skip to content
This repository was archived by the owner on Oct 14, 2025. It is now read-only.

Commit 9efb600

Browse files
authored
Chore: Add missing useRole tests (#97)
* Fixed impl, added additional test case * more tests * More tests * tests * ts expect error * Added missing tests for useRole
1 parent b2e019a commit 9efb600

File tree

3 files changed

+197
-8
lines changed

3 files changed

+197
-8
lines changed

src/lib/hooks/useRole/App.test.svelte

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@
3434
style={floating.floatingStyles}
3535
{...interactions.getFloatingProps()}
3636
>
37-
Floating
37+
{#each [1, 2, 3] as i}
38+
<div
39+
data-testid="item-{i}"
40+
{...interactions.getItemProps({ active: i === 2, selected: i === 2 })}
41+
></div>
42+
{/each}
3843
</div>
3944
{/if}

src/lib/hooks/useRole/index.svelte.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,24 @@ function useRole(context: FloatingContext, options: UseRoleOptions = {}): Elemen
4242

4343
const isNested = parentId != null;
4444

45+
const floatingProps = $derived({
46+
id: floatingId,
47+
...(ariaRole && { role: ariaRole }),
48+
});
49+
4550
return {
51+
// @ts-expect-error - variable prop is not specific enough
4652
get reference() {
4753
if (!enabled) {
4854
return {};
4955
}
56+
if (ariaRole === 'tooltip' || role === 'label') {
57+
return {
58+
[`aria-${role === 'label' ? 'labelledby' : 'describedby'}` as const]: open
59+
? floatingId
60+
: undefined,
61+
};
62+
}
5063
return {
5164
'aria-expanded': open ? 'true' : 'false',
5265
'aria-haspopup': ariaRole === 'alertdialog' ? 'dialog' : ariaRole,
@@ -62,9 +75,12 @@ function useRole(context: FloatingContext, options: UseRoleOptions = {}): Elemen
6275
if (!enabled) {
6376
return {};
6477
}
78+
if (ariaRole === 'tooltip' || role === 'label') {
79+
return floatingProps;
80+
}
6581
return {
66-
id: floatingId,
67-
...(ariaRole && { role: ariaRole }),
82+
...floatingProps,
83+
...(ariaRole === 'menu' && { 'aria-labelledby': referenceId }),
6884
};
6985
},
7086
get item() {

src/lib/hooks/useRole/index.test.svelte.ts

Lines changed: 173 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,24 +26,192 @@ describe('useRole', () => {
2626

2727
describe('role', () => {
2828
describe('dialog', () => {
29+
it('applies the `dialog` role to the floating element', () => {
30+
render(App, { role: 'dialog', open: true });
31+
32+
expect(screen.getByTestId('floating')).toHaveAttribute('role', 'dialog');
33+
});
34+
2935
it('applies the `aria-haspopup` attribute to the reference element', () => {
3036
render(App, { role: 'dialog' });
3137

3238
expect(screen.getByTestId('reference')).toHaveAttribute('aria-haspopup', 'dialog');
3339
});
3440

35-
it('applies the `dialog` role to the floating element', () => {
36-
render(App, { role: 'dialog', open: true });
41+
it('applies the `aria-expanded` attribute to the reference element based on `open` state', async () => {
42+
const { rerender } = render(App, { role: 'dialog', open: false });
3743

38-
expect(screen.getByTestId('floating')).toHaveAttribute('role', 'dialog');
44+
expect(screen.getByTestId('reference')).toHaveAttribute('aria-expanded', 'false');
45+
46+
await rerender({ open: true });
47+
48+
expect(screen.getByTestId('reference')).toHaveAttribute('aria-expanded', 'true');
3949
});
4050

41-
it('applies the `aria-expanded` attribute to the reference element based on `open` state', async () => {
51+
it('applies the `aria-controls` attribute with the correct id to the reference element based on the `open` state', async () => {
4252
const { rerender } = render(App, { role: 'dialog', open: false });
4353

54+
expect(screen.getByTestId('reference')).not.toHaveAttribute('aria-controls');
55+
56+
await rerender({ open: true });
57+
58+
expect(screen.getByTestId('reference')).toHaveAttribute(
59+
'aria-controls',
60+
screen.getByTestId('floating').id,
61+
);
62+
});
63+
});
64+
65+
describe('label', () => {
66+
it('applies the `aria-labelledby` attribute to the reference element', () => {
67+
render(App, { role: 'label', open: true });
68+
69+
expect(screen.getByTestId('reference')).toHaveAttribute('aria-labelledby');
70+
});
71+
});
72+
73+
describe('tooltip', () => {
74+
it('applies the `tooltip` role to the floating element', () => {
75+
render(App, { role: 'tooltip', open: true });
76+
77+
expect(screen.getByTestId('floating')).toHaveAttribute('role', 'tooltip');
78+
});
79+
80+
it('applies the `aria-describedby` attribute to the reference element based on the `open` state', async () => {
81+
const { rerender } = render(App, { role: 'tooltip', open: false });
82+
83+
expect(screen.getByTestId('reference')).not.toHaveAttribute('aria-describedby');
84+
85+
await rerender({ open: true });
86+
87+
expect(screen.getByTestId('reference')).toHaveAttribute('aria-describedby');
88+
});
89+
});
90+
91+
describe('menu', () => {
92+
it('applies the `menu` role to the floating element', () => {
93+
render(App, { role: 'menu', open: true });
94+
95+
expect(screen.getByTestId('floating')).toHaveAttribute('role', 'menu');
96+
});
97+
98+
it('applies the `aira-haspopup` attribute to the reference element', () => {
99+
render(App, { role: 'menu' });
100+
101+
expect(screen.getByTestId('reference')).toHaveAttribute('aria-haspopup', 'menu');
102+
});
103+
104+
it('applies the `aria-expanded` attribute to the reference element based on the `open` state', async () => {
105+
const { rerender } = render(App, { role: 'menu', open: false });
106+
107+
expect(screen.getByTestId('reference')).toHaveAttribute('aria-expanded', 'false');
108+
109+
await rerender({ open: true });
110+
111+
expect(screen.getByTestId('reference')).toHaveAttribute('aria-expanded', 'true');
112+
});
113+
114+
it('applies the `aria-controls` attribute with the correct id to the reference element based on the `open` state', async () => {
115+
const { rerender } = render(App, { role: 'menu', open: false });
116+
117+
expect(screen.getByTestId('reference')).not.toHaveAttribute('aria-controls');
118+
119+
await rerender({ open: true });
120+
121+
expect(screen.getByTestId('reference')).toHaveAttribute(
122+
'aria-controls',
123+
screen.getByTestId('floating').id,
124+
);
125+
});
126+
});
127+
128+
describe('listbox', () => {
129+
it('applies the `listbox` role to the floating element', () => {
130+
render(App, { role: 'listbox', open: true });
131+
132+
expect(screen.getByTestId('floating')).toHaveAttribute('role', 'listbox');
133+
});
134+
135+
it('applies the `aria-haspopup` attribute to the reference element', () => {
136+
render(App, { role: 'listbox' });
137+
138+
expect(screen.getByTestId('reference')).toHaveAttribute('aria-haspopup', 'listbox');
139+
});
140+
141+
it('applies the `aria-expanded` attribute to the reference element based on the `open` state', async () => {
142+
const { rerender } = render(App, { role: 'listbox', open: false });
143+
144+
expect(screen.getByTestId('reference')).toHaveAttribute('aria-expanded', 'false');
145+
146+
await rerender({ open: true });
147+
148+
expect(screen.getByTestId('reference')).toHaveAttribute('aria-expanded', 'true');
149+
});
150+
151+
it('applies the `aria-controls` attribute with the correct id to the reference element based on the `open` state', async () => {
152+
const { rerender } = render(App, { role: 'listbox', open: false });
153+
154+
expect(screen.getByTestId('reference')).not.toHaveAttribute('aria-controls');
155+
156+
await rerender({ open: true });
157+
158+
expect(screen.getByTestId('reference')).toHaveAttribute(
159+
'aria-controls',
160+
screen.getByTestId('floating').id,
161+
);
162+
});
163+
});
164+
165+
describe('select', () => {
166+
it('applies the `listbox` role to the floating element', () => {
167+
render(App, { role: 'select', open: true });
168+
169+
expect(screen.getByTestId('floating')).toHaveAttribute('role', 'listbox');
170+
});
171+
172+
it('applies the `aria-autocomplete` attribute to the reference element', () => {
173+
render(App, { role: 'select' });
174+
175+
expect(screen.getByTestId('reference')).toHaveAttribute('aria-autocomplete', 'none');
176+
});
177+
178+
it('applies the `aria-expanded` attribute to the reference element based on the `open` state', async () => {
179+
const { rerender } = render(App, { role: 'select', open: false });
180+
181+
expect(screen.getByTestId('reference')).toHaveAttribute('aria-expanded', 'false');
182+
183+
await rerender({ open: true });
184+
185+
expect(screen.getByTestId('reference')).toHaveAttribute('aria-expanded', 'true');
186+
});
187+
188+
it('applies the `aria-selected` attribute to the selected item within the floating element', async () => {
189+
render(App, { role: 'select', open: true });
190+
191+
expect(screen.getByTestId('item-1')).toHaveAttribute('aria-selected', 'false');
192+
expect(screen.getByTestId('item-2')).toHaveAttribute('aria-selected', 'true');
193+
});
194+
});
195+
196+
describe('combobox', () => {
197+
it('applies the `listbox` role to the floating element', () => {
198+
render(App, { role: 'combobox', open: true });
199+
200+
expect(screen.getByTestId('floating')).toHaveAttribute('role', 'listbox');
201+
});
202+
203+
it('applies the `aria-autocomplete` attribute to the reference element', () => {
204+
render(App, { role: 'combobox' });
205+
206+
expect(screen.getByTestId('reference')).toHaveAttribute('aria-autocomplete', 'list');
207+
});
208+
209+
it('applies the `aria-expanded` attribute to the reference element based on the `open` state', async () => {
210+
const { rerender } = render(App, { role: 'combobox', open: false });
211+
44212
expect(screen.getByTestId('reference')).toHaveAttribute('aria-expanded', 'false');
45213

46-
await rerender({ role: 'dialog', open: true });
214+
await rerender({ open: true });
47215

48216
expect(screen.getByTestId('reference')).toHaveAttribute('aria-expanded', 'true');
49217
});

0 commit comments

Comments
 (0)