Skip to content

Commit 08742ba

Browse files
committed
feat(Toolbar,OverflowMenu): support responsive height vis breakpoints
1 parent a7a847c commit 08742ba

9 files changed

Lines changed: 317 additions & 11 deletions

File tree

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

Lines changed: 36 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 'src/helpers';
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 */
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,29 @@ class OverflowMenu extends Component<OverflowMenuProps, OverflowMenuState> {
83100
}
84101
};
85102

103+
handleResizeHeight = () => {
104+
const breakpointHeight = globalHeightBreakpoints[this.props.breakpoint];
105+
if (!breakpointHeight) {
106+
// eslint-disable-next-line no-console
107+
console.error('OverflowMenu will not be visible without a valid breakpoint.');
108+
return;
109+
}
110+
111+
const relativeHeight = this.state.breakpointRef ? this.state.breakpointRef.clientHeight : window.innerHeight;
112+
const isBelowBreakpoint = relativeHeight < breakpointHeight;
113+
if (this.state.isBelowBreakpoint !== isBelowBreakpoint) {
114+
this.setState({ isBelowBreakpoint });
115+
}
116+
};
117+
86118
handleResizeWithDelay = debounce(this.handleResize, 250);
87119

88120
render() {
89121
// eslint-disable-next-line @typescript-eslint/no-unused-vars
90-
const { className, breakpoint, children, breakpointReference, ...props } = this.props;
122+
const { className, breakpoint, children, breakpointReference, isVertical, ...props } = this.props;
91123

92124
return (
93-
<div {...props} className={css(styles.overflowMenu, className)}>
125+
<div {...props} className={css(styles.overflowMenu, isVertical && styles.modifiers.vertical, className)}>
94126
<OverflowMenuContext.Provider value={{ isBelowBreakpoint: this.state.isBelowBreakpoint }}>
95127
{children}
96128
</OverflowMenuContext.Provider>

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

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

2828
```
2929

30+
### Simple vertical (responsive)
31+
32+
```ts file="./OverflowMenuSimpleVertical.tsx"
33+
34+
```
35+
3036
### Group types
3137

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

4652
```
4753

48-
### Breakpoint on container
54+
### Breakpoint on container width
4955

5056
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.
5157

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

5662
```
63+
64+
### Breakpoint on container height
65+
66+
By passing in the `breakpointReference` and `isVertical` properties, the overflow menu's breakpoint will be relative to the height of the reference container rather than the viewport height.
67+
68+
```ts file="./OverflowMenuBreakpointOnContainerHeight.tsx"
69+
70+
```
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
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+
<div style={{ height: '100%', maxHeight: '400px' }}>
62+
<div>
63+
<span id="overflowMenu-hasBreakpointOnContainer-height-slider-label">Current container width</span>:{' '}
64+
{containerHeight}%
65+
</div>
66+
<Slider
67+
value={containerHeight}
68+
onChange={onChange}
69+
max={100}
70+
min={20}
71+
step={20}
72+
showTicks
73+
showBoundaries={false}
74+
aria-labelledby="overflowMenu-hasBreakpointOnContainer-height-slider-label"
75+
/>
76+
</div>
77+
<div ref={containerRef} id="height-breakpoint-reference-container" style={containerStyles}>
78+
<OverflowMenu breakpointReference={containerRef} breakpoint="sm">
79+
<OverflowMenuContent>
80+
<OverflowMenuItem>Item 1</OverflowMenuItem>
81+
<OverflowMenuItem>Item 2</OverflowMenuItem>
82+
<OverflowMenuGroup>
83+
<OverflowMenuItem>Item 3</OverflowMenuItem>
84+
<OverflowMenuItem>Item 4</OverflowMenuItem>
85+
<OverflowMenuItem>Item 5</OverflowMenuItem>
86+
</OverflowMenuGroup>
87+
</OverflowMenuContent>
88+
<OverflowMenuControl>
89+
<Dropdown
90+
onSelect={onSelect}
91+
toggle={(toggleRef) => (
92+
<MenuToggle
93+
ref={toggleRef}
94+
aria-label="Height breakpoint on container example overflow menu"
95+
variant="plain"
96+
onClick={onToggle}
97+
isExpanded={isOpen}
98+
icon={<EllipsisVIcon />}
99+
/>
100+
)}
101+
isOpen={isOpen}
102+
onOpenChange={(isOpen) => setIsOpen(isOpen)}
103+
>
104+
<DropdownList>{dropdownItems}</DropdownList>
105+
</Dropdown>
106+
</OverflowMenuControl>
107+
</OverflowMenu>
108+
</div>
109+
</>
110+
);
111+
};
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={5} key="item5" isShared>
40+
Item 5
41+
</OverflowMenuDropdownItem>
42+
];
43+
44+
return (
45+
<OverflowMenu breakpoint="lg" 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)