diff --git a/packages/react-core/src/components/Toolbar/Toolbar.tsx b/packages/react-core/src/components/Toolbar/Toolbar.tsx index 5a0cf48de75..5ddb7e71a99 100644 --- a/packages/react-core/src/components/Toolbar/Toolbar.tsx +++ b/packages/react-core/src/components/Toolbar/Toolbar.tsx @@ -3,7 +3,8 @@ import styles from '@patternfly/react-styles/css/components/Toolbar/toolbar'; import { css } from '@patternfly/react-styles'; import { ToolbarContext } from './ToolbarUtils'; import { ToolbarLabelGroupContent } from './ToolbarLabelGroupContent'; -import { formatBreakpointMods, canUseDOM } from '../../helpers/util'; +import { formatBreakpointMods, canUseDOM, getBreakpoint } from '../../helpers/util'; +import { getResizeObserver } from '../../helpers/resizeObserver'; import { getOUIAProps, OUIAProps } from '../../helpers'; import { SSRSafeIds } from '../../helpers/SSRSafeIds/SSRSafeIds'; import { PageContext } from '../Page/PageContext'; @@ -34,6 +35,8 @@ export interface ToolbarProps extends React.HTMLProps, OUIAProps children?: React.ReactNode; /** Id of the data toolbar */ id?: string; + /** Flag indicating if the toolbar is a container */ + isContainer?: boolean; /** Flag indicating the toolbar height should expand to the full height of the container */ isFullHeight?: boolean; /** Flag indicating the toolbar is static */ @@ -84,6 +87,9 @@ interface FilterInfo { class Toolbar extends Component { static displayName = 'Toolbar'; labelGroupContentRef = createRef(); + toolbarRef = createRef(); + resizeObserver: () => void = () => {}; + containerBreakpoint: ReturnType; staticFilterInfo = {}; hasNoPadding = false; state = { @@ -109,18 +115,45 @@ class Toolbar extends Component { } }; + closeExpandableContentOnContainerResize = () => { + const containerWidth = this.toolbarRef.current?.clientWidth; + if (!containerWidth) { + return; + } + + const breakpoint = getBreakpoint(containerWidth); + if (breakpoint !== this.containerBreakpoint) { + this.containerBreakpoint = breakpoint; + if (this.state.isManagedToggleExpanded) { + this.setState({ isManagedToggleExpanded: false }); + } + } + }; + componentDidMount() { if (canUseDOM) { this.setState({ windowWidth: window.innerWidth }); } if (this.isToggleManaged() && canUseDOM) { - window.addEventListener('resize', this.closeExpandableContent); + if (this.props.isContainer && this.toolbarRef.current) { + this.resizeObserver = getResizeObserver( + this.toolbarRef.current, + this.closeExpandableContentOnContainerResize, + true + ); + } else { + window.addEventListener('resize', this.closeExpandableContent); + } } } componentWillUnmount() { if (this.isToggleManaged() && canUseDOM) { - window.removeEventListener('resize', this.closeExpandableContent); + if (this.props.isContainer) { + this.resizeObserver(); + } else { + window.removeEventListener('resize', this.closeExpandableContent); + } } } @@ -146,6 +179,7 @@ class Toolbar extends Component { toggleIsExpanded, className, children, + isContainer, isFullHeight, isStatic, isStickyBase, @@ -174,6 +208,7 @@ class Toolbar extends Component { className={css( styles.toolbar, hasNoPadding && styles.modifiers.noPadding, + isContainer && styles.modifiers.container, isFullHeight && styles.modifiers.fullHeight, isStatic && styles.modifiers.static, isSticky && styles.modifiers.sticky, @@ -187,6 +222,7 @@ class Toolbar extends Component { className )} id={randomId} + ref={this.toolbarRef} {...getOUIAProps(Toolbar.displayName, ouiaId !== undefined ? ouiaId : generatedOuiaId)} {...props} > diff --git a/packages/react-core/src/components/Toolbar/ToolbarContent.tsx b/packages/react-core/src/components/Toolbar/ToolbarContent.tsx index ee547c58acf..9e723934f1d 100644 --- a/packages/react-core/src/components/Toolbar/ToolbarContent.tsx +++ b/packages/react-core/src/components/Toolbar/ToolbarContent.tsx @@ -78,11 +78,11 @@ class ToolbarContent extends Component { return ( - {({ width, getBreakpoint, height, getVerticalBreakpoint }) => ( + {({ height, getVerticalBreakpoint }) => (
{
{ return ( - {({ width, getBreakpoint, height, getVerticalBreakpoint }) => ( + {({ height, getVerticalBreakpoint }) => (
{ | 'actionGroupPlain' | 'labelGroup' ], - formatBreakpointMods(visibility, styles, '', getBreakpoint(width)), + formatBreakpointMods(visibility, styles), formatBreakpointMods(visibilityAtHeight, styles, '', getVerticalBreakpoint(height), true), - formatBreakpointMods(align, styles, '', getBreakpoint(width)), - formatBreakpointMods(gap, styles, '', getBreakpoint(width)), - formatBreakpointMods(columnGap, styles, '', getBreakpoint(width)), - formatBreakpointMods(rowGap, styles, '', getBreakpoint(width)), - formatBreakpointMods(rowWrap, styles, '', getBreakpoint(width)), - formatBreakpointMods(flexGrow, styles, '', getBreakpoint(width)), + formatBreakpointMods(align, styles), + formatBreakpointMods(gap, styles), + formatBreakpointMods(columnGap, styles), + formatBreakpointMods(rowGap, styles), + formatBreakpointMods(rowWrap, styles), + formatBreakpointMods(flexGrow, styles), alignItems === 'start' && styles.modifiers.alignItemsStart, alignItems === 'center' && styles.modifiers.alignItemsCenter, alignItems === 'baseline' && styles.modifiers.alignItemsBaseline, diff --git a/packages/react-core/src/components/Toolbar/ToolbarItem.tsx b/packages/react-core/src/components/Toolbar/ToolbarItem.tsx index 66778c13451..4328f9888f4 100644 --- a/packages/react-core/src/components/Toolbar/ToolbarItem.tsx +++ b/packages/react-core/src/components/Toolbar/ToolbarItem.tsx @@ -21,6 +21,7 @@ export interface ToolbarItemProps extends React.HTMLProps { /** Visibility at various width breakpoints. */ visibility?: { default?: 'hidden' | 'visible'; + sm?: 'hidden' | 'visible'; md?: 'hidden' | 'visible'; lg?: 'hidden' | 'visible'; xl?: 'hidden' | 'visible'; @@ -236,7 +237,7 @@ export const ToolbarItem: React.FunctionComponent = ({ return ( - {({ width, getBreakpoint, height, getVerticalBreakpoint }) => ( + {({ height, getVerticalBreakpoint }) => (
= ({ variant === ToolbarItemVariant['label-group'] && styles.modifiers.labelGroup, isAllExpanded && styles.modifiers.expanded, isOverflowContainer && styles.modifiers.overflowContainer, - formatBreakpointMods(visibility, styles, '', getBreakpoint(width)), + formatBreakpointMods(visibility, styles), formatBreakpointMods(visibilityAtHeight, styles, '', getVerticalBreakpoint(height), true), - formatBreakpointMods(align, styles, '', getBreakpoint(width)), - formatBreakpointMods(gap, styles, '', getBreakpoint(width)), - formatBreakpointMods(columnGap, styles, '', getBreakpoint(width)), - formatBreakpointMods(rowGap, styles, '', getBreakpoint(width)), - formatBreakpointMods(rowWrap, styles, '', getBreakpoint(width)), - formatBreakpointMods(flexGrow, styles, '', getBreakpoint(width)), + formatBreakpointMods(align, styles), + formatBreakpointMods(gap, styles), + formatBreakpointMods(columnGap, styles), + formatBreakpointMods(rowGap, styles), + formatBreakpointMods(rowWrap, styles), + formatBreakpointMods(flexGrow, styles), alignItems === 'start' && styles.modifiers.alignItemsStart, alignItems === 'center' && styles.modifiers.alignItemsCenter, alignItems === 'baseline' && styles.modifiers.alignItemsBaseline, diff --git a/packages/react-core/src/components/Toolbar/ToolbarToggleGroup.tsx b/packages/react-core/src/components/Toolbar/ToolbarToggleGroup.tsx index d5caed8a187..5bfc1b6decf 100644 --- a/packages/react-core/src/components/Toolbar/ToolbarToggleGroup.tsx +++ b/packages/react-core/src/components/Toolbar/ToolbarToggleGroup.tsx @@ -7,7 +7,6 @@ import { ToolbarContext, ToolbarContentContext } from './ToolbarUtils'; import { Button } from '../Button'; import globalBreakpointLg from '@patternfly/react-tokens/dist/esm/t_global_breakpoint_lg'; import { formatBreakpointMods, toCamel } from '../../helpers/util'; -import { PageContext } from '../Page/PageContext'; import { ToolbarExpandableContent } from './ToolbarExpandableContent'; export interface ToolbarToggleGroupProps extends ToolbarGroupProps { @@ -188,99 +187,95 @@ class ToolbarToggleGroup extends Component { } return ( - - {({ width, getBreakpoint }) => ( - - {({ toggleIsExpanded: managedOnToggle }) => { - const _onToggle = onToggle !== undefined ? onToggle : managedOnToggle; + + {({ toggleIsExpanded: managedOnToggle }) => { + const _onToggle = onToggle !== undefined ? onToggle : managedOnToggle; - return ( - - {({ - expandableContentRef, - expandableContentId, - labelContainerRef: managedLabelContainerRef, - isExpanded: managedIsExpanded, - clearAllFilters: clearAllFiltersContext, - clearFiltersButtonText: clearFiltersButtonContext, - showClearFiltersButton: showClearFiltersButtonContext - }) => { - const _isExpanded = isExpanded !== undefined ? isExpanded : managedIsExpanded; - const _labelContainerRef = - labelContainerRef !== undefined ? labelContainerRef : managedLabelContainerRef; + return ( + + {({ + expandableContentRef, + expandableContentId, + labelContainerRef: managedLabelContainerRef, + isExpanded: managedIsExpanded, + clearAllFilters: clearAllFiltersContext, + clearFiltersButtonText: clearFiltersButtonContext, + showClearFiltersButton: showClearFiltersButtonContext + }) => { + const _isExpanded = isExpanded !== undefined ? isExpanded : managedIsExpanded; + const _labelContainerRef = + labelContainerRef !== undefined ? labelContainerRef : managedLabelContainerRef; - const breakpointMod: { - md?: 'show'; - lg?: 'show'; - xl?: 'show'; - '2xl'?: 'show'; - } = {}; - breakpointMod[breakpoint] = 'show'; + const breakpointMod: { + md?: 'show'; + lg?: 'show'; + xl?: 'show'; + '2xl'?: 'show'; + } = {}; + breakpointMod[breakpoint] = 'show'; - const expandableContent = ( - - {children} - - ); + const expandableContent = ( + + {children} + + ); - const toggleButton = ( -
-
- ); + const toggleButton = ( +
+
+ ); - return ( -
- {toggleButton} - {_isExpanded && ReactDOM.createPortal(expandableContent, expandableContentRef.current)} - {!_isExpanded && children} -
- ); - }} -
- ); - }} -
- )} -
+ return ( +
+ {toggleButton} + {_isExpanded && ReactDOM.createPortal(expandableContent, expandableContentRef.current)} + {!_isExpanded && children} +
+ ); + }} + + ); + }} + ); } } diff --git a/packages/react-core/src/components/Toolbar/__tests__/Toolbar.test.tsx b/packages/react-core/src/components/Toolbar/__tests__/Toolbar.test.tsx index 7e7494e525b..1339d63385f 100644 --- a/packages/react-core/src/components/Toolbar/__tests__/Toolbar.test.tsx +++ b/packages/react-core/src/components/Toolbar/__tests__/Toolbar.test.tsx @@ -299,4 +299,42 @@ describe('Toolbar', () => { ); expect(screen.getByTestId('toolbar-sticky-stuck')).toHaveClass(styles.modifiers.stickyStuck); }); + + it('renders the container modifier when isContainer is true', () => { + render(); + + expect(screen.getByTestId('toolbar-container')).toHaveClass('pf-m-container'); + }); + + it('renders all responsive visibility modifiers', () => { + render(); + + const item = screen.getByTestId('toolbar-item'); + + expect(item).toHaveClass('pf-m-hidden-on-md'); + expect(item).toHaveClass('pf-m-visible-on-xl'); + }); + + it('renders all responsive row wrap modifiers', () => { + render(); + + const contentSection = screen.getByTestId('toolbar-content').querySelector('div'); + + expect(contentSection).toHaveClass('pf-m-nowrap-on-md'); + expect(contentSection).toHaveClass('pf-m-wrap-on-xl'); + }); + + it('renders the toggle group breakpoint modifier', () => { + render( + + + }> + Content + + + + ); + + expect(screen.getByTestId('toolbar-toggle-group')).toHaveClass('pf-m-show-on-xl'); + }); }); diff --git a/packages/react-core/src/components/Toolbar/examples/Toolbar.md b/packages/react-core/src/components/Toolbar/examples/Toolbar.md index 9b00cd5132f..f9e46b684fe 100644 --- a/packages/react-core/src/components/Toolbar/examples/Toolbar.md +++ b/packages/react-core/src/components/Toolbar/examples/Toolbar.md @@ -6,6 +6,7 @@ section: components --- import { Fragment, useState, useLayoutEffect, useRef } from 'react'; +import './ToolbarContainerQuery.css'; import EditIcon from '@patternfly/react-icons/dist/esm/icons/edit-icon'; import RhUiCopyFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-copy-fill-icon'; @@ -78,6 +79,21 @@ To change the background color of a toolbar, use the `colorVariant` property on ``` +## Responsive toolbar behavior + +Toolbar width-based responsive modifiers use `@container` queries with the global PatternFly breakpoints. Responsive inset modifiers (`inset`) are an exception and use viewport media queries. + +By default, the container `pf-v6-contain-toolbar` is established on `:root`, so the breakpoints behave like viewport breakpoints. To make a toolbar respond to its own available width, set the `isContainer` property on ``. Note: Alternatively, if a wrapping element creates an `inline-size` or `size` container with the container name `pf-v6-contain-toolbar`, the breakpoints will apply to that container's width instead. + +### Toolbar as container +Add `isContainer` to the toolbar so width-based modifiers respond to the toolbar's width. Resize the dashed box to see items and groups show and hide at the global breakpoints. + +_Debugging note: `xl` and `2xl` may not trigger when the content area is narrower than those breakpoints (view in full screen)._ + +```ts file="./ToolbarContainerQuery.tsx" + +``` + ## Examples with toggle groups and filters The following examples use toggle groups to allow for more responsive and complex toolbars with multiple items and groups of items. To visualize responsive toolbar behavior in the following examples, resize the browser to a smaller screen width. diff --git a/packages/react-core/src/components/Toolbar/examples/ToolbarContainerQuery.css b/packages/react-core/src/components/Toolbar/examples/ToolbarContainerQuery.css new file mode 100644 index 00000000000..d9314b0c2ff --- /dev/null +++ b/packages/react-core/src/components/Toolbar/examples/ToolbarContainerQuery.css @@ -0,0 +1,14 @@ +.toolbar-container-query-resize { + width: 800px; + min-width: 300px; + max-width: 100%; + padding: var(--pf-t--global--spacer--sm); + overflow: auto; + resize: horizontal; + border: var(--pf-t--global--border--width--extra-strong) dashed var(--pf-t--global--border--color--default); +} + +.toolbar-container-query-resize .pf-v6-c-toolbar__group { + border: var(--pf-t--global--border--width--control--default) dashed var(--pf-t--global--border--color--default); + padding: var(--pf-t--global--spacer--sm); +} diff --git a/packages/react-core/src/components/Toolbar/examples/ToolbarContainerQuery.tsx b/packages/react-core/src/components/Toolbar/examples/ToolbarContainerQuery.tsx new file mode 100644 index 00000000000..728edd14492 --- /dev/null +++ b/packages/react-core/src/components/Toolbar/examples/ToolbarContainerQuery.tsx @@ -0,0 +1,35 @@ +import { Button, Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem } from '@patternfly/react-core'; + +export const ToolbarContainerQuery: React.FunctionComponent = () => ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+);