Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/react-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"tslib": "^2.8.1"
},
"devDependencies": {
"@patternfly/patternfly": "6.6.0-prerelease.50",
"@patternfly/patternfly": "6.7.0-prerelease.1",
"case-anything": "^3.1.2",
"css": "^3.0.0",
"fs-extra": "^11.3.3"
Expand Down
177 changes: 120 additions & 57 deletions packages/react-core/src/components/Toolbar/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -34,6 +35,8 @@ export interface ToolbarProps extends React.HTMLProps<HTMLDivElement>, OUIAProps
children?: React.ReactNode;
/** Id of the data toolbar */
id?: string;
/** Flag indicating if the toolbar is a container for CSS container queries */
isContainer?: boolean;
/** Flag indicating the toolbar height should expand to the full height of the container */
isFullHeight?: boolean;
/** Flag indicating the toolbar is static */
Expand Down Expand Up @@ -84,6 +87,9 @@ interface FilterInfo {
class Toolbar extends Component<ToolbarProps, ToolbarState> {
static displayName = 'Toolbar';
labelGroupContentRef = createRef<HTMLDivElement>();
toolbarRef = createRef<HTMLDivElement>();
resizeObserver: () => void = () => {};
containerBreakpoint: ReturnType<typeof getBreakpoint>;
staticFilterInfo = {};
hasNoPadding = false;
state = {
Expand All @@ -100,28 +106,67 @@ class Toolbar extends Component<ToolbarProps, ToolbarState> {
}));
};

closeExpandableContent = (e: any) => {
if (e.target.innerWidth !== this.state.windowWidth) {
closeExpandableContent = (e?: any) => {
if (e && e.target.innerWidth !== this.state.windowWidth) {
this.setState(() => ({
isManagedToggleExpanded: false,
windowWidth: e.target.innerWidth
}));
}
};

closeExpandableContentOnContainerResize = () => {
const containerWidth = this.toolbarRef.current?.clientWidth;
if (!containerWidth) {
return;
}

const breakpoint = getBreakpoint(containerWidth);
const isInitialMeasurement = this.containerBreakpoint === undefined;
if (breakpoint !== this.containerBreakpoint) {
this.containerBreakpoint = breakpoint;
if (!isInitialMeasurement && this.state.isManagedToggleExpanded) {
this.setState({ isManagedToggleExpanded: false });
}
}
};

setupResizeHandling = () => {
if (!this.isToggleManaged() || !canUseDOM) {
return;
}

this.containerBreakpoint = undefined;
const reference = this.props.isContainer ? this.toolbarRef.current : undefined;
const handler = this.props.isContainer ? this.closeExpandableContentOnContainerResize : this.closeExpandableContent;

this.resizeObserver = getResizeObserver(reference, handler, true);
};

cleanupResizeHandling = () => {
this.resizeObserver();
this.resizeObserver = () => {};
};

componentDidMount() {
if (canUseDOM) {
this.setState({ windowWidth: window.innerWidth });
}
if (this.isToggleManaged() && canUseDOM) {
window.addEventListener('resize', this.closeExpandableContent);
this.setupResizeHandling();
}

componentDidUpdate(prevProps: ToolbarProps) {
const wasToggleManaged = !(prevProps.isExpanded || !!prevProps.toggleIsExpanded);
const isToggleManaged = this.isToggleManaged();

if (prevProps.isContainer !== this.props.isContainer || wasToggleManaged !== isToggleManaged) {
this.cleanupResizeHandling();
this.setupResizeHandling();
}
}

componentWillUnmount() {
if (this.isToggleManaged() && canUseDOM) {
window.removeEventListener('resize', this.closeExpandableContent);
}
this.cleanupResizeHandling();
}

updateNumberFilters = (categoryName: string, numberOfFilters: number) => {
Expand All @@ -146,6 +191,7 @@ class Toolbar extends Component<ToolbarProps, ToolbarState> {
toggleIsExpanded,
className,
children,
isContainer,
isFullHeight,
isStatic,
isStickyBase,
Expand All @@ -169,56 +215,73 @@ class Toolbar extends Component<ToolbarProps, ToolbarState> {

return (
<PageContext.Consumer>
{({ width, getBreakpoint }) => (
<div
className={css(
styles.toolbar,
hasNoPadding && styles.modifiers.noPadding,
isFullHeight && styles.modifiers.fullHeight,
isStatic && styles.modifiers.static,
isSticky && styles.modifiers.sticky,
isStickyBase && styles.modifiers.stickyBase,
isStickyStuck && styles.modifiers.stickyStuck,
isVertical && styles.modifiers.vertical,
formatBreakpointMods(inset, styles, '', getBreakpoint(width)),
colorVariant === 'primary' && styles.modifiers.primary,
colorVariant === 'secondary' && styles.modifiers.secondary,
colorVariant === 'no-background' && styles.modifiers.noBackground,
className
)}
id={randomId}
{...getOUIAProps(Toolbar.displayName, ouiaId !== undefined ? ouiaId : generatedOuiaId)}
{...props}
>
<ToolbarContext.Provider
value={{
isExpanded,
toggleIsExpanded: isToggleManaged ? this.toggleIsExpanded : toggleIsExpanded,
labelGroupContentRef: this.labelGroupContentRef,
updateNumberFilters: this.updateNumberFilters,
numberOfFilters,
clearAllFilters,
clearFiltersButtonText,
showClearFiltersButton,
toolbarId: randomId,
customLabelGroupContent
}}
{(pageContext) => {
const toolbarContent = (
<div
className={css(
styles.toolbar,
hasNoPadding && styles.modifiers.noPadding,
isContainer && styles.modifiers.container,
isFullHeight && styles.modifiers.fullHeight,
isStatic && styles.modifiers.static,
isSticky && styles.modifiers.sticky,
isStickyBase && styles.modifiers.stickyBase,
isStickyStuck && styles.modifiers.stickyStuck,
isVertical && styles.modifiers.vertical,
formatBreakpointMods(
inset,
styles,
'',
!isContainer ? pageContext.getBreakpoint(pageContext.width) : undefined
),
colorVariant === 'primary' && styles.modifiers.primary,
colorVariant === 'secondary' && styles.modifiers.secondary,
colorVariant === 'no-background' && styles.modifiers.noBackground,
className
)}
id={randomId}
ref={this.toolbarRef}
{...getOUIAProps(Toolbar.displayName, ouiaId !== undefined ? ouiaId : generatedOuiaId)}
{...props}
>
{children}
<ToolbarLabelGroupContent
isExpanded={isExpanded}
labelGroupContentRef={this.labelGroupContentRef}
clearAllFilters={clearAllFilters}
showClearFiltersButton={showClearFiltersButton}
clearFiltersButtonText={clearFiltersButtonText}
numberOfFilters={numberOfFilters}
numberOfFiltersText={numberOfFiltersText}
collapseListedFiltersBreakpoint={collapseListedFiltersBreakpoint}
customLabelGroupContent={customLabelGroupContent}
/>
</ToolbarContext.Provider>
</div>
)}
<ToolbarContext.Provider
value={{
isExpanded,
toggleIsExpanded: isToggleManaged ? this.toggleIsExpanded : toggleIsExpanded,
labelGroupContentRef: this.labelGroupContentRef,
updateNumberFilters: this.updateNumberFilters,
numberOfFilters,
clearAllFilters,
clearFiltersButtonText,
showClearFiltersButton,
toolbarId: randomId,
customLabelGroupContent,
isContainer,
toolbarRef: this.toolbarRef
}}
>
{children}
<ToolbarLabelGroupContent
isExpanded={isExpanded}
labelGroupContentRef={this.labelGroupContentRef}
clearAllFilters={clearAllFilters}
showClearFiltersButton={showClearFiltersButton}
clearFiltersButtonText={clearFiltersButtonText}
numberOfFilters={numberOfFilters}
numberOfFiltersText={numberOfFiltersText}
collapseListedFiltersBreakpoint={collapseListedFiltersBreakpoint}
customLabelGroupContent={customLabelGroupContent}
/>
</ToolbarContext.Provider>
</div>
);

return isContainer ? (
<PageContext.Provider value={{ ...pageContext, width: null }}>{toolbarContent}</PageContext.Provider>
) : (
toolbarContent
);
}}
</PageContext.Consumer>
);
};
Expand Down
1 change: 1 addition & 0 deletions packages/react-core/src/components/Toolbar/ToolbarItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface ToolbarItemProps extends React.HTMLProps<HTMLDivElement> {
/** Visibility at various width breakpoints. */
visibility?: {
default?: 'hidden' | 'visible';
sm?: 'hidden' | 'visible';
md?: 'hidden' | 'visible';
lg?: 'hidden' | 'visible';
xl?: 'hidden' | 'visible';
Expand Down
19 changes: 10 additions & 9 deletions packages/react-core/src/components/Toolbar/ToolbarToggleGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@ import * as ReactDOM from 'react-dom';
import styles from '@patternfly/react-styles/css/components/Toolbar/toolbar';
import { css } from '@patternfly/react-styles';
import { ToolbarGroupProps } from './ToolbarGroup';
import { ToolbarContext, ToolbarContentContext } from './ToolbarUtils';
import { globalBreakpoints, 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';
import { PageContext } from '../Page/PageContext';

export interface ToolbarToggleGroupProps extends ToolbarGroupProps {
/** Flag indicating when toggle group is expanded for non-managed toolbar toggle groups. */
Expand Down Expand Up @@ -156,10 +155,9 @@ class ToolbarToggleGroup extends Component<ToolbarToggleGroupProps> {
toggleRef = createRef<HTMLButtonElement>();
expandableContentRef = createRef<HTMLDivElement>();

isContentPopup = () => {
const viewportSize = typeof window !== 'undefined' ? window.innerWidth : 1200;
const lgBreakpointValue = parseInt(globalBreakpointLg.value);
return viewportSize < lgBreakpointValue;
isContentPopup = (width?: number) => {
const size = width ?? (typeof window !== 'undefined' ? window.innerWidth : 1200);
return size < globalBreakpoints.lg;
};

render() {
Expand Down Expand Up @@ -191,7 +189,7 @@ class ToolbarToggleGroup extends Component<ToolbarToggleGroupProps> {
<PageContext.Consumer>
{({ width, getBreakpoint }) => (
<ToolbarContext.Consumer>
{({ toggleIsExpanded: managedOnToggle }) => {
{({ toggleIsExpanded: managedOnToggle, isContainer, toolbarRef }) => {
const _onToggle = onToggle !== undefined ? onToggle : managedOnToggle;

return (
Expand Down Expand Up @@ -238,7 +236,10 @@ class ToolbarToggleGroup extends Component<ToolbarToggleGroupProps> {
onClick={_onToggle}
aria-label="Show Filters"
{...(_isExpanded && { 'aria-expanded': true })}
aria-haspopup={_isExpanded && this.isContentPopup()}
aria-haspopup={
_isExpanded &&
this.isContentPopup(isContainer ? toolbarRef?.current?.clientWidth : undefined)
}
aria-controls={_isExpanded ? expandableContentId : undefined}
ref={this.toggleRef}
icon={toggleIcon}
Expand Down
4 changes: 4 additions & 0 deletions packages/react-core/src/components/Toolbar/ToolbarUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export interface ToolbarContextProps {
showClearFiltersButton?: boolean;
toolbarId?: string;
customLabelGroupContent?: React.ReactNode;
/** Flag indicating if the toolbar is a container */
isContainer?: boolean;
/** Ref to the toolbar root element */
toolbarRef?: RefObject<HTMLDivElement | null>;
}

export const ToolbarContext = createContext<ToolbarContextProps>({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(<Toolbar isContainer data-testid="toolbar-container" />);

expect(screen.getByTestId('toolbar-container')).toHaveClass(styles.modifiers.container);
});

it('renders all responsive visibility modifiers', () => {
render(<ToolbarItem data-testid="toolbar-item" visibility={{ md: 'hidden', xl: 'visible' }} />);

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(<ToolbarContent data-testid="toolbar-content" rowWrap={{ md: 'nowrap', xl: 'wrap' }} />);

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(
<Toolbar>
<ToolbarContent>
<ToolbarToggleGroup data-testid="toolbar-toggle-group" breakpoint="xl" toggleIcon={<span />}>
Content
</ToolbarToggleGroup>
</ToolbarContent>
</Toolbar>
);

expect(screen.getByTestId('toolbar-toggle-group')).toHaveClass('pf-m-show-on-xl');
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Fragment } from 'react';
import { render } from '@testing-library/react';
import { fireEvent, render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { ToolbarToggleGroup } from '../ToolbarToggleGroup';
import { Toolbar } from '../Toolbar';
import { ToolbarContent } from '../ToolbarContent';
Expand All @@ -25,4 +26,24 @@ describe('ToolbarToggleGroup', () => {

expect(myMock).toHaveBeenCalled();
});

it('sets aria-haspopup when expandable content is a popup in a narrow toolbar container', () => {
const clientWidthMock = jest.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockReturnValue(600);

render(
<Toolbar isContainer>
<ToolbarContent>
<ToolbarToggleGroup breakpoint="lg" toggleIcon={<span />}>
Filter controls
</ToolbarToggleGroup>
</ToolbarContent>
</Toolbar>
);

const toggle = screen.getByRole('button', { name: 'Show Filters' });
fireEvent.click(toggle);

expect(toggle).toHaveAttribute('aria-haspopup', 'true');
clientWidthMock.mockRestore();
});
});
Loading
Loading