Skip to content

Commit feca983

Browse files
authored
feat(Toolbar,OverflowMenu): support responsive height vis breakpoints (#12347)
* feat(Toolbar,OverflowMenu): support responsive height vis breakpoints * fix some examples * update import to relative * clean up various examples, add warn for sm OverflowMenu vertical breakpoint * clean up few docs nits * add tests for Toolbar visibilityAtHeight, OverflowMenu isVertical
1 parent cb3856a commit feca983

13 files changed

Lines changed: 407 additions & 12 deletions

packages/react-core/src/components/OverflowMenu/OverflowMenu.tsx

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,21 @@ import styles from '@patternfly/react-styles/css/components/OverflowMenu/overflo
33
import { css } from '@patternfly/react-styles';
44
import { OverflowMenuContext } from './OverflowMenuContext';
55
import { debounce } from '../../helpers/util';
6-
import { globalWidthBreakpoints } from '../../helpers/constants';
6+
import { globalWidthBreakpoints, globalHeightBreakpoints } from '../../helpers/constants';
77
import { getResizeObserver } from '../../helpers/resizeObserver';
8+
import { PickOptional } from '../../helpers/typeUtils';
89

910
export interface OverflowMenuProps extends React.HTMLProps<HTMLDivElement> {
1011
/** Any elements that can be rendered in the menu */
1112
children?: any;
1213
/** Additional classes added to the OverflowMenu. */
1314
className?: string;
14-
/** Indicates breakpoint at which to switch between horizontal menu and vertical dropdown */
15+
/** Indicates breakpoint at which to switch between expanded and collapsed states. The "sm" breakpoint does not apply to vertical overflow menus. */
1516
breakpoint: 'sm' | 'md' | 'lg' | 'xl' | '2xl';
1617
/** A container reference to base the specified breakpoint on instead of the viewport width. */
1718
breakpointReference?: HTMLElement | (() => HTMLElement) | React.RefObject<any>;
19+
/** Indicates the overflow menu orientation is vertical and should respond to height changes instead of width. */
20+
isVertical?: boolean;
1821
}
1922

2023
export interface OverflowMenuState extends React.HTMLProps<HTMLDivElement> {
@@ -24,6 +27,11 @@ export interface OverflowMenuState extends React.HTMLProps<HTMLDivElement> {
2427

2528
class OverflowMenu extends Component<OverflowMenuProps, OverflowMenuState> {
2629
static displayName = 'OverflowMenu';
30+
31+
static defaultProps: PickOptional<OverflowMenuProps> = {
32+
isVertical: false
33+
};
34+
2735
constructor(props: OverflowMenuProps) {
2836
super(props);
2937
this.state = {
@@ -69,6 +77,15 @@ class OverflowMenu extends Component<OverflowMenuProps, OverflowMenuState> {
6977
}
7078

7179
handleResize = () => {
80+
const { isVertical } = this.props;
81+
if (isVertical) {
82+
this.handleResizeHeight();
83+
} else {
84+
this.handleResizeWidth();
85+
}
86+
};
87+
88+
handleResizeWidth = () => {
7289
const breakpointWidth = globalWidthBreakpoints[this.props.breakpoint];
7390
if (!breakpointWidth) {
7491
// eslint-disable-next-line no-console
@@ -83,14 +100,35 @@ class OverflowMenu extends Component<OverflowMenuProps, OverflowMenuState> {
83100
}
84101
};
85102

103+
handleResizeHeight = () => {
104+
const breakpointHeight = globalHeightBreakpoints[this.props.breakpoint];
105+
if (breakpointHeight === 0) {
106+
// eslint-disable-next-line no-console
107+
console.warn('The "sm" breakpoint does not apply to vertical overflow menus.');
108+
return;
109+
}
110+
111+
if (!breakpointHeight) {
112+
// eslint-disable-next-line no-console
113+
console.error('OverflowMenu will not be visible without a valid breakpoint.');
114+
return;
115+
}
116+
117+
const relativeHeight = this.state.breakpointRef ? this.state.breakpointRef.clientHeight : window.innerHeight;
118+
const isBelowBreakpoint = relativeHeight < breakpointHeight;
119+
if (this.state.isBelowBreakpoint !== isBelowBreakpoint) {
120+
this.setState({ isBelowBreakpoint });
121+
}
122+
};
123+
86124
handleResizeWithDelay = debounce(this.handleResize, 250);
87125

88126
render() {
89127
// eslint-disable-next-line @typescript-eslint/no-unused-vars
90-
const { className, breakpoint, children, breakpointReference, ...props } = this.props;
128+
const { className, breakpoint, children, breakpointReference, isVertical, ...props } = this.props;
91129

92130
return (
93-
<div {...props} className={css(styles.overflowMenu, className)}>
131+
<div {...props} className={css(styles.overflowMenu, isVertical && styles.modifiers.vertical, className)}>
94132
<OverflowMenuContext.Provider value={{ isBelowBreakpoint: this.state.isBelowBreakpoint }}>
95133
{children}
96134
</OverflowMenuContext.Provider>

packages/react-core/src/components/OverflowMenu/__tests__/OverflowMenu.test.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,22 @@ describe('OverflowMenu', () => {
7979

8080
expect(resizeObserver).toHaveBeenCalledWith(containerRef.current, expect.any(Function));
8181
});
82+
83+
test(`applies ${styles.modifiers.vertical} when isVertical is passed`, () => {
84+
render(<OverflowMenu breakpoint="md" isVertical data-testid="test-id" />);
85+
expect(screen.getByTestId('test-id')).toHaveClass(styles.modifiers.vertical);
86+
});
87+
88+
test('warns when using "sm" breakpoint and isVertical is passed', () => {
89+
const warnMock = jest.fn() as any;
90+
const originalConsole = global.console;
91+
global.console = { ...originalConsole, warn: warnMock } as any;
92+
93+
try {
94+
render(<OverflowMenu breakpoint="sm" isVertical />);
95+
expect(warnMock).toHaveBeenCalledWith('The "sm" breakpoint does not apply to vertical overflow menus.');
96+
} finally {
97+
global.console = originalConsole;
98+
}
99+
});
82100
});

packages/react-core/src/components/OverflowMenu/examples/OverflowMenu.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-ico
2727

2828
```
2929

30+
### Vertical
31+
32+
Passing `isVertical` to `OverflowMenu` will change its behavior to respond to breakpoints based on window height instead of width.
33+
34+
```ts file="./OverflowMenuSimpleVertical.tsx"
35+
36+
```
37+
3038
### Group types
3139

3240
```ts file="./OverflowMenuGroupTypes.tsx"
@@ -45,7 +53,7 @@ import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-ico
4553

4654
```
4755

48-
### Breakpoint on container
56+
### Breakpoint on container width
4957

5058
By passing in the `breakpointReference` property, the overflow menu's breakpoint will be relative to the width of the reference container rather than the viewport width.
5159

@@ -54,3 +62,11 @@ You can change the container width in this example by adjusting the slider. As t
5462
```ts file="./OverflowMenuBreakpointOnContainer.tsx"
5563

5664
```
65+
66+
### Breakpoint on container height
67+
68+
By passing in the `breakpointReference` and `isVertical` properties, the overflow menu's breakpoint will be determined relative to the height of the reference container rather than the window height.
69+
70+
```ts isFullscreen file="./OverflowMenuBreakpointOnContainerHeight.tsx"
71+
72+
```
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { useRef, useState } from 'react';
2+
import {
3+
OverflowMenu,
4+
OverflowMenuControl,
5+
OverflowMenuContent,
6+
OverflowMenuGroup,
7+
OverflowMenuItem,
8+
OverflowMenuDropdownItem,
9+
MenuToggle,
10+
Slider,
11+
SliderOnChangeEvent,
12+
Dropdown,
13+
DropdownList
14+
} from '@patternfly/react-core';
15+
import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon';
16+
17+
export const OverflowMenuBreakpointOnContainerHeight: React.FunctionComponent = () => {
18+
const [isOpen, setIsOpen] = useState(false);
19+
const [containerHeight, setContainerHeight] = useState(100);
20+
const containerRef = useRef<HTMLDivElement>(null);
21+
22+
const onToggle = () => {
23+
setIsOpen(!isOpen);
24+
};
25+
26+
const onSelect = () => {
27+
setIsOpen(!isOpen);
28+
};
29+
30+
const onChange = (_event: SliderOnChangeEvent, value: number) => {
31+
setContainerHeight(value);
32+
};
33+
34+
const containerStyles = {
35+
height: `${containerHeight}%`,
36+
padding: '1rem',
37+
borderWidth: '2px',
38+
borderStyle: 'dashed'
39+
};
40+
41+
const dropdownItems = [
42+
<OverflowMenuDropdownItem itemId={0} key="item1" isShared>
43+
Item 1
44+
</OverflowMenuDropdownItem>,
45+
<OverflowMenuDropdownItem itemId={1} key="item2" isShared>
46+
Item 2
47+
</OverflowMenuDropdownItem>,
48+
<OverflowMenuDropdownItem itemId={2} key="item3" isShared>
49+
Item 3
50+
</OverflowMenuDropdownItem>,
51+
<OverflowMenuDropdownItem itemId={3} key="item4" isShared>
52+
Item 4
53+
</OverflowMenuDropdownItem>,
54+
<OverflowMenuDropdownItem itemId={4} key="item5" isShared>
55+
Item 5
56+
</OverflowMenuDropdownItem>
57+
];
58+
59+
return (
60+
<>
61+
<span id="overflowMenu-hasBreakpointOnContainer-height-slider-label">Current container height</span>:{' '}
62+
{containerHeight}%
63+
<Slider
64+
value={containerHeight}
65+
onChange={onChange}
66+
max={100}
67+
min={20}
68+
step={20}
69+
showTicks
70+
showBoundaries={false}
71+
aria-labelledby="overflowMenu-hasBreakpointOnContainer-height-slider-label"
72+
/>
73+
<div style={{ height: '100%' }}>
74+
<div ref={containerRef} id="height-breakpoint-reference-container" style={containerStyles}>
75+
<OverflowMenu breakpointReference={containerRef} breakpoint="md" isVertical>
76+
<OverflowMenuContent>
77+
<OverflowMenuItem>Item 1</OverflowMenuItem>
78+
<OverflowMenuItem>Item 2</OverflowMenuItem>
79+
<OverflowMenuGroup>
80+
<OverflowMenuItem>Item 3</OverflowMenuItem>
81+
<OverflowMenuItem>Item 4</OverflowMenuItem>
82+
<OverflowMenuItem>Item 5</OverflowMenuItem>
83+
</OverflowMenuGroup>
84+
</OverflowMenuContent>
85+
<OverflowMenuControl>
86+
<Dropdown
87+
onSelect={onSelect}
88+
toggle={(toggleRef) => (
89+
<MenuToggle
90+
ref={toggleRef}
91+
aria-label="Height breakpoint on container example overflow menu"
92+
variant="plain"
93+
onClick={onToggle}
94+
isExpanded={isOpen}
95+
icon={<EllipsisVIcon />}
96+
/>
97+
)}
98+
isOpen={isOpen}
99+
onOpenChange={(isOpen) => setIsOpen(isOpen)}
100+
>
101+
<DropdownList>{dropdownItems}</DropdownList>
102+
</Dropdown>
103+
</OverflowMenuControl>
104+
</OverflowMenu>
105+
</div>
106+
</div>
107+
</>
108+
);
109+
};
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { useState } from 'react';
2+
import {
3+
OverflowMenu,
4+
OverflowMenuControl,
5+
OverflowMenuContent,
6+
OverflowMenuGroup,
7+
OverflowMenuItem,
8+
OverflowMenuDropdownItem,
9+
MenuToggle,
10+
Dropdown,
11+
DropdownList
12+
} from '@patternfly/react-core';
13+
import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon';
14+
15+
export const OverflowMenuSimpleVertical: React.FunctionComponent = () => {
16+
const [isOpen, setIsOpen] = useState(false);
17+
18+
const onToggle = () => {
19+
setIsOpen(!isOpen);
20+
};
21+
22+
const onSelect = () => {
23+
setIsOpen(!isOpen);
24+
};
25+
26+
const dropdownItems = [
27+
<OverflowMenuDropdownItem itemId={0} key="item1" isShared>
28+
Item 1
29+
</OverflowMenuDropdownItem>,
30+
<OverflowMenuDropdownItem itemId={1} key="item2" isShared>
31+
Item 2
32+
</OverflowMenuDropdownItem>,
33+
<OverflowMenuDropdownItem itemId={2} key="item3" isShared>
34+
Item 3
35+
</OverflowMenuDropdownItem>,
36+
<OverflowMenuDropdownItem itemId={3} key="item4" isShared>
37+
Item 4
38+
</OverflowMenuDropdownItem>,
39+
<OverflowMenuDropdownItem itemId={4} key="item5" isShared>
40+
Item 5
41+
</OverflowMenuDropdownItem>
42+
];
43+
44+
return (
45+
<OverflowMenu breakpoint="md" isVertical>
46+
<OverflowMenuContent>
47+
<OverflowMenuItem>Item</OverflowMenuItem>
48+
<OverflowMenuItem>Item</OverflowMenuItem>
49+
<OverflowMenuGroup>
50+
<OverflowMenuItem>Item</OverflowMenuItem>
51+
<OverflowMenuItem>Item</OverflowMenuItem>
52+
<OverflowMenuItem>Item</OverflowMenuItem>
53+
</OverflowMenuGroup>
54+
</OverflowMenuContent>
55+
<OverflowMenuControl>
56+
<Dropdown
57+
onSelect={onSelect}
58+
toggle={(toggleRef) => (
59+
<MenuToggle
60+
ref={toggleRef}
61+
aria-label="Simple example overflow menu"
62+
variant="plain"
63+
onClick={onToggle}
64+
isExpanded={isOpen}
65+
icon={<EllipsisVIcon />}
66+
/>
67+
)}
68+
isOpen={isOpen}
69+
onOpenChange={(isOpen) => setIsOpen(isOpen)}
70+
>
71+
<DropdownList>{dropdownItems}</DropdownList>
72+
</Dropdown>
73+
</OverflowMenuControl>
74+
</OverflowMenu>
75+
);
76+
};

packages/react-core/src/components/Toolbar/ToolbarContent.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,22 @@ import { PageContext } from '../Page/PageContext';
88
export interface ToolbarContentProps extends React.HTMLProps<HTMLDivElement> {
99
/** Classes applied to root element of the data toolbar content row */
1010
className?: string;
11-
/** Visibility at various breakpoints. */
11+
/** Visibility at various width breakpoints. */
1212
visibility?: {
1313
default?: 'hidden' | 'visible';
1414
md?: 'hidden' | 'visible';
1515
lg?: 'hidden' | 'visible';
1616
xl?: 'hidden' | 'visible';
1717
'2xl'?: 'hidden' | 'visible';
1818
};
19+
/** Visibility at various height breakpoints. */
20+
visibilityAtHeight?: {
21+
default?: 'hidden' | 'visible';
22+
md?: 'hidden' | 'visible';
23+
lg?: 'hidden' | 'visible';
24+
xl?: 'hidden' | 'visible';
25+
'2xl'?: 'hidden' | 'visible';
26+
};
1927
/** Value to set for content wrapping at various breakpoints */
2028
rowWrap?: {
2129
default?: 'wrap' | 'nowrap';
@@ -59,6 +67,7 @@ class ToolbarContent extends Component<ToolbarContentProps> {
5967
isExpanded,
6068
toolbarId,
6169
visibility,
70+
visibilityAtHeight,
6271
rowWrap,
6372
alignItems,
6473
clearAllFilters,
@@ -69,11 +78,12 @@ class ToolbarContent extends Component<ToolbarContentProps> {
6978

7079
return (
7180
<PageContext.Consumer>
72-
{({ width, getBreakpoint }) => (
81+
{({ width, getBreakpoint, height, getVerticalBreakpoint }) => (
7382
<div
7483
className={css(
7584
styles.toolbarContent,
7685
formatBreakpointMods(visibility, styles, '', getBreakpoint(width)),
86+
formatBreakpointMods(visibilityAtHeight, styles, '', getVerticalBreakpoint(height), true),
7787
className
7888
)}
7989
ref={this.expandableContentRef}

0 commit comments

Comments
 (0)