Skip to content

Commit c411012

Browse files
ECHOES-1008 Make keyboard navigation work for group label links
1 parent 3fd9400 commit c411012

File tree

3 files changed

+61
-30
lines changed

3 files changed

+61
-30
lines changed

src/components/dropdown-menu/DropdownMenuGroupLabel.tsx

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,54 +20,85 @@
2020

2121
import styled from '@emotion/styled';
2222
import * as radixDropdownMenu from '@radix-ui/react-dropdown-menu';
23-
import { forwardRef, ReactNode } from 'react';
23+
import { forwardRef } from 'react';
24+
import { LinkStandalone } from '../links';
25+
import { LinkBaseProps } from '../links/LinkTypes';
2426

2527
import { cssVar } from '~utils/design-tokens';
2628

2729
export interface DropdownMenuGroupLabelProps extends radixDropdownMenu.DropdownMenuLabelProps {
2830
/**
29-
* Optional content to display at the end of the group label element.
31+
* Optional link to display at the end of the group label element.
3032
*/
31-
suffix?: ReactNode;
33+
link?: {
34+
linkElement: LinkBaseProps['children'];
35+
to: LinkBaseProps['to'];
36+
};
3237
}
3338

3439
export const DropdownMenuGroupLabel = forwardRef<HTMLDivElement, DropdownMenuGroupLabelProps>(
3540
(props, ref) => {
36-
const { children, suffix, ...rest } = props;
41+
const { children, link, ...rest } = props;
3742

3843
return (
39-
<StyledDropdownMenuGroupLabel ref={ref} {...rest}>
40-
{/* Fragment wrapper to ensure single child structure, needed for compatibility */}
41-
<>
44+
<StyledWrapper>
45+
<StyledDropdownMenuGroupLabel ref={ref} {...rest}>
4246
{children}
47+
</StyledDropdownMenuGroupLabel>
4348

44-
{suffix && <StyledSuffix>{suffix}</StyledSuffix>}
45-
</>
46-
</StyledDropdownMenuGroupLabel>
49+
{link && (
50+
<StyledMenuItem asChild>
51+
<StyledLink to={link.to}>{link.linkElement}</StyledLink>
52+
</StyledMenuItem>
53+
)}
54+
</StyledWrapper>
4755
);
4856
},
4957
);
5058

5159
DropdownMenuGroupLabel.displayName = 'DropdownMenuGroupLabel';
5260

53-
const StyledDropdownMenuGroupLabel = styled(radixDropdownMenu.Label)`
61+
const StyledWrapper = styled.div`
5462
align-items: center;
55-
color: ${cssVar('color-text-default')};
5663
display: flex;
57-
font: ${cssVar('typography-text-small-semi-bold')};
5864
justify-content: space-between;
65+
`;
66+
67+
const StyledDropdownMenuGroupLabel = styled(radixDropdownMenu.Label)`
68+
color: ${cssVar('color-text-default')};
69+
font: ${cssVar('typography-text-small-semi-bold')};
5970
6071
padding: ${cssVar('dimension-space-100')} ${cssVar('dimension-space-150')}
6172
${cssVar('dimension-space-50')};
6273
`;
6374

6475
StyledDropdownMenuGroupLabel.displayName = 'StyledDropdownMenuGroupLabel';
6576

66-
const StyledSuffix = styled.span`
67-
align-items: center;
68-
display: flex;
69-
flex: 0 0 auto;
70-
justify-content: flex-end;
77+
const StyledMenuItem = styled(radixDropdownMenu.Item)`
78+
/* With asChild, these styles apply directly to the <a> tag */
79+
background: none;
80+
padding: ${cssVar('dimension-space-50')} ${cssVar('dimension-space-100')};
81+
82+
&:hover,
83+
&[data-highlighted] {
84+
background: none;
85+
}
7186
`;
7287

