From 08742baa827acb8cec58e680c21565c5ad1f8d53 Mon Sep 17 00:00:00 2001 From: Katie McFaul Date: Fri, 10 Apr 2026 13:09:25 -0400 Subject: [PATCH] feat(Toolbar,OverflowMenu): support responsive height vis breakpoints --- .../components/OverflowMenu/OverflowMenu.tsx | 40 ++++++- .../OverflowMenu/examples/OverflowMenu.md | 16 ++- ...verflowMenuBreakpointOnContainerHeight.tsx | 111 ++++++++++++++++++ .../examples/OverflowMenuSimpleVertical.tsx | 76 ++++++++++++ .../src/components/Toolbar/ToolbarContent.tsx | 14 ++- .../src/components/Toolbar/ToolbarGroup.tsx | 14 ++- .../src/components/Toolbar/ToolbarItem.tsx | 14 ++- .../components/Toolbar/examples/Toolbar.md | 10 ++ .../Toolbar/examples/ToolbarVertical.tsx | 33 ++++++ 9 files changed, 317 insertions(+), 11 deletions(-) create mode 100644 packages/react-core/src/components/OverflowMenu/examples/OverflowMenuBreakpointOnContainerHeight.tsx create mode 100644 packages/react-core/src/components/OverflowMenu/examples/OverflowMenuSimpleVertical.tsx create mode 100644 packages/react-core/src/components/Toolbar/examples/ToolbarVertical.tsx diff --git a/packages/react-core/src/components/OverflowMenu/OverflowMenu.tsx b/packages/react-core/src/components/OverflowMenu/OverflowMenu.tsx index c1e10cda2e1..b1927db374a 100644 --- a/packages/react-core/src/components/OverflowMenu/OverflowMenu.tsx +++ b/packages/react-core/src/components/OverflowMenu/OverflowMenu.tsx @@ -3,18 +3,21 @@ import styles from '@patternfly/react-styles/css/components/OverflowMenu/overflo import { css } from '@patternfly/react-styles'; import { OverflowMenuContext } from './OverflowMenuContext'; import { debounce } from '../../helpers/util'; -import { globalWidthBreakpoints } from '../../helpers/constants'; +import { globalWidthBreakpoints, globalHeightBreakpoints } from '../../helpers/constants'; import { getResizeObserver } from '../../helpers/resizeObserver'; +import { PickOptional } from 'src/helpers'; export interface OverflowMenuProps extends React.HTMLProps { /** Any elements that can be rendered in the menu */ children?: any; /** Additional classes added to the OverflowMenu. */ className?: string; - /** Indicates breakpoint at which to switch between horizontal menu and vertical dropdown */ + /** Indicates breakpoint at which to switch between expanded and collapsed states */ breakpoint: 'sm' | 'md' | 'lg' | 'xl' | '2xl'; /** A container reference to base the specified breakpoint on instead of the viewport width. */ breakpointReference?: HTMLElement | (() => HTMLElement) | React.RefObject; + /** Indicates the overflow menu orientation is vertical and should respond to height changes instead of width. */ + isVertical?: boolean; } export interface OverflowMenuState extends React.HTMLProps { @@ -24,6 +27,11 @@ export interface OverflowMenuState extends React.HTMLProps { class OverflowMenu extends Component { static displayName = 'OverflowMenu'; + + static defaultProps: PickOptional = { + isVertical: false + }; + constructor(props: OverflowMenuProps) { super(props); this.state = { @@ -69,6 +77,15 @@ class OverflowMenu extends Component { } handleResize = () => { + const { isVertical } = this.props; + if (isVertical) { + this.handleResizeHeight(); + } else { + this.handleResizeWidth(); + } + }; + + handleResizeWidth = () => { const breakpointWidth = globalWidthBreakpoints[this.props.breakpoint]; if (!breakpointWidth) { // eslint-disable-next-line no-console @@ -83,14 +100,29 @@ class OverflowMenu extends Component { } }; + handleResizeHeight = () => { + const breakpointHeight = globalHeightBreakpoints[this.props.breakpoint]; + if (!breakpointHeight) { + // eslint-disable-next-line no-console + console.error('OverflowMenu will not be visible without a valid breakpoint.'); + return; + } + + const relativeHeight = this.state.breakpointRef ? this.state.breakpointRef.clientHeight : window.innerHeight; + const isBelowBreakpoint = relativeHeight < breakpointHeight; + if (this.state.isBelowBreakpoint !== isBelowBreakpoint) { + this.setState({ isBelowBreakpoint }); + } + }; + handleResizeWithDelay = debounce(this.handleResize, 250); render() { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { className, breakpoint, children, breakpointReference, ...props } = this.props; + const { className, breakpoint, children, breakpointReference, isVertical, ...props } = this.props; return ( -
+
{children} diff --git a/packages/react-core/src/components/OverflowMenu/examples/OverflowMenu.md b/packages/react-core/src/components/OverflowMenu/examples/OverflowMenu.md index e73498c856d..489b021b6ff 100644 --- a/packages/react-core/src/components/OverflowMenu/examples/OverflowMenu.md +++ b/packages/react-core/src/components/OverflowMenu/examples/OverflowMenu.md @@ -27,6 +27,12 @@ import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-ico ``` +### Simple vertical (responsive) + +```ts file="./OverflowMenuSimpleVertical.tsx" + +``` + ### Group types ```ts file="./OverflowMenuGroupTypes.tsx" @@ -45,7 +51,7 @@ import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-ico ``` -### Breakpoint on container +### Breakpoint on container width 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. @@ -54,3 +60,11 @@ You can change the container width in this example by adjusting the slider. As t ```ts file="./OverflowMenuBreakpointOnContainer.tsx" ``` + +### Breakpoint on container height + +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. + +```ts file="./OverflowMenuBreakpointOnContainerHeight.tsx" + +``` diff --git a/packages/react-core/src/components/OverflowMenu/examples/OverflowMenuBreakpointOnContainerHeight.tsx b/packages/react-core/src/components/OverflowMenu/examples/OverflowMenuBreakpointOnContainerHeight.tsx new file mode 100644 index 00000000000..ad102106c2f --- /dev/null +++ b/packages/react-core/src/components/OverflowMenu/examples/OverflowMenuBreakpointOnContainerHeight.tsx @@ -0,0 +1,111 @@ +import { useRef, useState } from 'react'; +import { + OverflowMenu, + OverflowMenuControl, + OverflowMenuContent, + OverflowMenuGroup, + OverflowMenuItem, + OverflowMenuDropdownItem, + MenuToggle, + Slider, + SliderOnChangeEvent, + Dropdown, + DropdownList +} from '@patternfly/react-core'; +import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon'; + +export const OverflowMenuBreakpointOnContainerHeight: React.FunctionComponent = () => { + const [isOpen, setIsOpen] = useState(false); + const [containerHeight, setContainerHeight] = useState(100); + const containerRef = useRef(null); + + const onToggle = () => { + setIsOpen(!isOpen); + }; + + const onSelect = () => { + setIsOpen(!isOpen); + }; + + const onChange = (_event: SliderOnChangeEvent, value: number) => { + setContainerHeight(value); + }; + + const containerStyles = { + height: `${containerHeight}%`, + padding: '1rem', + borderWidth: '2px', + borderStyle: 'dashed' + }; + + const dropdownItems = [ + + Item 1 + , + + Item 2 + , + + Item 3 + , + + Item 4 + , + + Item 5 + + ]; + + return ( + <> +
+
+ Current container width:{' '} + {containerHeight}% +
+ +
+
+ + + Item 1 + Item 2 + + Item 3 + Item 4 + Item 5 + + + + ( + } + /> + )} + isOpen={isOpen} + onOpenChange={(isOpen) => setIsOpen(isOpen)} + > + {dropdownItems} + + + +
+ + ); +}; diff --git a/packages/react-core/src/components/OverflowMenu/examples/OverflowMenuSimpleVertical.tsx b/packages/react-core/src/components/OverflowMenu/examples/OverflowMenuSimpleVertical.tsx new file mode 100644 index 00000000000..7a57868d54e --- /dev/null +++ b/packages/react-core/src/components/OverflowMenu/examples/OverflowMenuSimpleVertical.tsx @@ -0,0 +1,76 @@ +import { useState } from 'react'; +import { + OverflowMenu, + OverflowMenuControl, + OverflowMenuContent, + OverflowMenuGroup, + OverflowMenuItem, + OverflowMenuDropdownItem, + MenuToggle, + Dropdown, + DropdownList +} from '@patternfly/react-core'; +import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon'; + +export const OverflowMenuSimpleVertical: React.FunctionComponent = () => { + const [isOpen, setIsOpen] = useState(false); + + const onToggle = () => { + setIsOpen(!isOpen); + }; + + const onSelect = () => { + setIsOpen(!isOpen); + }; + + const dropdownItems = [ + + Item 1 + , + + Item 2 + , + + Item 3 + , + + Item 4 + , + + Item 5 + + ]; + + return ( + + + Item + Item + + Item + Item + Item + + + + ( + } + /> + )} + isOpen={isOpen} + onOpenChange={(isOpen) => setIsOpen(isOpen)} + > + {dropdownItems} + + + + ); +}; diff --git a/packages/react-core/src/components/Toolbar/ToolbarContent.tsx b/packages/react-core/src/components/Toolbar/ToolbarContent.tsx index ec722df5a42..ee547c58acf 100644 --- a/packages/react-core/src/components/Toolbar/ToolbarContent.tsx +++ b/packages/react-core/src/components/Toolbar/ToolbarContent.tsx @@ -8,7 +8,7 @@ import { PageContext } from '../Page/PageContext'; export interface ToolbarContentProps extends React.HTMLProps { /** Classes applied to root element of the data toolbar content row */ className?: string; - /** Visibility at various breakpoints. */ + /** Visibility at various width breakpoints. */ visibility?: { default?: 'hidden' | 'visible'; md?: 'hidden' | 'visible'; @@ -16,6 +16,14 @@ export interface ToolbarContentProps extends React.HTMLProps { xl?: 'hidden' | 'visible'; '2xl'?: 'hidden' | 'visible'; }; + /** Visibility at various height breakpoints. */ + visibilityAtHeight?: { + default?: 'hidden' | 'visible'; + md?: 'hidden' | 'visible'; + lg?: 'hidden' | 'visible'; + xl?: 'hidden' | 'visible'; + '2xl'?: 'hidden' | 'visible'; + }; /** Value to set for content wrapping at various breakpoints */ rowWrap?: { default?: 'wrap' | 'nowrap'; @@ -59,6 +67,7 @@ class ToolbarContent extends Component { isExpanded, toolbarId, visibility, + visibilityAtHeight, rowWrap, alignItems, clearAllFilters, @@ -69,11 +78,12 @@ class ToolbarContent extends Component { return ( - {({ width, getBreakpoint }) => ( + {({ width, getBreakpoint, height, getVerticalBreakpoint }) => (
, | 'action-group-inline' | 'action-group-plain' | 'label-group'; - /** Visibility at various breakpoints. */ + /** Visibility at various width breakpoints. */ visibility?: { default?: 'hidden' | 'visible'; md?: 'hidden' | 'visible'; @@ -31,6 +31,14 @@ export interface ToolbarGroupProps extends Omit, xl?: 'hidden' | 'visible'; '2xl'?: 'hidden' | 'visible'; }; + /** Visibility at various height breakpoints. */ + visibilityAtHeight?: { + default?: 'hidden' | 'visible'; + md?: 'hidden' | 'visible'; + lg?: 'hidden' | 'visible'; + xl?: 'hidden' | 'visible'; + '2xl'?: 'hidden' | 'visible'; + }; /** Applies to a child of a flex layout, and aligns that child (and any adjacent children on the other side of it) to one side of the main axis */ align?: { default?: 'alignEnd' | 'alignStart' | 'alignCenter'; @@ -178,6 +186,7 @@ class ToolbarGroupWithRef extends Component { render() { const { visibility, + visibilityAtHeight, align, alignItems, alignSelf, @@ -195,7 +204,7 @@ class ToolbarGroupWithRef extends Component { return ( - {({ width, getBreakpoint }) => ( + {({ width, getBreakpoint, height, getVerticalBreakpoint }) => (
{ | 'labelGroup' ], formatBreakpointMods(visibility, styles, '', getBreakpoint(width)), + formatBreakpointMods(visibilityAtHeight, styles, '', getVerticalBreakpoint(height), true), formatBreakpointMods(align, styles, '', getBreakpoint(width)), formatBreakpointMods(gap, styles, '', getBreakpoint(width)), formatBreakpointMods(columnGap, styles, '', getBreakpoint(width)), diff --git a/packages/react-core/src/components/Toolbar/ToolbarItem.tsx b/packages/react-core/src/components/Toolbar/ToolbarItem.tsx index 687654455ac..454ea9476e7 100644 --- a/packages/react-core/src/components/Toolbar/ToolbarItem.tsx +++ b/packages/react-core/src/components/Toolbar/ToolbarItem.tsx @@ -17,7 +17,7 @@ export interface ToolbarItemProps extends React.HTMLProps { className?: string; /** A type modifier which modifies spacing specifically depending on the type of item */ variant?: ToolbarItemVariant | 'pagination' | 'label' | 'label-group' | 'separator' | 'expand-all'; - /** Visibility at various breakpoints. */ + /** Visibility at various width breakpoints. */ visibility?: { default?: 'hidden' | 'visible'; md?: 'hidden' | 'visible'; @@ -25,6 +25,14 @@ export interface ToolbarItemProps extends React.HTMLProps { xl?: 'hidden' | 'visible'; '2xl'?: 'hidden' | 'visible'; }; + /** Visibility at various height breakpoints. */ + visibilityAtHeight?: { + default?: 'hidden' | 'visible'; + md?: 'hidden' | 'visible'; + lg?: 'hidden' | 'visible'; + xl?: 'hidden' | 'visible'; + '2xl'?: 'hidden' | 'visible'; + }; /** Applies to a child of a flex layout, and aligns that child (and any adjacent children on the other side of it) to one side of the main axis */ align?: { default?: 'alignEnd' | 'alignStart' | 'alignCenter'; @@ -174,6 +182,7 @@ export const ToolbarItem: React.FunctionComponent = ({ className, variant, visibility, + visibilityAtHeight, gap, columnGap, rowGap, @@ -202,7 +211,7 @@ export const ToolbarItem: React.FunctionComponent = ({ return ( - {({ width, getBreakpoint }) => ( + {({ width, getBreakpoint, height, getVerticalBreakpoint }) => (
= ({ isAllExpanded && styles.modifiers.expanded, isOverflowContainer && styles.modifiers.overflowContainer, formatBreakpointMods(visibility, styles, '', getBreakpoint(width)), + formatBreakpointMods(visibilityAtHeight, styles, '', getVerticalBreakpoint(height), true), formatBreakpointMods(align, styles, '', getBreakpoint(width)), formatBreakpointMods(gap, styles, '', getBreakpoint(width)), formatBreakpointMods(columnGap, styles, '', getBreakpoint(width)), diff --git a/packages/react-core/src/components/Toolbar/examples/Toolbar.md b/packages/react-core/src/components/Toolbar/examples/Toolbar.md index 89639b516c7..e6251177ae5 100644 --- a/packages/react-core/src/components/Toolbar/examples/Toolbar.md +++ b/packages/react-core/src/components/Toolbar/examples/Toolbar.md @@ -34,6 +34,14 @@ To adjust a toolbar’s inset, use the `inset` property. You can set the inset v ``` +### Vertical toolbar + +A toolbar's orientation may be changed using the `isVertical` property. Responsive behavior when height is adjusted may be customized for the `ToolbarContent`, `ToolbarGroup`, and `ToolbarItem` components using their respective `visibilityAtHeight` property. + +```ts file="./ToolbarVertical.tsx" + +``` + ### Sticky toolbar To lock a toolbar and prevent it from scrolling with other content, use a sticky toolbar. @@ -114,11 +122,13 @@ When all of a toolbar's required elements cannot fit in a single line, you can s ``` ## Examples with spacers and wrapping + You may adjust the space between toolbar items to arrange them into groups. Read our spacers documentation to learn more about using spacers. Items are spaced “16px” apart by default and can be modified by changing their or their parents' `gap`, `columnGap`, and `rowGap` properties. You can set the property values at multiple breakpoints, including "default", "md", "lg", "xl", and "2xl". ### Toolbar content wrapping + The toolbar content section will wrap by default, but you can set the `rowRap` property to `noWrap` to make it not wrap. ```ts file="./ToolbarContentWrap.tsx" diff --git a/packages/react-core/src/components/Toolbar/examples/ToolbarVertical.tsx b/packages/react-core/src/components/Toolbar/examples/ToolbarVertical.tsx new file mode 100644 index 00000000000..863d741a455 --- /dev/null +++ b/packages/react-core/src/components/Toolbar/examples/ToolbarVertical.tsx @@ -0,0 +1,33 @@ +import { Button, Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem } from '@patternfly/react-core'; +import EditIcon from '@patternfly/react-icons/dist/esm/icons/edit-icon'; +import CloneIcon from '@patternfly/react-icons/dist/esm/icons/clone-icon'; +import SyncIcon from '@patternfly/react-icons/dist/esm/icons/sync-icon'; + +export const ToolbarGroups: React.FunctionComponent = () => ( + + + + +