Skip to content

Commit 15d4e5c

Browse files
authored
feat(container-menu)!: Add getAnchorProps for explicit anchor handling (#695)
1 parent a79dac8 commit 15d4e5c

File tree

8 files changed

+417
-82
lines changed

8 files changed

+417
-82
lines changed

packages/menu/README.md

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ Check out [storybook](https://zendeskgarden.github.io/react-containers) for live
1818

1919
### useMenu
2020

21+
#### Menu items
22+
2123
```jsx
2224
import { useMenu } from '@zendeskgarden/container-menu';
2325

@@ -50,6 +52,40 @@ const Menu = () => {
5052
};
5153
```
5254

55+
#### Menu links
56+
57+
```jsx
58+
import { useMenu } from '@zendeskgarden/container-menu';
59+
60+
const Menu = () => {
61+
const triggerRef = useRef();
62+
const menuRef = useRef();
63+
const items = [
64+
{ value: 'home', label: 'Home', href="#", selected: true },
65+
{ value: 'about', label: 'About', href="www.example.com/about" },
66+
{ value: 'support', label: 'Support', href="www.support.example.com", external: true }
67+
];
68+
const { isExpanded, getTriggerProps, getMenuProps, getItemProps, getAnchorProps } = useMenu({
69+
triggerRef,
70+
menuRef,
71+
items
72+
});
73+
74+
return (
75+
<>
76+
<button {...getTriggerProps()}>Menu</button>
77+
<ul {...getMenuProps()} style={{ visibility: isExpanded ? 'visible' : 'hidden' }}>
78+
{items.map(item => (
79+
<li key={item.value} {...getItemProps({ item })}>
80+
<a {...getAnchorProps({ item })}>{item.label}</a>
81+
</li>
82+
))}
83+
</ul>
84+
</>
85+
);
86+
};
87+
```
88+
5389
### MenuContainer
5490

5591
```jsx
@@ -61,27 +97,20 @@ const Menu = () => {
6197
const items = [
6298
{ value: 'value-1', label: 'One' },
6399
{ value: 'value-2', label: 'Two' },
64-
{ value: 'value-3', label: 'Three', href: '#0' },
65-
{ value: 'value-4', label: 'Four' }
100+
{ value: 'value-3', label: 'Three' }
66101
];
67102

68103
return (
69104
<MenuContainer triggerRef={triggerRef} menuRef={menuRef} items={items}>
70-
{({ isExpanded, getTriggerProps, getMenuProps, getItemProps, getSeparatorProps }) => (
105+
{({ isExpanded, getTriggerProps, getMenuProps, getItemProps }) => (
71106
<>
72107
<button {...getTriggerProps()}>Menu</button>
73108
<ul {...getMenuProps()} style={{ visibility: isExpanded ? 'visible' : 'hidden' }}>
74-
{items.map(item =>
75-
item.href ? (
76-
<li key={item.value} role="none">
77-
<a {...getItemProps({ item })}>{item.label}</a>
78-
</li>
79-
) : (
80-
<li key={item.value} {...getItemProps({ item })}>
81-
{item.label}
82-
</li>
83-
)
84-
)}
109+
{items.map(item => (
110+
<li key={item.value} {...getItemProps({ item })}>
111+
{item.label}
112+
</li>
113+
))}
85114
</ul>
86115
</>
87116
)}

packages/menu/demo/menu.stories.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ export const Controlled: Story = {
4848
return (
4949
<MenuStory
5050
{...args}
51+
// Storybook sets args to null when they are removed from controls. This breaks useMenu since getControlledValue treats null as intentional input.
52+
selectedItems={args.selectedItems === null ? undefined : args.selectedItems}
5153
// eslint-disable-next-line @typescript-eslint/no-unused-vars
5254
onChange={({ type, ...rest }) => {
5355
updateArgs(rest);

packages/menu/demo/stories/MenuStory.tsx

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* found at http://www.apache.org/licenses/LICENSE-2.0.
66
*/
77

8-
import React, { AnchorHTMLAttributes, LiHTMLAttributes, useRef } from 'react';
8+
import React, { useEffect, useRef } from 'react';
99
import { StoryFn } from '@storybook/react';
1010
import classNames from 'classnames';
1111
import {
@@ -29,18 +29,20 @@ interface IUseMenuComponentProps extends MenuReturnValue {
2929
type MenuItemProps = {
3030
item: IMenuItemBase;
3131
getItemProps: IUseMenuComponentProps['getItemProps'];
32+
getAnchorProps: IUseMenuComponentProps['getAnchorProps'];
3233
focusedValue: IUseMenuComponentProps['focusedValue'];
3334
isSelected?: boolean;
3435
};
3536

36-
const Item = ({ item, getItemProps, focusedValue, isSelected }: MenuItemProps) => {
37-
const itemProps = getItemProps({ item });
37+
const Item = ({ item, getAnchorProps, getItemProps, focusedValue, isSelected }: MenuItemProps) => {
38+
const itemProps = getItemProps<HTMLLIElement>({ item });
39+
const anchorProps = getAnchorProps({ item });
3840

3941
const itemChildren = (
4042
<>
4143
<span className="inline-flex justify-center items-center w-4">
42-
{item?.type === 'radio' && !!isSelected && '•'}
43-
{item?.type === 'checkbox' && !!isSelected && '✓'}
44+
{!!isSelected && item.type === 'radio' && '•'}
45+
{!!isSelected && (item.type === 'checkbox' || !!item.href) && '✓'}
4446
</span>
4547
{item.label || item.value}
4648
</>
@@ -54,16 +56,21 @@ const Item = ({ item, getItemProps, focusedValue, isSelected }: MenuItemProps) =
5456
'cursor-pointer': !item.disabled,
5557
'cursor-default': item.disabled
5658
})}
57-
role={itemProps.href ? 'none' : undefined}
58-
{...(!itemProps.href && (itemProps as LiHTMLAttributes<HTMLLIElement>))}
59+
{...itemProps}
5960
>
60-
{itemProps.href ? (
61+
{anchorProps ? (
6162
<a
62-
{...(itemProps as AnchorHTMLAttributes<HTMLAnchorElement>)}
63-
className="w-full rounded-sm outline-offset-0 transition-none border-width-none"
63+
{...anchorProps}
64+
className={classNames(
65+
' w-full rounded-sm outline-offset-0 transition-none border-width-none',
66+
{
67+
'text-grey-400': item.disabled,
68+
'cursor-default': item.disabled
69+
}
70+
)}
6471
>
6572
{itemChildren}
66-
{!!item.isExternal && (
73+
{anchorProps.target === '_blank' && (
6774
<>
6875
<span aria-hidden="true"></span>
6976
<span className="sr-only">(opens in new window)</span>
@@ -85,11 +92,20 @@ const Component = ({
8592
getTriggerProps,
8693
getMenuProps,
8794
getItemProps,
95+
getAnchorProps,
8896
getItemGroupProps,
8997
getSeparatorProps
9098
}: MenuReturnValue & UseMenuProps) => {
9199
const selectedValues = selection.map(item => item.value);
92100

101+
useEffect(() => {
102+
const originalWindowOpen = window.open;
103+
window.open = () => null;
104+
return () => {
105+
window.open = originalWindowOpen;
106+
};
107+
}, []);
108+
93109
return (
94110
<div className="relative">
95111
<button className="px-2 py-1" type="button" {...getTriggerProps()}>
@@ -116,6 +132,7 @@ const Component = ({
116132
key={groupItem.value}
117133
item={{ ...groupItem }}
118134
getItemProps={getItemProps}
135+
getAnchorProps={getAnchorProps}
119136
focusedValue={focusedValue}
120137
isSelected={selectedValues.includes(groupItem.value)}
121138
/>
@@ -141,6 +158,8 @@ const Component = ({
141158
item={item}
142159
focusedValue={focusedValue}
143160
getItemProps={getItemProps}
161+
getAnchorProps={getAnchorProps}
162+
isSelected={selectedValues.includes(item.value)}
144163
/>
145164
);
146165
})}

packages/menu/demo/stories/data.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,16 @@ export const ITEMS: MenuItem[] = [
1616
value: 'plant-04',
1717
label: 'Aloe Vera',
1818
href: 'https://en.wikipedia.org/wiki/Aloe_vera',
19-
isExternal: false
19+
external: false
2020
},
21-
{ value: 'plant-05', label: 'Succulent' },
21+
{
22+
value: 'plant-05',
23+
label: 'Palm tree',
24+
href: 'https://en.wikipedia.org/wiki/Palm_tree',
25+
external: true,
26+
disabled: true
27+
},
28+
{ value: 'plant-06', label: 'Succulent' },
2229
{
2330
label: 'Choose favorites',
2431
items: [

0 commit comments

Comments
 (0)