Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Toolbar): allow multiple toggle groups #9329

Merged
merged 6 commits into from
Sep 1, 2023
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
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
Loading