From f21a95c095180a882039349bb45856f939c1fc11 Mon Sep 17 00:00:00 2001 From: Sarah Rambacher Date: Fri, 12 Dec 2025 09:36:51 -0500 Subject: [PATCH] feat(toolbar): implement container query option --- .claude/settings.local.json | 8 + .../src/components/Toolbar/Toolbar.tsx | 78 ++++- .../components/Toolbar/ToolbarToggleGroup.tsx | 9 +- .../src/components/Toolbar/ToolbarUtils.tsx | 13 +- .../components/Toolbar/examples/Toolbar.css | 11 + .../components/Toolbar/examples/Toolbar.md | 21 ++ .../examples/ToolbarContainerQueryBasic.tsx | 140 +++++++++ .../ToolbarContainerQueryBreakpoints.tsx | 137 +++++++++ packages/react-core/src/demos/Toolbar.md | 18 ++ .../src/demos/examples/Toolbar/Toolbar.css | 10 + .../examples/Toolbar/ToolbarComparison.tsx | 159 +++++++++++ .../demos/examples/Toolbar/ToolbarInModal.tsx | 202 +++++++++++++ .../examples/Toolbar/ToolbarInSidebar.tsx | 266 ++++++++++++++++++ 13 files changed, 1065 insertions(+), 7 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 packages/react-core/src/components/Toolbar/examples/Toolbar.css create mode 100644 packages/react-core/src/components/Toolbar/examples/ToolbarContainerQueryBasic.tsx create mode 100644 packages/react-core/src/components/Toolbar/examples/ToolbarContainerQueryBreakpoints.tsx create mode 100644 packages/react-core/src/demos/examples/Toolbar/Toolbar.css create mode 100644 packages/react-core/src/demos/examples/Toolbar/ToolbarComparison.tsx create mode 100644 packages/react-core/src/demos/examples/Toolbar/ToolbarInModal.tsx create mode 100644 packages/react-core/src/demos/examples/Toolbar/ToolbarInSidebar.tsx diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000000..5271a717d77 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(npm run lint)", + "Bash(npm run lint:*)" + ] + } +} diff --git a/packages/react-core/src/components/Toolbar/Toolbar.tsx b/packages/react-core/src/components/Toolbar/Toolbar.tsx index 8941ba29493..87ab48f8fd6 100644 --- a/packages/react-core/src/components/Toolbar/Toolbar.tsx +++ b/packages/react-core/src/components/Toolbar/Toolbar.tsx @@ -2,11 +2,12 @@ import { Component, createRef } from 'react'; import styles from '@patternfly/react-styles/css/components/Toolbar/toolbar'; import { GenerateId } from '../../helpers/GenerateId/GenerateId'; import { css } from '@patternfly/react-styles'; -import { ToolbarContext } from './ToolbarUtils'; +import { ToolbarContext, globalBreakpoints, containerBreakpoints } from './ToolbarUtils'; import { ToolbarLabelGroupContent } from './ToolbarLabelGroupContent'; import { formatBreakpointMods, canUseDOM } from '../../helpers/util'; import { getDefaultOUIAId, getOUIAProps, OUIAProps } from '../../helpers'; import { PageContext } from '../Page/PageContext'; +import { getResizeObserver } from '../../helpers/resizeObserver'; export enum ToolbarColorVariant { default = 'default', @@ -59,6 +60,10 @@ export interface ToolbarProps extends React.HTMLProps, OUIAProps colorVariant?: ToolbarColorVariant | 'default' | 'no-background' | 'primary' | 'secondary'; /** Flag indicating the toolbar padding is removed */ hasNoPadding?: boolean; + /** Use container queries instead of viewport media queries for responsive behavior */ + useContainerQuery?: boolean; + /** Breakpoint for container queries. Only applies when useContainerQuery is true. */ + containerQueryBreakpoint?: 'sm' | 'md' | 'lg' | 'xl' | '2xl'; } export interface ToolbarState { @@ -69,6 +74,8 @@ export interface ToolbarState { filterInfo: FilterInfo; /** Used to keep track of window width so we can collapse expanded content when window is resizing */ windowWidth: number; + /** Used to keep track of container width so we can collapse expanded content when container is resizing */ + containerWidth: number; ouiaStateId: string; } @@ -79,12 +86,15 @@ interface FilterInfo { class Toolbar extends Component { static displayName = 'Toolbar'; labelGroupContentRef = createRef(); + toolbarRef = createRef(); + observer: any = () => {}; staticFilterInfo = {}; hasNoPadding = false; state = { isManagedToggleExpanded: false, filterInfo: {}, windowWidth: canUseDOM ? window.innerWidth : 1200, + containerWidth: 0, ouiaStateId: getDefaultOUIAId(Toolbar.displayName) }; @@ -105,15 +115,58 @@ class Toolbar extends Component { } }; + closeExpandableContentOnContainerResize = () => { + if (this.toolbarRef.current && this.toolbarRef.current.clientWidth) { + const newWidth = this.toolbarRef.current.clientWidth; + + if (newWidth !== this.state.containerWidth) { + // If expanded and container is wide enough for inline display at the specific breakpoint, close it + const specificBreakpoint = this.props.containerQueryBreakpoint || 'lg'; + + // Use container breakpoints when using container queries, otherwise use global breakpoints + let isWideEnoughForInline: boolean; + if (this.props.useContainerQuery) { + // Handle 'sm' case for container breakpoints + const breakpointKey = specificBreakpoint === 'sm' ? 'sm' : specificBreakpoint; + isWideEnoughForInline = newWidth >= containerBreakpoints[breakpointKey]; + } else { + // Handle 'sm' case - map to 'md' since globalBreakpoints doesn't have 'sm' + const breakpointKey = specificBreakpoint === 'sm' ? 'md' : specificBreakpoint; + isWideEnoughForInline = newWidth >= globalBreakpoints[breakpointKey]; + } + + if (this.state.isManagedToggleExpanded && isWideEnoughForInline) { + this.setState(() => ({ + isManagedToggleExpanded: false, + containerWidth: newWidth + })); + } else { + // Just update width without closing + this.setState(() => ({ + containerWidth: newWidth + })); + } + } + } + }; + componentDidMount() { if (this.isToggleManaged() && canUseDOM) { - window.addEventListener('resize', this.closeExpandableContent); + if (this.props.useContainerQuery && this.toolbarRef.current) { + this.observer = 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.useContainerQuery) { + this.observer(); + } else { + window.removeEventListener('resize', this.closeExpandableContent); + } } } @@ -147,6 +200,8 @@ class Toolbar extends Component { numberOfFiltersText, customLabelGroupContent, colorVariant = ToolbarColorVariant.default, + useContainerQuery, + containerQueryBreakpoint, ...props } = this.props; @@ -167,6 +222,19 @@ class Toolbar extends Component { isFullHeight && styles.modifiers.fullHeight, isStatic && styles.modifiers.static, isSticky && styles.modifiers.sticky, + useContainerQuery && !containerQueryBreakpoint && styles.modifiers.container, + useContainerQuery && + containerQueryBreakpoint && + ((): string => { + const breakpointClassMap: Record = { + '2xl': styles.modifiers.container_2xl, + sm: styles.modifiers.containerSm, + md: styles.modifiers.containerMd, + lg: styles.modifiers.containerLg, + xl: styles.modifiers.containerXl + }; + return breakpointClassMap[containerQueryBreakpoint] || ''; + })(), formatBreakpointMods(inset, styles, '', getBreakpoint(width)), colorVariant === 'primary' && styles.modifiers.primary, colorVariant === 'secondary' && styles.modifiers.secondary, @@ -174,6 +242,7 @@ class Toolbar extends Component { className )} id={randomId} + ref={this.toolbarRef} {...getOUIAProps(Toolbar.displayName, ouiaId !== undefined ? ouiaId : this.state.ouiaStateId)} {...props} > @@ -188,7 +257,8 @@ class Toolbar extends Component { clearFiltersButtonText, showClearFiltersButton, toolbarId: randomId, - customLabelGroupContent + customLabelGroupContent, + useContainerQuery }} > {children} diff --git a/packages/react-core/src/components/Toolbar/ToolbarToggleGroup.tsx b/packages/react-core/src/components/Toolbar/ToolbarToggleGroup.tsx index 408434a654c..deda4c178db 100644 --- a/packages/react-core/src/components/Toolbar/ToolbarToggleGroup.tsx +++ b/packages/react-core/src/components/Toolbar/ToolbarToggleGroup.tsx @@ -191,7 +191,7 @@ class ToolbarToggleGroup extends Component { {({ width, getBreakpoint }) => ( - {({ toggleIsExpanded: managedOnToggle }) => { + {({ toggleIsExpanded: managedOnToggle, useContainerQuery }) => { const _onToggle = onToggle !== undefined ? onToggle : managedOnToggle; return ( @@ -260,7 +260,12 @@ class ToolbarToggleGroup extends Component { | 'actionGroupPlain' | 'labelGroup' ], - formatBreakpointMods(breakpointMod, styles, '', getBreakpoint(width)), + formatBreakpointMods( + breakpointMod, + styles, + '', + useContainerQuery ? undefined : getBreakpoint(width) + ), formatBreakpointMods(visibility, styles, '', getBreakpoint(width)), formatBreakpointMods(gap, styles, '', getBreakpoint(width)), formatBreakpointMods(columnGap, styles, '', getBreakpoint(width)), diff --git a/packages/react-core/src/components/Toolbar/ToolbarUtils.tsx b/packages/react-core/src/components/Toolbar/ToolbarUtils.tsx index fbddb1ee6cd..3b007f70f3d 100644 --- a/packages/react-core/src/components/Toolbar/ToolbarUtils.tsx +++ b/packages/react-core/src/components/Toolbar/ToolbarUtils.tsx @@ -15,6 +15,7 @@ export interface ToolbarContextProps { showClearFiltersButton?: boolean; toolbarId?: string; customLabelGroupContent?: React.ReactNode; + useContainerQuery?: boolean; } export const ToolbarContext = createContext({ @@ -23,7 +24,8 @@ export const ToolbarContext = createContext({ labelGroupContentRef: null, updateNumberFilters: () => {}, numberOfFilters: 0, - clearAllFilters: () => {} + clearAllFilters: () => {}, + useContainerQuery: false }); interface ToolbarContentContextProps { @@ -49,3 +51,12 @@ export const globalBreakpoints = { xl: parseInt(globalBreakpointXl.value) * 16, '2xl': parseInt(globalBreakpoint2xl.value) * 16 }; + +// Container query breakpoints match CSS container query values +export const containerBreakpoints = { + sm: 286, + md: 478, + lg: 702, + xl: 992, // You may need to verify this value + '2xl': 1200 // You may need to verify this value +}; diff --git a/packages/react-core/src/components/Toolbar/examples/Toolbar.css b/packages/react-core/src/components/Toolbar/examples/Toolbar.css new file mode 100644 index 00000000000..8c90314d7ea --- /dev/null +++ b/packages/react-core/src/components/Toolbar/examples/Toolbar.css @@ -0,0 +1,11 @@ +.ws-react-c-toolbar-resize-container { + resize: horizontal; + overflow: auto; + border: var(--pf-t--global--border--width--extra-strong) dashed var(--pf-t--global--border--color--default); + /* padding: var(--pf-t--global--spacer--md); */ + width: 800px; + min-width: 300px; + max-width: 100%; + height: 15em; +} + diff --git a/packages/react-core/src/components/Toolbar/examples/Toolbar.md b/packages/react-core/src/components/Toolbar/examples/Toolbar.md index 89639b516c7..686132009c2 100644 --- a/packages/react-core/src/components/Toolbar/examples/Toolbar.md +++ b/packages/react-core/src/components/Toolbar/examples/Toolbar.md @@ -5,6 +5,7 @@ propComponents: ['Toolbar', 'ToolbarContent', 'ToolbarGroup', 'ToolbarItem', 'To section: components --- +import './Toolbar.css'; import { Fragment, useState } from 'react'; import EditIcon from '@patternfly/react-icons/dist/esm/icons/edit-icon'; @@ -113,6 +114,26 @@ When all of a toolbar's required elements cannot fit in a single line, you can s ``` +## Examples with container queries + +Container queries allow the toolbar to respond to its container size rather than the viewport size. This is useful when toolbars appear in sidebars, cards, modals, or other constrained spaces where you want the toolbar to adapt to its container's width independently from the viewport. + +### Basic container query usage + +The toolbar adapts based on its container width instead of the viewport width. Resize the container to see the responsive behavior. + +```ts file="./ToolbarContainerQueryBasic.tsx" + +``` + +### Container query breakpoints + +Use `containerQueryBreakpoint` to collapse the toolbar at different container widths. + +```ts file="./ToolbarContainerQueryBreakpoints.tsx" + +``` + ## 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. diff --git a/packages/react-core/src/components/Toolbar/examples/ToolbarContainerQueryBasic.tsx b/packages/react-core/src/components/Toolbar/examples/ToolbarContainerQueryBasic.tsx new file mode 100644 index 00000000000..14d1a179b3c --- /dev/null +++ b/packages/react-core/src/components/Toolbar/examples/ToolbarContainerQueryBasic.tsx @@ -0,0 +1,140 @@ +import { Fragment, useState } from 'react'; +import { + MenuToggle, + MenuToggleElement, + Toolbar, + ToolbarItem, + ToolbarContent, + ToolbarToggleGroup, + ToolbarGroup, + SearchInput, + Select, + SelectList, + SelectOption +} from '@patternfly/react-core'; +import FilterIcon from '@patternfly/react-icons/dist/esm/icons/filter-icon'; +import './Toolbar.css'; + +export const ToolbarContainerQueryBasic: React.FunctionComponent = () => { + const [inputValue, setInputValue] = useState(''); + const [statusIsExpanded, setStatusIsExpanded] = useState(false); + const [statusSelected, setStatusSelected] = useState(''); + const [riskIsExpanded, setRiskIsExpanded] = useState(false); + const [riskSelected, setRiskSelected] = useState(''); + + const statusOptions = ['New', 'Pending', 'Running', 'Cancelled']; + const riskOptions = ['Low', 'Medium', 'High']; + + const onInputChange = (newValue: string) => { + setInputValue(newValue); + }; + + const onStatusToggle = () => { + setStatusIsExpanded(!statusIsExpanded); + }; + + const onStatusSelect = (_event: React.MouseEvent | undefined, selection: string) => { + setStatusSelected(selection); + setStatusIsExpanded(false); + }; + + const onRiskToggle = () => { + setRiskIsExpanded(!riskIsExpanded); + }; + + const onRiskSelect = (_event: React.MouseEvent | undefined, selection: string) => { + setRiskSelected(selection); + setRiskIsExpanded(false); + }; + + const toggleGroupItems = ( + + + onInputChange(value)} + value={inputValue} + onClear={() => { + onInputChange(''); + }} + /> + + + + + + + + + + + ); + + const items = ( + } breakpoint="lg"> + {toggleGroupItems} + + ); + + return ( +
+ + {items} + +
+ ); +}; diff --git a/packages/react-core/src/components/Toolbar/examples/ToolbarContainerQueryBreakpoints.tsx b/packages/react-core/src/components/Toolbar/examples/ToolbarContainerQueryBreakpoints.tsx new file mode 100644 index 00000000000..cf0f08df900 --- /dev/null +++ b/packages/react-core/src/components/Toolbar/examples/ToolbarContainerQueryBreakpoints.tsx @@ -0,0 +1,137 @@ +import { Fragment, useState } from 'react'; +import { + MenuToggle, + MenuToggleElement, + Toolbar, + ToolbarItem, + ToolbarContent, + ToolbarToggleGroup, + ToolbarGroup, + SearchInput, + Select, + SelectList, + SelectOption, + Title +} from '@patternfly/react-core'; +import FilterIcon from '@patternfly/react-icons/dist/esm/icons/filter-icon'; +import './Toolbar.css'; + +export const ToolbarContainerQueryBreakpoints: React.FunctionComponent = () => { + const [inputValueMd, setInputValueMd] = useState(''); + const [statusExpandedMd, setStatusExpandedMd] = useState(false); + const [statusSelectedMd, setStatusSelectedMd] = useState(''); + + const [inputValueLg, setInputValueLg] = useState(''); + const [statusExpandedLg, setStatusExpandedLg] = useState(false); + const [statusSelectedLg, setStatusSelectedLg] = useState(''); + + const statusOptions = ['New', 'Pending', 'Running']; + + const createToolbar = ( + breakpoint: 'sm' | 'md' | 'lg', + inputValue: string, + setInputValue: (value: string) => void, + statusExpanded: boolean, + setStatusExpanded: (value: boolean) => void, + statusSelected: string, + setStatusSelected: (value: string) => void + ) => { + const toggleGroupItems = ( + + + setInputValue(value)} + value={inputValue} + onClear={() => setInputValue('')} + /> + + + + + + + + ); + + return ( +
+ + Breakpoint: {breakpoint} + +

+ Collapses at {breakpoint} container width +

+
+ + + } breakpoint={breakpoint}> + {toggleGroupItems} + + + +
+
+ ); + }; + + return ( +
+ {createToolbar( + 'md', + inputValueMd, + setInputValueMd, + statusExpandedMd, + setStatusExpandedMd, + statusSelectedMd, + setStatusSelectedMd + )} + + {createToolbar( + 'xl', + inputValueLg, + setInputValueLg, + statusExpandedLg, + setStatusExpandedLg, + statusSelectedLg, + setStatusSelectedLg + )} +
+ ); +}; diff --git a/packages/react-core/src/demos/Toolbar.md b/packages/react-core/src/demos/Toolbar.md index 19560201610..d8c35225602 100644 --- a/packages/react-core/src/demos/Toolbar.md +++ b/packages/react-core/src/demos/Toolbar.md @@ -3,6 +3,7 @@ id: Toolbar section: components --- +import './examples/Toolbar/Toolbar.css'; import { Fragment, useState } from 'react'; import PauseIcon from '@patternfly/react-icons/dist/esm/icons/pause-icon'; import PlayIcon from '@patternfly/react-icons/dist/esm/icons/play-icon'; @@ -12,6 +13,8 @@ import DownloadIcon from '@patternfly/react-icons/dist/esm/icons/download-icon'; import CogIcon from '@patternfly/react-icons/dist/esm/icons/cog-icon'; import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon'; import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon'; +import FilterIcon from '@patternfly/react-icons/dist/esm/icons/filter-icon'; +import BarsIcon from '@patternfly/react-icons/dist/esm/icons/bars-icon'; import { DashboardWrapper } from '@patternfly/react-core/dist/js/demos/DashboardWrapper'; ## Demos @@ -22,3 +25,18 @@ This is an example of toolbar usage in log viewer. ```ts file="examples/Toolbar/ConsoleLogViewerToolbar.tsx" isFullscreen ``` + +### Toolbar with container queries in sidebar + +This demo shows a toolbar using container queries inside a collapsible sidebar. The sidebar toolbar responds to the sidebar width, while the main content toolbar responds to the viewport width. This demonstrates how container queries enable independent responsive behavior for toolbars in different contexts. + +```ts file="examples/Toolbar/ToolbarInSidebar.tsx" isFullscreen +``` + +### Toolbar with container queries in modal + +This demo shows how container queries enable toolbars to work responsively inside modal dialogs. The toolbar adapts to the modal's width rather than the viewport width, making it work correctly regardless of the modal size. + +```ts file="examples/Toolbar/ToolbarInModal.tsx" isFullscreen +``` + diff --git a/packages/react-core/src/demos/examples/Toolbar/Toolbar.css b/packages/react-core/src/demos/examples/Toolbar/Toolbar.css new file mode 100644 index 00000000000..91ffc12c8d3 --- /dev/null +++ b/packages/react-core/src/demos/examples/Toolbar/Toolbar.css @@ -0,0 +1,10 @@ +.ws-react-c-toolbar-resize-container { + resize: horizontal; + overflow: auto; + border: var(--pf-t--global--border--width--extra-strong) dashed var(--pf-t--global--border--color--default); + /* padding: var(--pf-t--global--spacer--md); */ + width: 800px; + min-width: 300px; + max-width: 100%; + height: 15em; +} diff --git a/packages/react-core/src/demos/examples/Toolbar/ToolbarComparison.tsx b/packages/react-core/src/demos/examples/Toolbar/ToolbarComparison.tsx new file mode 100644 index 00000000000..f264ee6c8ad --- /dev/null +++ b/packages/react-core/src/demos/examples/Toolbar/ToolbarComparison.tsx @@ -0,0 +1,159 @@ +import { Fragment, useState } from 'react'; +import { + MenuToggle, + MenuToggleElement, + Toolbar, + ToolbarItem, + ToolbarContent, + ToolbarToggleGroup, + ToolbarGroup, + SearchInput, + Select, + SelectList, + SelectOption, + Title, + Card, + CardBody, + Stack, + StackItem, + CardHeader +} from '@patternfly/react-core'; +import FilterIcon from '@patternfly/react-icons/dist/esm/icons/filter-icon'; +import './Toolbar.css'; + +export const ToolbarComparison: React.FunctionComponent = () => { + // Media query toolbar state + const [mediaInputValue, setMediaInputValue] = useState(''); + const [mediaStatusExpanded, setMediaStatusExpanded] = useState(false); + const [mediaStatusSelected, setMediaStatusSelected] = useState(''); + + // Container query toolbar state + const [containerInputValue, setContainerInputValue] = useState(''); + const [containerStatusExpanded, setContainerStatusExpanded] = useState(false); + const [containerStatusSelected, setContainerStatusSelected] = useState(''); + + const statusOptions = ['Active', 'Inactive', 'Pending']; + + const createToolbarContent = ( + inputValue: string, + setInputValue: (value: string) => void, + statusExpanded: boolean, + setStatusExpanded: (value: boolean) => void, + statusSelected: string, + setStatusSelected: (value: string) => void, + label: string + ) => ( + + + setInputValue(value)} + value={inputValue} + onClear={() => setInputValue('')} + /> + + + + + + + + ); + + return ( +
+ + + + + + Viewport Media Queries (Default) + +

