Skip to content

Commit f21a95c

Browse files
committed
feat(toolbar): implement container query option
1 parent 2b36c61 commit f21a95c

File tree

13 files changed

+1065
-7
lines changed

13 files changed

+1065
-7
lines changed

.claude/settings.local.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(npm run lint)",
5+
"Bash(npm run lint:*)"
6+
]
7+
}
8+
}

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

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import { Component, createRef } from 'react';
22
import styles from '@patternfly/react-styles/css/components/Toolbar/toolbar';
33
import { GenerateId } from '../../helpers/GenerateId/GenerateId';
44
import { css } from '@patternfly/react-styles';
5-
import { ToolbarContext } from './ToolbarUtils';
5+
import { ToolbarContext, globalBreakpoints, containerBreakpoints } from './ToolbarUtils';
66
import { ToolbarLabelGroupContent } from './ToolbarLabelGroupContent';
77
import { formatBreakpointMods, canUseDOM } from '../../helpers/util';
88
import { getDefaultOUIAId, getOUIAProps, OUIAProps } from '../../helpers';
99
import { PageContext } from '../Page/PageContext';
10+
import { getResizeObserver } from '../../helpers/resizeObserver';
1011

1112
export enum ToolbarColorVariant {
1213
default = 'default',
@@ -59,6 +60,10 @@ export interface ToolbarProps extends React.HTMLProps<HTMLDivElement>, OUIAProps
5960
colorVariant?: ToolbarColorVariant | 'default' | 'no-background' | 'primary' | 'secondary';
6061
/** Flag indicating the toolbar padding is removed */
6162
hasNoPadding?: boolean;
63+
/** Use container queries instead of viewport media queries for responsive behavior */
64+
useContainerQuery?: boolean;
65+
/** Breakpoint for container queries. Only applies when useContainerQuery is true. */
66+
containerQueryBreakpoint?: 'sm' | 'md' | 'lg' | 'xl' | '2xl';
6267
}
6368

