Skip to content

Commit

Permalink
feat(Toolbar): allow multiple toggle groups (#9329)
Browse files Browse the repository at this point in the history
* feat(Toolbar): allow multiple toggle groups

* remove dupe id

* multiple groups second pass

* toggle group with poppers

* swap back to createPortal

* update integration
  • Loading branch information
kmcfaul authored Sep 1, 2023
1 parent 12ac5fc commit b1093bf
Show file tree
Hide file tree
Showing 13 changed files with 413 additions and 153 deletions.
18 changes: 7 additions & 11 deletions packages/react-core/src/components/Toolbar/ToolbarContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import styles from '@patternfly/react-styles/css/components/Toolbar/toolbar';
import { css } from '@patternfly/react-styles';
import { ToolbarContentContext, ToolbarContext } from './ToolbarUtils';
import { formatBreakpointMods } from '../../helpers/util';
import { ToolbarExpandableContent } from './ToolbarExpandableContent';
import { PageContext } from '../Page/PageContext';

export interface ToolbarContentProps extends React.HTMLProps<HTMLDivElement> {
Expand Down Expand Up @@ -70,13 +69,15 @@ class ToolbarContent extends React.Component<ToolbarContentProps> {
formatBreakpointMods(visibility, styles, '', getBreakpoint(width)),
className
)}
ref={this.expandableContentRef}
{...props}
>
<ToolbarContext.Consumer>
{({
clearAllFilters: clearAllFiltersContext,
clearFiltersButtonText: clearFiltersButtonContext,
showClearFiltersButton: showClearFiltersButtonContext,
isExpanded: isExpandedContext,
toolbarId: toolbarIdContext
}) => {
const expandableContentId = `${
Expand All @@ -87,7 +88,11 @@ class ToolbarContent extends React.Component<ToolbarContentProps> {
value={{
expandableContentRef: this.expandableContentRef,
expandableContentId,
chipContainerRef: this.chipContainerRef
chipContainerRef: this.chipContainerRef,
isExpanded: isExpanded || isExpandedContext,
clearAllFilters: clearAllFilters || clearAllFiltersContext,
clearFiltersButtonText: clearFiltersButtonText || clearFiltersButtonContext,
showClearFiltersButton: showClearFiltersButton || showClearFiltersButtonContext
}}
>
<div
Expand All @@ -103,15 +108,6 @@ class ToolbarContent extends React.Component<ToolbarContentProps> {
>
{children}
</div>
<ToolbarExpandableContent
id={expandableContentId}
isExpanded={isExpanded}
expandableContentRef={this.expandableContentRef}
chipContainerRef={this.chipContainerRef}
clearAllFilters={clearAllFilters || clearAllFiltersContext}
showClearFiltersButton={showClearFiltersButton || showClearFiltersButtonContext}
clearFiltersButtonText={clearFiltersButtonText || clearFiltersButtonContext}
/>
</ToolbarContentContext.Provider>
);
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ class ToolbarExpandableContent extends React.Component<ToolbarExpandableContentP

render() {
const {
children,
className,
expandableContentRef,
chipContainerRef,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
isExpanded,
clearAllFilters,
clearFiltersButtonText,
Expand All @@ -54,8 +54,12 @@ class ToolbarExpandableContent extends React.Component<ToolbarExpandableContentP
};

return (
<div className={css(styles.toolbarExpandableContent, className)} ref={expandableContentRef} {...props}>
<ToolbarGroup />
<div
className={css(styles.toolbarExpandableContent, isExpanded && styles.modifiers.expanded, className)}
ref={expandableContentRef}
{...props}
>
<ToolbarGroup>{children}</ToolbarGroup>
{numberOfFilters > 0 && (
<ToolbarGroup className={styles.modifiers.chipContainer}>
<ToolbarGroup ref={chipContainerRef} />
Expand Down
14 changes: 12 additions & 2 deletions packages/react-core/src/components/Toolbar/ToolbarFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export interface ToolbarChip {
}

export interface ToolbarFilterProps extends ToolbarItemProps {
/** Flag indicating when toolbar toggle group is expanded for non-managed toolbar toggle groups. */
isExpanded?: boolean;
/** An array of strings to be displayed as chips in the expandable content */
chips?: (string | ToolbarChip)[];
/** Callback passed by consumer used to close the entire chip group */
Expand All @@ -37,6 +39,8 @@ export interface ToolbarFilterProps extends ToolbarItemProps {
categoryName: string | ToolbarChipGroup;
/** Flag to show the toolbar item */
showToolbarItem?: boolean;
/** Reference to a chip container created with a custom expandable content group, for non-managed multiple toolbar toggle groups. */
expandableChipContainerRef?: React.RefObject<HTMLDivElement>;
}

interface ToolbarFilterState {
Expand Down Expand Up @@ -90,9 +94,12 @@ class ToolbarFilter extends React.Component<ToolbarFilterProps, ToolbarFilterSta
chipGroupCollapsedText,
categoryName,
showToolbarItem,
isExpanded,
expandableChipContainerRef,
...props
} = this.props;
const { isExpanded, chipGroupContentRef } = this.context;
const { isExpanded: managedIsExpanded, chipGroupContentRef } = this.context;
const _isExpanded = isExpanded !== undefined ? isExpanded : managedIsExpanded;
const categoryKey =
typeof categoryName !== 'string' && categoryName.hasOwnProperty('key')
? categoryName.key
Expand Down Expand Up @@ -123,7 +130,7 @@ class ToolbarFilter extends React.Component<ToolbarFilterProps, ToolbarFilterSta
</ToolbarItem>
) : null;

if (!isExpanded && this.state.isMounted) {
if (!_isExpanded && this.state.isMounted) {
return (
<React.Fragment>
{showToolbarItem && <ToolbarItem {...props}>{children}</ToolbarItem>}
Expand All @@ -138,6 +145,9 @@ class ToolbarFilter extends React.Component<ToolbarFilterProps, ToolbarFilterSta
<React.Fragment>
{showToolbarItem && <ToolbarItem {...props}>{children}</ToolbarItem>}
{chipContainerRef.current && ReactDOM.createPortal(chipGroup, chipContainerRef.current)}
{expandableChipContainerRef &&
expandableChipContainerRef.current &&
ReactDOM.createPortal(chipGroup, expandableChipContainerRef.current)}
</React.Fragment>
)}
</ToolbarContentContext.Consumer>
Expand Down
139 changes: 92 additions & 47 deletions packages/react-core/src/components/Toolbar/ToolbarToggleGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ import { Button } from '../Button';
import globalBreakpointLg from '@patternfly/react-tokens/dist/esm/global_breakpoint_lg';
import { formatBreakpointMods, toCamel, canUseDOM } from '../../helpers/util';
import { PageContext } from '../Page/PageContext';
import { ToolbarExpandableContent } from './ToolbarExpandableContent';

export interface ToolbarToggleGroupProps extends ToolbarGroupProps {
/** Flag indicating when toggle group is expanded for non-managed toolbar toggle groups. */
isExpanded?: boolean;
/** Callback for toggle group click event for non-managed toolbar toggle groups. */
onToggle?: (event: React.MouseEvent) => void;
/** An icon to be rendered when the toggle group has collapsed down */
toggleIcon: React.ReactNode;
/** Controls when filters are shown and when the toggle button is hidden. */
Expand Down Expand Up @@ -46,10 +51,21 @@ export interface ToolbarToggleGroupProps extends ToolbarGroupProps {
xl?: 'spaceItemsNone' | 'spaceItemsSm' | 'spaceItemsMd' | 'spaceItemsLg';
'2xl'?: 'spaceItemsNone' | 'spaceItemsSm' | 'spaceItemsMd' | 'spaceItemsLg';
};
/** Reference to a chip container group for filters inside the toolbar toggle group */
chipContainerRef?: React.RefObject<any>;
/** Optional callback for clearing all filters in the toolbar toggle group */
clearAllFilters?: () => void;
/** Flag indicating that the clear all filters button should be visible in the toolbar toggle group */
showClearFiltersButton?: boolean;
/** Text to display in the clear all filters button of the toolbar toggle group */
clearFiltersButtonText?: string;
}

class ToolbarToggleGroup extends React.Component<ToolbarToggleGroupProps> {
static displayName = 'ToolbarToggleGroup';
toggleRef = React.createRef<HTMLButtonElement>();
expandableContentRef = React.createRef<HTMLDivElement>();

isContentPopup = () => {
const viewportSize = canUseDOM ? window.innerWidth : 1200;
const lgBreakpointValue = parseInt(globalBreakpointLg.value);
Expand All @@ -67,6 +83,12 @@ class ToolbarToggleGroup extends React.Component<ToolbarToggleGroupProps> {
spaceItems,
className,
children,
isExpanded,
onToggle,
chipContainerRef,
clearAllFilters,
showClearFiltersButton,
clearFiltersButtonText,
...props
} = this.props;

Expand All @@ -79,64 +101,87 @@ class ToolbarToggleGroup extends React.Component<ToolbarToggleGroupProps> {
<PageContext.Consumer>
{({ width, getBreakpoint }) => (
<ToolbarContext.Consumer>
{({ isExpanded, toggleIsExpanded }) => (
<ToolbarContentContext.Consumer>
{({ expandableContentRef, expandableContentId }) => {
if (expandableContentRef.current && expandableContentRef.current.classList) {
if (isExpanded) {
expandableContentRef.current.classList.add(styles.modifiers.expanded);
} else {
expandableContentRef.current.classList.remove(styles.modifiers.expanded);
}
}
{({ toggleIsExpanded: managedOnToggle }) => {
const _onToggle = onToggle !== undefined ? onToggle : managedOnToggle;

return (
<ToolbarContentContext.Consumer>
{({
expandableContentRef,
expandableContentId,
chipContainerRef: managedChipContainerRef,
isExpanded: managedIsExpanded,
clearAllFilters: clearAllFiltersContext,
clearFiltersButtonText: clearFiltersButtonContext,
showClearFiltersButton: showClearFiltersButtonContext
}) => {
const _isExpanded = isExpanded !== undefined ? isExpanded : managedIsExpanded;
const _chipContainerRef =
chipContainerRef !== undefined ? chipContainerRef : managedChipContainerRef;

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 = (
<ToolbarExpandableContent
id={expandableContentId}
expandableContentRef={this.expandableContentRef}
isExpanded={_isExpanded}
clearAllFilters={clearAllFilters || clearAllFiltersContext}
showClearFiltersButton={showClearFiltersButton || showClearFiltersButtonContext}
clearFiltersButtonText={clearFiltersButtonText || clearFiltersButtonContext}
chipContainerRef={_chipContainerRef}
>
{children}
</ToolbarExpandableContent>
);

return (
<div
className={css(
styles.toolbarGroup,
styles.modifiers.toggleGroup,
variant &&
styles.modifiers[toCamel(variant) as 'filterGroup' | 'iconButtonGroup' | 'buttonGroup'],
formatBreakpointMods(breakpointMod, styles, '', getBreakpoint(width)),
formatBreakpointMods(visibility, styles, '', getBreakpoint(width)),
formatBreakpointMods(alignment, styles, '', getBreakpoint(width)),
formatBreakpointMods(spacer, styles, '', getBreakpoint(width)),
formatBreakpointMods(spaceItems, styles, '', getBreakpoint(width)),
className
)}
{...props}
>
const toggleButton = (
<div className={css(styles.toolbarToggle)}>
<Button
variant="plain"
onClick={toggleIsExpanded}
onClick={_onToggle}
aria-label="Show Filters"
{...(isExpanded && { 'aria-expanded': true })}
aria-haspopup={isExpanded && this.isContentPopup()}
{...(_isExpanded && { 'aria-expanded': true })}
aria-haspopup={_isExpanded && this.isContentPopup()}
aria-controls={expandableContentId}
ref={this.toggleRef}
>
{toggleIcon}
</Button>
</div>
{isExpanded
? (ReactDOM.createPortal(
children,
expandableContentRef.current.firstElementChild
) as React.ReactElement)
: children}
</div>
);
}}
</ToolbarContentContext.Consumer>
)}
);

return (
<div
className={css(
styles.toolbarGroup,
styles.modifiers.toggleGroup,
variant &&
styles.modifiers[toCamel(variant) as 'filterGroup' | 'iconButtonGroup' | 'buttonGroup'],
formatBreakpointMods(breakpointMod, styles, '', getBreakpoint(width)),
formatBreakpointMods(visibility, styles, '', getBreakpoint(width)),
formatBreakpointMods(alignment, styles, '', getBreakpoint(width)),
formatBreakpointMods(spacer, styles, '', getBreakpoint(width)),
formatBreakpointMods(spaceItems, styles, '', getBreakpoint(width)),
className
)}
{...props}
>
{toggleButton}
{_isExpanded && ReactDOM.createPortal(expandableContent, expandableContentRef.current)}
{!_isExpanded && children}
</div>
);
}}
</ToolbarContentContext.Consumer>
);
}}
</ToolbarContext.Consumer>
)}
</PageContext.Consumer>
Expand Down
7 changes: 6 additions & 1 deletion packages/react-core/src/components/Toolbar/ToolbarUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,17 @@ interface ToolbarContentContextProps {
expandableContentRef: RefObject<HTMLDivElement>;
expandableContentId: string;
chipContainerRef: RefObject<any>;
isExpanded?: boolean;
clearAllFilters?: () => void;
clearFiltersButtonText?: string;
showClearFiltersButton?: boolean;
}

export const ToolbarContentContext = React.createContext<ToolbarContentContextProps>({
expandableContentRef: null,
expandableContentId: '',
chipContainerRef: null
chipContainerRef: null,
clearAllFilters: () => {}
});

export const globalBreakpoints = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,6 @@ exports[`ToolbarContent should match snapshot (auto-generated) 1`] = `
ReactNode
</div>
</div>
<div
class="pf-v5-c-toolbar__expandable-content"
id="string-expandable-content-0"
>
<div
class="pf-v5-c-toolbar__group"
/>
</div>
</div>
</DocumentFragment>
`;
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ describe('Toolbar', () => {
<ToolbarFilter
chips={['New', 'Pending']}
deleteChip={(category, chip) => {}}
deleteChipGroup={category => {}}
deleteChipGroup={(category) => {}}
categoryName="Status"
>
test content
Expand Down Expand Up @@ -102,8 +102,7 @@ describe('Toolbar', () => {
);

expect(asFragment()).toMatchSnapshot();
// Expecting 2 matches for text because the buttons also exist in hidden expandable content for mobile view
expect(screen.getAllByRole('button', { name: 'Save filters' }).length).toBe(2);
expect(screen.getAllByRole('button', { name: 'Clear all filters' }).length).toBe(2);
expect(screen.getAllByRole('button', { name: 'Save filters' }).length).toBe(1);
expect(screen.getAllByRole('button', { name: 'Clear all filters' }).length).toBe(1);
});
});
Loading

0 comments on commit b1093bf

Please sign in to comment.