+ Collapses at lg viewport breakpoint +

+
+ +
+ + + } breakpoint="lg"> + {createToolbarContent( + mediaInputValue, + setMediaInputValue, + mediaStatusExpanded, + setMediaStatusExpanded, + mediaStatusSelected, + setMediaStatusSelected, + 'Media query' + )} + + + +
+
+
+
+ + + + + + Container Queries + +

+ Collapses at lg container breakpoint +

+
+ +
+ + + } breakpoint="lg"> + {createToolbarContent( + containerInputValue, + setContainerInputValue, + containerStatusExpanded, + setContainerStatusExpanded, + containerStatusSelected, + setContainerStatusSelected, + 'Container query' + )} + + + +
+
+
+
+
+
+ ); +}; diff --git a/packages/react-core/src/demos/examples/Toolbar/ToolbarInModal.tsx b/packages/react-core/src/demos/examples/Toolbar/ToolbarInModal.tsx new file mode 100644 index 00000000000..c8c19dfb8eb --- /dev/null +++ b/packages/react-core/src/demos/examples/Toolbar/ToolbarInModal.tsx @@ -0,0 +1,202 @@ +import { Fragment, useState } from 'react'; +import { + Button, + Modal, + ModalVariant, + ModalBody, + MenuToggle, + MenuToggleElement, + Toolbar, + ToolbarItem, + ToolbarContent, + ToolbarToggleGroup, + ToolbarGroup, + SearchInput, + Select, + SelectList, + SelectOption, + Title, + Card, + CardBody, + CardHeader, + CardFooter, + DataList, + DataListItem, + DataListItemRow, + DataListItemCells, + DataListCell, + ModalHeader +} from '@patternfly/react-core'; +import FilterIcon from '@patternfly/react-icons/dist/esm/icons/filter-icon'; + +export const ToolbarInModal: React.FunctionComponent = () => { + const [isModalOpen, setIsModalOpen] = useState(false); + const [inputValue, setInputValue] = useState(''); + const [statusExpanded, setStatusExpanded] = useState(false); + const [statusSelected, setStatusSelected] = useState(''); + const [priorityExpanded, setPriorityExpanded] = useState(false); + const [prioritySelected, setPrioritySelected] = useState(''); + + const statusOptions = ['Open', 'In Progress', 'Resolved', 'Closed']; + const priorityOptions = ['Low', 'Medium', 'High', 'Critical']; + + const handleModalToggle = () => { + setIsModalOpen(!isModalOpen); + }; + + const toolbarItems = ( + + + setInputValue(value)} + value={inputValue} + onClear={() => setInputValue('')} + /> + + + + + + + + + + + ); + + const sampleData = [ + { id: 'ISSUE-001', title: 'Login page not responsive on mobile', status: 'Open', priority: 'High' }, + { id: 'ISSUE-002', title: 'Dashboard widgets load slowly', status: 'In Progress', priority: 'Medium' }, + { id: 'ISSUE-003', title: 'Export CSV feature missing', status: 'Open', priority: 'Low' }, + { id: 'ISSUE-004', title: 'Search results pagination broken', status: 'Resolved', priority: 'Critical' }, + { id: 'ISSUE-005', title: 'User profile images not uploading', status: 'In Progress', priority: 'High' } + ]; + + return ( + <> + + + + Toolbar in Modal Demo + + + +

+ This demo shows a toolbar inside a modal dialog using container queries. The toolbar responds to the modal's + width rather than the viewport width, making it work correctly even when the modal is smaller than the + viewport. +

+
+ + + +
+ + + Close + + ]} + > + + + + } breakpoint="md"> + {toolbarItems} + + + + + + + {sampleData.map((item) => ( + + + + {item.id} + , + + {item.title} + , + + {item.status} + , + + {item.priority} + + ]} + /> + + + ))} + + + + + ); +}; diff --git a/packages/react-core/src/demos/examples/Toolbar/ToolbarInSidebar.tsx b/packages/react-core/src/demos/examples/Toolbar/ToolbarInSidebar.tsx new file mode 100644 index 00000000000..634a4083176 --- /dev/null +++ b/packages/react-core/src/demos/examples/Toolbar/ToolbarInSidebar.tsx @@ -0,0 +1,266 @@ +import { Fragment, useState } from 'react'; +import { + Page, + PageSidebar, + PageSidebarBody, + PageSection, + MenuToggle, + MenuToggleElement, + Toolbar, + ToolbarItem, + ToolbarContent, + ToolbarToggleGroup, + ToolbarGroup, + SearchInput, + Select, + SelectList, + SelectOption, + Title, + Card, + CardBody, + Masthead, + MastheadToggle, + MastheadMain, + MastheadBrand, + MastheadContent, + PageToggleButton +} from '@patternfly/react-core'; +import FilterIcon from '@patternfly/react-icons/dist/esm/icons/filter-icon'; +import BarsIcon from '@patternfly/react-icons/dist/esm/icons/bars-icon'; + +export const ToolbarInSidebar: React.FunctionComponent = () => { + const [isSidebarOpen, setIsSidebarOpen] = useState(true); + + // Sidebar toolbar state + const [sidebarInputValue, setSidebarInputValue] = useState(''); + const [sidebarCategoryExpanded, setSidebarCategoryExpanded] = useState(false); + const [sidebarCategorySelected, setSidebarCategorySelected] = useState(''); + + // Main content toolbar state + const [mainInputValue, setMainInputValue] = useState(''); + const [mainStatusExpanded, setMainStatusExpanded] = useState(false); + const [mainStatusSelected, setMainStatusSelected] = useState(''); + const [mainRiskExpanded, setMainRiskExpanded] = useState(false); + const [mainRiskSelected, setMainRiskSelected] = useState(''); + + const categoryOptions = ['Documentation', 'Tutorials', 'Examples']; + const statusOptions = ['New', 'Pending', 'Running', 'Cancelled']; + const riskOptions = ['Low', 'Medium', 'High']; + + const Header = ( + + + setIsSidebarOpen(!isSidebarOpen)} + > + + + + + + + Container Query Demo + + + + + + Sidebar: {isSidebarOpen ? 'Open' : 'Closed'} + + + + ); + + const sidebarToolbarItems = ( + + + setSidebarInputValue(value)} + value={sidebarInputValue} + onClear={() => setSidebarInputValue('')} + /> + + + + + + + + ); + + const Sidebar = ( + + +
+ + Sidebar Filters + +

+ This toolbar uses container queries and responds to the sidebar width, not the viewport. +

+ + + } breakpoint="md"> + {sidebarToolbarItems} + + + +
+
+
+ ); + + const mainToolbarItems = ( + + + setMainInputValue(value)} + value={mainInputValue} + onClear={() => setMainInputValue('')} + /> + + + + + + + + + + + ); + + return ( + + + + + + Main Content Filters + +

+ This toolbar uses viewport media queries (default) and responds to the browser width. +

+ + + } breakpoint="lg"> + {mainToolbarItems} + + + +
+
+ + + + How to test + +
    +
  • Toggle the sidebar open/closed using the hamburger menu
  • +
  • The sidebar toolbar collapses based on sidebar width (container queries)
  • +
  • The main content toolbar collapses based on viewport width (media queries)
  • +
  • Resize your browser to see the main toolbar collapse independently
  • +
+
+
+
+
+ ); +};