6469
export interface ToolbarState {
@@ -69,6 +74,8 @@ export interface ToolbarState {
6974
filterInfo: FilterInfo;
7075
/** Used to keep track of window width so we can collapse expanded content when window is resizing */
7176
windowWidth: number;
77+
/** Used to keep track of container width so we can collapse expanded content when container is resizing */
78+
containerWidth: number;
7279
ouiaStateId: string;
7380
}
7481

@@ -79,12 +86,15 @@ interface FilterInfo {
7986
class Toolbar extends Component<ToolbarProps, ToolbarState> {
8087
static displayName = 'Toolbar';
8188
labelGroupContentRef = createRef<HTMLDivElement>();
89+
toolbarRef = createRef<HTMLDivElement>();
90+
observer: any = () => {};
8291
staticFilterInfo = {};
8392
hasNoPadding = false;
8493
state = {
8594
isManagedToggleExpanded: false,
8695
filterInfo: {},
8796
windowWidth: canUseDOM ? window.innerWidth : 1200,
97+
containerWidth: 0,
8898
ouiaStateId: getDefaultOUIAId(Toolbar.displayName)
8999
};
90100

@@ -105,15 +115,58 @@ class Toolbar extends Component<ToolbarProps, ToolbarState> {
105115
}
106116
};
107117

118+
closeExpandableContentOnContainerResize = () => {
119+
if (this.toolbarRef.current && this.toolbarRef.current.clientWidth) {
120+
const newWidth = this.toolbarRef.current.clientWidth;
121+
122+
if (newWidth !== this.state.containerWidth) {
123+
// If expanded and container is wide enough for inline display at the specific breakpoint, close it
124+
const specificBreakpoint = this.props.containerQueryBreakpoint || 'lg';
125+
126+
// Use container breakpoints when using container queries, otherwise use global breakpoints
127+
let isWideEnoughForInline: boolean;
128+
if (this.props.useContainerQuery) {
129+
// Handle 'sm' case for container breakpoints
130+
const breakpointKey = specificBreakpoint === 'sm' ? 'sm' : specificBreakpoint;
131+
isWideEnoughForInline = newWidth >= containerBreakpoints[breakpointKey];
132+
} else {
133+
// Handle 'sm' case - map to 'md' since globalBreakpoints doesn't have 'sm'
134+
const breakpointKey = specificBreakpoint === 'sm' ? 'md' : specificBreakpoint;
135+
isWideEnoughForInline = newWidth >= globalBreakpoints[breakpointKey];
136+
}
137+
138+
if (this.state.isManagedToggleExpanded && isWideEnoughForInline) {
139+
this.setState(() => ({
140+
isManagedToggleExpanded: false,
141+
containerWidth: newWidth
142+
}));
143+
} else {
144+
// Just update width without closing
145+
this.setState(() => ({
146+
containerWidth: newWidth
147+
}));
148+
}
149+
}
150+
}
151+
};
152+
108153
componentDidMount() {
109154
if (this.isToggleManaged() && canUseDOM) {
110-
window.addEventListener('resize', this.closeExpandableContent);
155+
if (this.props.useContainerQuery && this.toolbarRef.current) {
156+
this.observer = getResizeObserver(this.toolbarRef.current, this.closeExpandableContentOnContainerResize, true);
157+
} else {
158+
window.addEventListener('resize', this.closeExpandableContent);
159+
}
111160
}
112161
}
113162

114163
componentWillUnmount() {
115164
if (this.isToggleManaged() && canUseDOM) {
116-
window.removeEventListener('resize', this.closeExpandableContent);
165+
if (this.props.useContainerQuery) {
166+
this.observer();
167+
} else {
168+
window.removeEventListener('resize', this.closeExpandableContent);
169+
}
117170
}
118171
}
119172

@@ -147,6 +200,8 @@ class Toolbar extends Component<ToolbarProps, ToolbarState> {
147200
numberOfFiltersText,
148201
customLabelGroupContent,
149202
colorVariant = ToolbarColorVariant.default,
203+
useContainerQuery,
204+
containerQueryBreakpoint,
150205
...props
151206
} = this.props;
152207

@@ -167,13 +222,27 @@ class Toolbar extends Component<ToolbarProps, ToolbarState> {
167222
isFullHeight && styles.modifiers.fullHeight,
168223
isStatic && styles.modifiers.static,
169224
isSticky && styles.modifiers.sticky,
225+
useContainerQuery && !containerQueryBreakpoint && styles.modifiers.container,
226+
useContainerQuery &&
227+
containerQueryBreakpoint &&
228+
((): string => {
229+
const breakpointClassMap: Record<string, string> = {
230+
'2xl': styles.modifiers.container_2xl,
231+
sm: styles.modifiers.containerSm,
232+
md: styles.modifiers.containerMd,
233+
lg: styles.modifiers.containerLg,
234+
xl: styles.modifiers.containerXl
235+
};
236+
return breakpointClassMap[containerQueryBreakpoint] || '';
237+
})(),
170238
formatBreakpointMods(inset, styles, '', getBreakpoint(width)),
171239
colorVariant === 'primary' && styles.modifiers.primary,
172240
colorVariant === 'secondary' && styles.modifiers.secondary,
173241
colorVariant === 'no-background' && styles.modifiers.noBackground,
174242
className
175243
)}
176244
id={randomId}
245+
ref={this.toolbarRef}
177246
{...getOUIAProps(Toolbar.displayName, ouiaId !== undefined ? ouiaId : this.state.ouiaStateId)}
178247
{...props}
179248
>
@@ -188,7 +257,8 @@ class Toolbar extends Component<ToolbarProps, ToolbarState> {
188257
clearFiltersButtonText,
189258
showClearFiltersButton,
190259
toolbarId: randomId,
191-
customLabelGroupContent
260+
customLabelGroupContent,
261+
useContainerQuery
192262
}}
193263
>
194264
{children}

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ class ToolbarToggleGroup extends Component<ToolbarToggleGroupProps> {
191191
<PageContext.Consumer>
192192
{({ width, getBreakpoint }) => (
193193
<ToolbarContext.Consumer>
194-
{({ toggleIsExpanded: managedOnToggle }) => {
194+
{({ toggleIsExpanded: managedOnToggle, useContainerQuery }) => {
195195
const _onToggle = onToggle !== undefined ? onToggle : managedOnToggle;
196196

197197
return (
@@ -260,7 +260,12 @@ class ToolbarToggleGroup extends Component<ToolbarToggleGroupProps> {
260260
| 'actionGroupPlain'
261261
| 'labelGroup'
262262
],
263-
formatBreakpointMods(breakpointMod, styles, '', getBreakpoint(width)),
263+
formatBreakpointMods(
264+
breakpointMod,
265+
styles,
266+
'',
267+
useContainerQuery ? undefined : getBreakpoint(width)
268+
),
264269
formatBreakpointMods(visibility, styles, '', getBreakpoint(width)),
265270
formatBreakpointMods(gap, styles, '', getBreakpoint(width)),
266271
formatBreakpointMods(columnGap, styles, '', getBreakpoint(width)),

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface ToolbarContextProps {
1515
showClearFiltersButton?: boolean;
1616
toolbarId?: string;
1717
customLabelGroupContent?: React.ReactNode;
18+
useContainerQuery?: boolean;
1819
}
1920

2021
export const ToolbarContext = createContext<ToolbarContextProps>({
@@ -23,7 +24,8 @@ export const ToolbarContext = createContext<ToolbarContextProps>({
2324
labelGroupContentRef: null,
2425
updateNumberFilters: () => {},
2526
numberOfFilters: 0,
26-
clearAllFilters: () => {}
27+
clearAllFilters: () => {},
28+
useContainerQuery: false
2729
});
2830

2931
interface ToolbarContentContextProps {
@@ -49,3 +51,12 @@ export const globalBreakpoints = {
4951
xl: parseInt(globalBreakpointXl.value) * 16,
5052
'2xl': parseInt(globalBreakpoint2xl.value) * 16
5153
};
54+
55+
// Container query breakpoints match CSS container query values
56+
export const containerBreakpoints = {
57+
sm: 286,
58+
md: 478,
59+
lg: 702,
60+
xl: 992, // You may need to verify this value
61+
'2xl': 1200 // You may need to verify this value
62+
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
.ws-react-c-toolbar-resize-container {
2+
resize: horizontal;
3+
overflow: auto;
4+
border: var(--pf-t--global--border--width--extra-strong) dashed var(--pf-t--global--border--color--default);
5+
/* padding: var(--pf-t--global--spacer--md); */
6+
width: 800px;
7+
min-width: 300px;
8+
max-width: 100%;
9+
height: 15em;
10+
}
11+

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ propComponents: ['Toolbar', 'ToolbarContent', 'ToolbarGroup', 'ToolbarItem', 'To
55
section: components
66
---
77

8+
import './Toolbar.css';
89
import { Fragment, useState } from 'react';
910

1011
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
113114

114115
```
115116

117+
## Examples with container queries
118+
119+
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.
120+
121+
### Basic container query usage
122+
123+
The toolbar adapts based on its container width instead of the viewport width. Resize the container to see the responsive behavior.
124+
125+
```ts file="./ToolbarContainerQueryBasic.tsx"
126+
127+
```
128+
129+
### Container query breakpoints
130+
131+
Use `containerQueryBreakpoint` to collapse the toolbar at different container widths.
132+
133+
```ts file="./ToolbarContainerQueryBreakpoints.tsx"
134+
135+
```
136+
116137
## Examples with spacers and wrapping
117138
You may adjust the space between toolbar items to arrange them into groups. Read our spacers documentation to learn more about using spacers.
118139

0 commit comments

Comments
 (0)