73-
StyledSuffix.displayName = 'StyledSuffix';
88+
const StyledLink = styled(LinkStandalone)`
89+
/* Match the label typography (same as StyledDropdownMenuGroupLabel) */
90+
font: ${cssVar('typography-text-small-semi-bold')};
91+
margin-right: ${cssVar('dimension-space-100')};
92+
93+
/* Show focus ring only for keyboard navigation - && needed for extra specificity */
94+
&&:focus-visible {
95+
border-radius: ${cssVar('border-radius-200')};
96+
outline: ${cssVar('color-focus-default')} solid ${cssVar('focus-border-width-default')};
97+
outline-offset: ${cssVar('focus-border-offset-default')};
98+
}
99+
100+
/* Hide focus ring on mouse hover - && needed for extra specificity */
101+
&&:hover {
102+
outline: none;
103+
}
104+
`;

src/components/dropdown-menu/__tests__/DropdownMenu-test.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import { ThemeProvider } from '~utils/theme';
2525
import { DropdownMenu, DropdownMenuAlign } from '..';
2626
import { Button } from '../../buttons';
2727
import { IconBell, IconCalendar } from '../../icons';
28-
import { LinkStandalone } from '../../links';
2928

3029
const items = <DropdownMenu.ItemButton>An item</DropdownMenu.ItemButton>;
3130
const trigger = <Button>Trigger</Button>;
@@ -323,14 +322,13 @@ it('should render GroupLabel without suffix', () => {
323322
expect(screen.getByText('Item in group')).toBeVisible();
324323
});
325324

326-
it('should render GroupLabel with suffix', () => {
325+
it('should render GroupLabel with link', () => {
327326
renderWithMemoryRouter(
328327
<DropdownMenu
329328
isOpen
330329
items={
331330
<>
332-
<DropdownMenu.GroupLabel
333-
suffix={<LinkStandalone to="/view-all">View all</LinkStandalone>}>
331+
<DropdownMenu.GroupLabel link={{ linkElement: 'View all', to: '/view-all' }}>
334332
My Group
335333
</DropdownMenu.GroupLabel>
336334
<DropdownMenu.ItemButton>Item in group</DropdownMenu.ItemButton>
@@ -341,7 +339,11 @@ it('should render GroupLabel with suffix', () => {
341339
);
342340

343341
expect(screen.getByText('My Group')).toBeVisible();
344-
expect(screen.getByRole('link', { name: 'View all' })).toBeVisible();
342+
343+
const link = screen.getByRole('menuitem', { name: 'View all' });
344+
345+
expect(link).toBeVisible();
346+
expect(link).toHaveAttribute('href', '/view-all');
345347
});
346348

347349
it('should forward ref to GroupLabel', () => {

stories/dropdown-menu/DropdownMenu-stories.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -160,30 +160,28 @@ export const MenuWithADisabledButton: Story = {
160160
),
161161
};
162162

163-
export const MenuWithGroupLabelSuffix: Story = {
163+
export const MenuWithGroupLabelAction: Story = {
164164
render: () => (
165165
<BasicWrapper>
166166
<DropdownMenu
167167
items={
168168
<>
169-
<DropdownMenu.GroupLabel
170-
suffix={<LinkStandalone to="/enterprises">View all</LinkStandalone>}>
169+
<DropdownMenu.GroupLabel link={{ linkElement: 'View all', to: '/enterprises' }}>
171170
Enterprises
172171
</DropdownMenu.GroupLabel>
173172

174173
{enterpriseItems}
175174

176175
<DropdownMenu.Separator />
177176

178-
<DropdownMenu.GroupLabel
179-
suffix={<LinkStandalone to="/theme-settings">Configure</LinkStandalone>}>
177+
<DropdownMenu.GroupLabel link={{ linkElement: 'Configure', to: '/theme-settings' }}>
180178
Theme
181179
</DropdownMenu.GroupLabel>
182180

183181
{themeItems}
184182
</>
185183
}>
186-
<MenuButton>Menu with suffix links</MenuButton>
184+
<MenuButton>Menu with links</MenuButton>
187185
</DropdownMenu>
188186
</BasicWrapper>
189187
),

0 commit comments

Comments
 (0)