Skip to content

Commit 56e5179

Browse files
committed
refactor: return previous selection state on link item click
1 parent 0a234c9 commit 56e5179

File tree

2 files changed

+80
-27
lines changed

2 files changed

+80
-27
lines changed

packages/menu/src/MenuContainer.spec.tsx

Lines changed: 75 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -284,31 +284,36 @@ describe('MenuContainer', () => {
284284
});
285285

286286
describe('navigational menu items (links)', () => {
287-
it('applies external anchor attributes, only if not disabled', () => {
287+
it('applies href and external anchor attributes, only when not disabled', () => {
288288
const { getByTestId } = render(
289289
<TestMenu
290290
items={[
291-
{ value: 'link-1', href: '#1', isExternal: true },
292-
{ value: 'link-2', href: '#2', isExternal: true, disabled: true }
291+
{ value: 'link-1', href: '#1', external: true },
292+
{ value: 'link-2', href: '#2', external: true, disabled: true }
293293
]}
294294
/>
295295
);
296296
const menu = getByTestId('menu');
297297

298+
expect(menu.firstChild).toHaveAttribute('href', '#1');
298299
expect(menu.firstChild).toHaveAttribute('target', '_blank');
299300
expect(menu.firstChild).toHaveAttribute('rel', 'noopener noreferrer');
300301

302+
expect(menu.childNodes[1]).not.toHaveAttribute('href');
301303
expect(menu.childNodes[1]).not.toHaveAttribute('target', '_blank');
302304
expect(menu.childNodes[1]).not.toHaveAttribute('rel', 'noopener noreferrer');
303305
});
304306

305-
it('applies the correct aria-current attribute to active link', async () => {
307+
it('tracks click events on links and alls the onChange function with the correct parameters', async () => {
308+
const onChangeSpy = jest.fn();
309+
306310
const { getByTestId, getByText } = render(
307311
<TestMenu
308312
items={[
309313
{ value: 'link-1', href: '#1' },
310314
{ value: 'link-2', href: '#2' }
311315
]}
316+
onChange={onChangeSpy}
312317
/>
313318
);
314319
const trigger = getByTestId('trigger');
@@ -319,7 +324,51 @@ describe('MenuContainer', () => {
319324
await user.click(link);
320325
});
321326

322-
expect(link).toHaveAttribute('aria-current', 'page');
327+
expect(onChangeSpy.mock.calls[2][0]).toStrictEqual({
328+
type: 'menuItem:click',
329+
value: 'link-2',
330+
isExpanded: false,
331+
selectedItems: []
332+
});
333+
334+
expect(link).not.toHaveAttribute('aria-current', 'page');
335+
});
336+
337+
it('supports setting the selected link via item.selected and ignore link selection changes', async () => {
338+
const onChangeSpy = jest.fn();
339+
const { getByTestId, getByText } = render(
340+
<TestMenu
341+
items={[
342+
{ value: 'link-1', href: '#1' },
343+
{ value: 'link-2', href: '#2', selected: true }
344+
]}
345+
onChange={onChangeSpy}
346+
/>
347+
);
348+
const trigger = getByTestId('trigger');
349+
const secondLink = getByText('link-2');
350+
351+
await waitFor(async () => {
352+
await user.click(trigger);
353+
});
354+
355+
expect(secondLink).toHaveAttribute('aria-current', 'page');
356+
357+
const firstLink = getByText('link-1');
358+
359+
await waitFor(async () => {
360+
await user.click(trigger);
361+
await user.click(firstLink);
362+
});
363+
364+
expect(onChangeSpy.mock.calls[3][0]).toStrictEqual({
365+
type: 'menuItem:click',
366+
value: 'link-1',
367+
isExpanded: false,
368+
selectedItems: [{ href: '#2', selected: true, value: 'link-2' }]
369+
});
370+
371+
expect(firstLink).not.toHaveAttribute('aria-current');
323372
});
324373
});
325374

@@ -1297,7 +1346,7 @@ describe('MenuContainer', () => {
12971346
});
12981347

12991348
describe('navigational menu items (links)', () => {
1300-
it('applies the correct aria-current attribute to selected link', async () => {
1349+
it('applies the correct aria-current attribute to user-selected link', async () => {
13011350
const { getByTestId, getByText } = render(
13021351
<TestMenu
13031352
items={[
@@ -1317,7 +1366,7 @@ describe('MenuContainer', () => {
13171366
expect(link).toHaveAttribute('aria-current', 'page');
13181367
});
13191368

1320-
it('prevents default when clicking a selected link', async () => {
1369+
it('prevents default only when clicking on user-selected link', async () => {
13211370
const { getByTestId, getByText } = render(
13221371
<TestMenu
13231372
items={[
@@ -1328,18 +1377,31 @@ describe('MenuContainer', () => {
13281377
/>
13291378
);
13301379
const trigger = getByTestId('trigger');
1331-
const link = getByText('link-2');
1380+
const firstLink = getByText('link-1');
1381+
const secondLink = getByText('link-2');
13321382

13331383
await waitFor(async () => {
13341384
await user.click(trigger);
1385+
await user.click(firstLink);
13351386
});
13361387

1337-
const event = createEvent.click(link);
1338-
event.preventDefault = jest.fn();
1339-
fireEvent(link, event);
1388+
expect(firstLink).not.toHaveAttribute('aria-current');
13401389

1341-
expect(event.preventDefault).toHaveBeenCalledTimes(1);
1342-
expect(link).toHaveAttribute('aria-current', 'page');
1390+
const firstLinkClickEvent = createEvent.click(firstLink);
1391+
firstLinkClickEvent.preventDefault = jest.fn();
1392+
fireEvent(firstLink, firstLinkClickEvent);
1393+
1394+
// fire click event one more time to test behavior on previously clicked anchor
1395+
fireEvent(firstLink, firstLinkClickEvent);
1396+
1397+
expect(firstLinkClickEvent.preventDefault).toHaveBeenCalledTimes(0);
1398+
1399+
const secondLinkClickEvent = createEvent.click(secondLink);
1400+
secondLinkClickEvent.preventDefault = jest.fn();
1401+
fireEvent(secondLink, secondLinkClickEvent);
1402+
1403+
expect(secondLinkClickEvent.preventDefault).toHaveBeenCalledTimes(1);
1404+
expect(secondLink).toHaveAttribute('aria-current', 'page');
13431405
});
13441406
});
13451407
});

packages/menu/src/useMenu.ts

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -163,24 +163,15 @@ export const useMenu = <T extends HTMLElement = HTMLElement, M extends HTMLEleme
163163

164164
if (type === 'checkbox') {
165165
isSelected = !!controlledSelectedItems.find(item => item.value === value);
166-
} else if (type === 'radio') {
166+
} else if (type === 'radio' || href) {
167167
const match = controlledSelectedItems.filter(item => item.name === name)[0];
168168

169169
isSelected = match?.value === value;
170-
} else if (href) {
171-
const selection =
172-
Array.isArray(selectedItems) && selectedItems.length
173-
? selectedItems
174-
: initialSelectedItems;
175-
176-
const current = selection.filter(item => item.name === name)[0];
177-
178-
isSelected = current?.value === value;
179170
}
180171

181172
return isSelected;
182173
},
183-
[controlledSelectedItems, initialSelectedItems, selectedItems]
174+
[controlledSelectedItems]
184175
);
185176

186177
const getNextFocusedValue = useCallback(
@@ -231,9 +222,10 @@ export const useMenu = <T extends HTMLElement = HTMLElement, M extends HTMLEleme
231222

232223
const getSelectedItems = useCallback(
233224
({ value, type, name, label, selected, href }: IMenuItemBase) => {
234-
let changes: ISelectedItem[] | null = [...controlledSelectedItems];
225+
if (href) return controlledSelectedItems;
226+
if (!type) return null;
235227

236-
if (!type || href) return null;
228+
let changes: ISelectedItem[] | null = [...controlledSelectedItems];
237229

238230
const selectedItem = {
239231
value,
@@ -480,7 +472,6 @@ export const useMenu = <T extends HTMLElement = HTMLElement, M extends HTMLEleme
480472
});
481473
},
482474
[
483-
selectedItems,
484475
getSelectedItems,
485476
state.nestedPathIds,
486477
isExpandedControlled,

0 commit comments

Comments
 (0)