From 8a9ae66f0acea87c7a3820dd0bf331944b684528 Mon Sep 17 00:00:00 2001 From: Katie McFaul Date: Mon, 3 Jul 2023 15:09:24 -0400 Subject: [PATCH 1/6] feat(Toolbar): allow multiple toggle groups --- .../Toolbar/ToolbarExpandableContent.tsx | 7 +- .../src/components/Toolbar/ToolbarFilter.tsx | 8 +- .../components/Toolbar/ToolbarToggleGroup.tsx | 123 +++++----- .../src/components/Toolbar/examples/Test.tsx | 226 ++++++++++++++++++ .../components/Toolbar/examples/Toolbar.md | 18 +- 5 files changed, 318 insertions(+), 64 deletions(-) create mode 100644 packages/react-core/src/components/Toolbar/examples/Test.tsx diff --git a/packages/react-core/src/components/Toolbar/ToolbarExpandableContent.tsx b/packages/react-core/src/components/Toolbar/ToolbarExpandableContent.tsx index 463db0c5b29..a7d87059f68 100644 --- a/packages/react-core/src/components/Toolbar/ToolbarExpandableContent.tsx +++ b/packages/react-core/src/components/Toolbar/ToolbarExpandableContent.tsx @@ -40,7 +40,6 @@ class ToolbarExpandableContent extends React.Component +
{numberOfFilters > 0 && ( diff --git a/packages/react-core/src/components/Toolbar/ToolbarFilter.tsx b/packages/react-core/src/components/Toolbar/ToolbarFilter.tsx index 4c71e7ce245..dbded306250 100644 --- a/packages/react-core/src/components/Toolbar/ToolbarFilter.tsx +++ b/packages/react-core/src/components/Toolbar/ToolbarFilter.tsx @@ -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 */ @@ -90,9 +92,11 @@ class ToolbarFilter extends React.Component ) : null; - if (!isExpanded && this.state.isMounted) { + if (!_isExpanded && this.state.isMounted) { return ( {showToolbarItem && {children}} diff --git a/packages/react-core/src/components/Toolbar/ToolbarToggleGroup.tsx b/packages/react-core/src/components/Toolbar/ToolbarToggleGroup.tsx index 672419b75f5..212ea42b49e 100644 --- a/packages/react-core/src/components/Toolbar/ToolbarToggleGroup.tsx +++ b/packages/react-core/src/components/Toolbar/ToolbarToggleGroup.tsx @@ -10,6 +10,10 @@ import { formatBreakpointMods, toCamel, canUseDOM } from '../../helpers/util'; import { PageContext } from '../Page/PageContext'; 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. */ @@ -67,6 +71,8 @@ class ToolbarToggleGroup extends React.Component { spaceItems, className, children, + isExpanded, + onToggle, ...props } = this.props; @@ -79,64 +85,73 @@ class ToolbarToggleGroup extends React.Component { {({ width, getBreakpoint }) => ( - {({ isExpanded, toggleIsExpanded }) => ( - - {({ 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); + {({ isExpanded: managedIsExpanded, toggleIsExpanded: managedOnToggle }) => { + const _isExpanded = isExpanded !== undefined ? isExpanded : managedIsExpanded; + const _onToggle = onToggle !== undefined ? onToggle : managedOnToggle; + + return ( + + {({ expandableContentRef, expandableContentId }) => { + if ( + isExpanded === undefined && + expandableContentRef.current && + expandableContentRef.current.classList + ) { + if (_isExpanded) { + expandableContentRef.current.classList.add(styles.modifiers.expanded); + } else { + expandableContentRef.current.classList.remove(styles.modifiers.expanded); + } } - } - 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'; - return ( -
-
- + return ( +
+
+ +
+ {_isExpanded + ? (ReactDOM.createPortal( + children, + expandableContentRef.current.firstElementChild + ) as React.ReactElement) + : children}
- {isExpanded - ? (ReactDOM.createPortal( - children, - expandableContentRef.current.firstElementChild - ) as React.ReactElement) - : children} -
- ); - }} - - )} + ); + }} + + ); + }} )} diff --git a/packages/react-core/src/components/Toolbar/examples/Test.tsx b/packages/react-core/src/components/Toolbar/examples/Test.tsx new file mode 100644 index 00000000000..e17e0de90a1 --- /dev/null +++ b/packages/react-core/src/components/Toolbar/examples/Test.tsx @@ -0,0 +1,226 @@ +import React from 'react'; +import { + MenuToggle, + MenuToggleElement, + Toolbar, + ToolbarItem, + ToolbarContent, + ToolbarToggleGroup, + ToolbarGroup, + SearchInput, + Select, + SelectList, + SelectOption, + ToolbarFilter, + Badge +} from '@patternfly/react-core'; +import FilterIcon from '@patternfly/react-icons/dist/esm/icons/filter-icon'; + +export const Test: React.FunctionComponent = () => { + const [isExpanded, setIsExpanded] = React.useState(false); + const [isExpanded2, setIsExpanded2] = React.useState(false); + + const [inputValue, setInputValue] = React.useState(''); + const [filters, setFilters] = React.useState({ + risk: ['Low'], + status: ['New', 'Pending'] + }); + + const [isStatusExpanded, setIsStatusExpanded] = React.useState(false); + const [isRiskExpanded, setIsRiskExpanded] = React.useState(false); + + const onInputChange = (newValue: string) => { + setInputValue(newValue); + }; + + const onStatusToggle = () => { + setIsStatusExpanded(!isStatusExpanded); + }; + + const onRiskToggle = () => { + setIsRiskExpanded(!isRiskExpanded); + }; + + const onSelect = (type: string, event: React.MouseEvent | React.ChangeEvent | undefined, selection: string) => { + const checked = (event?.target as HTMLInputElement).checked; + setFilters((prev) => { + const prevSelections = prev[type]; + return { + ...prev, + [type]: checked ? [...prevSelections, selection] : prevSelections.filter((value) => value !== selection) + }; + }); + }; + + const onStatusSelect = (event: React.MouseEvent | React.ChangeEvent | undefined, selection: string) => { + onSelect('status', event, selection); + }; + + const onRiskSelect = (event: React.MouseEvent | React.ChangeEvent | undefined, selection: string) => { + onSelect('risk', event, selection); + }; + + const onDelete = (type: string, id: string) => { + if (type === 'Risk') { + setFilters({ risk: filters.risk.filter((fil: string) => fil !== id), status: filters.status }); + } else if (type === 'Status') { + setFilters({ risk: filters.risk, status: filters.status.filter((fil: string) => fil !== id) }); + } else { + setFilters({ risk: [], status: [] }); + } + }; + + const onDeleteGroup = (type: string) => { + if (type === 'Risk') { + setFilters({ risk: [], status: filters.status }); + } else if (type === 'Status') { + setFilters({ risk: filters.risk, status: [] }); + } + }; + + const statusMenuItems = ( + + + New + + + Pending + + + Running + + + Cancelled + + + ); + + const riskMenuItems = ( + + + Low + + + Medium + + + High + + + ); + + const toggleGroupItems = ( + + + onInputChange(value)} + value={inputValue} + onClear={() => { + onInputChange(''); + }} + /> + + + onDelete(category as string, chip as string)} + deleteChipGroup={(category) => onDeleteGroup(category as string)} + categoryName="Status" + isExpanded={isExpanded} + > + + + + + ); + + const toggleGroup2Items = ( + + onDelete(category as string, chip as string)} + categoryName="Risk" + isExpanded={isExpanded2} + > + + + + ); + + return ( + + + setIsExpanded(!isExpanded)} + toggleIcon={} + breakpoint="md" + > + {toggleGroupItems} + + setIsExpanded2(!isExpanded2)} + toggleIcon={} + breakpoint="xl" + > + {toggleGroup2Items} + + + + ); +}; diff --git a/packages/react-core/src/components/Toolbar/examples/Toolbar.md b/packages/react-core/src/components/Toolbar/examples/Toolbar.md index 6028f3d4e59..de3011cd7f3 100644 --- a/packages/react-core/src/components/Toolbar/examples/Toolbar.md +++ b/packages/react-core/src/components/Toolbar/examples/Toolbar.md @@ -14,6 +14,12 @@ import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-ico ## Examples +### TEST - remove before merging + +```ts file="./Test.tsx" + +``` + ### Toolbar items A toolbar can contain multiple toolbar items, like filters and buttons. @@ -28,7 +34,7 @@ Note: This example does not demonstrate responsive toolbar behavior. Responsive You may adjust the space between toolbar items to arrange them into groups. Read our spacers documentation to learn more about using spacers. -Items are spaced “16px” apart by default. To adjust the size of the space between items, use the `spacer` property of each ``. You can set the `spacer` value at multiple breakpoints, including "default", "md", "lg", "xl", and "2xl". Available `spacer` values include "spacerNone", "spacerSm", "spacerMd", or "spacerLg" into each breakpoint. +Items are spaced “16px” apart by default. To adjust the size of the space between items, use the `spacer` property of each ``. You can set the `spacer` value at multiple breakpoints, including "default", "md", "lg", "xl", and "2xl". Available `spacer` values include "spacerNone", "spacerSm", "spacerMd", or "spacerLg" into each breakpoint. ```ts file="./ToolbarSpacers.tsx" @@ -54,27 +60,28 @@ To adjust a toolbar’s inset, use the `inset` property. You can set the inset v ### Sticky toolbar -To lock a toolbar and prevent it from scrolling with other content, use a sticky toolbar. +To lock a toolbar and prevent it from scrolling with other content, use a sticky toolbar. In the following example, toggle the "is toolbar sticky" checkbox to see the difference between a sticky and non-sticky toolbar. ```ts file="./ToolbarSticky.tsx" + ``` ### With groups of items -You can group similar items together to create desired associations and to enable items to respond to changes in viewport width together. +You can group similar items together to create desired associations and to enable items to respond to changes in viewport width together. Note: This example does not demonstrate responsive toolbar behavior. Responsive toolbars are shown in the [examples with toggle groups and filters](/components/toolbar#examples-with-toggle-groups-and-filters). ```ts file="./ToolbarGroups.tsx" ``` + ## Examples with toggle groups and filters The following examples use toggle groups to allow for more responsive and complex toolbars with multiple items and groups of items. To visualize responsive toolbar behavior in the following examples, resize the browser to a smaller screen width. - ### Component managed toggle groups A toggle group allows you to collapse a set of items into an overlay panel at a certain breakpoint. For example, when a toggle group contains filter controls, its contents will collapse into an overlay panel when the toolbar adapts to a change in the viewport size. The contents can be toggled by selecting the filter icon in the overlay panel. @@ -102,7 +109,6 @@ You can add filters to a toolbar to let users filter the content that a toolbar The `` component expects applied filters and a delete chip handler to be passed in as properties. Pass in a `deleteChipGroup` property to close the entire chip group. Once close, the rendering of chips will be handled responsively by the toolbar. - ```ts file="./ToolbarWithFilters.tsx" ``` @@ -120,5 +126,5 @@ To customize the chip groups generated by toolbar filters, use the `customChipGr When all of a toolbar's required elements cannot fit in a single line, you can split toolbar items into multiple rows. ```ts file="./ToolbarStacked.tsx" -``` +``` From d290ab8e317e39e3cc656a19c982175a6a1b1400 Mon Sep 17 00:00:00 2001 From: Katie McFaul Date: Wed, 5 Jul 2023 10:46:43 -0400 Subject: [PATCH 2/6] remove dupe id --- packages/react-core/src/components/Toolbar/examples/Test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-core/src/components/Toolbar/examples/Test.tsx b/packages/react-core/src/components/Toolbar/examples/Test.tsx index e17e0de90a1..0d3f9f63eb5 100644 --- a/packages/react-core/src/components/Toolbar/examples/Test.tsx +++ b/packages/react-core/src/components/Toolbar/examples/Test.tsx @@ -202,7 +202,7 @@ export const Test: React.FunctionComponent = () => { ); return ( - + Date: Thu, 6 Jul 2023 10:55:40 -0400 Subject: [PATCH 3/6] multiple groups second pass --- .../src/components/Toolbar/ToolbarContent.tsx | 25 ++- .../src/components/Toolbar/ToolbarFilter.tsx | 6 + .../components/Toolbar/ToolbarToggleGroup.tsx | 19 +- .../src/components/Toolbar/examples/Test.tsx | 172 ++++++++++++------ .../src/components/Toolbar/index.ts | 1 + 5 files changed, 149 insertions(+), 74 deletions(-) diff --git a/packages/react-core/src/components/Toolbar/ToolbarContent.tsx b/packages/react-core/src/components/Toolbar/ToolbarContent.tsx index 40c5d44d05d..f6c21b25f92 100644 --- a/packages/react-core/src/components/Toolbar/ToolbarContent.tsx +++ b/packages/react-core/src/components/Toolbar/ToolbarContent.tsx @@ -33,6 +33,8 @@ export interface ToolbarContentProps extends React.HTMLProps { clearFiltersButtonText?: string; /** Id of the parent Toolbar component */ toolbarId?: string; + /** Custom expandable content for the toolbar, for non-managed multiple toolbar toggle groups. */ + expandableContent?: React.ReactNode; } class ToolbarContent extends React.Component { @@ -58,6 +60,7 @@ class ToolbarContent extends React.Component { showClearFiltersButton, clearFiltersButtonText, alignSelf, + expandableContent, ...props } = this.props; @@ -103,15 +106,19 @@ class ToolbarContent extends React.Component { > {children}
- + {expandableContent ? ( + expandableContent + ) : ( + + )} ); }} diff --git a/packages/react-core/src/components/Toolbar/ToolbarFilter.tsx b/packages/react-core/src/components/Toolbar/ToolbarFilter.tsx index dbded306250..c3d3aacc14e 100644 --- a/packages/react-core/src/components/Toolbar/ToolbarFilter.tsx +++ b/packages/react-core/src/components/Toolbar/ToolbarFilter.tsx @@ -39,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; } interface ToolbarFilterState { @@ -93,6 +95,7 @@ class ToolbarFilter extends React.Component {showToolbarItem && {children}} {chipContainerRef.current && ReactDOM.createPortal(chipGroup, chipContainerRef.current)} + {expandableChipContainerRef && + expandableChipContainerRef.current && + ReactDOM.createPortal(chipGroup, expandableChipContainerRef.current)}
)} diff --git a/packages/react-core/src/components/Toolbar/ToolbarToggleGroup.tsx b/packages/react-core/src/components/Toolbar/ToolbarToggleGroup.tsx index 212ea42b49e..0a5ed22f2fa 100644 --- a/packages/react-core/src/components/Toolbar/ToolbarToggleGroup.tsx +++ b/packages/react-core/src/components/Toolbar/ToolbarToggleGroup.tsx @@ -50,10 +50,13 @@ export interface ToolbarToggleGroupProps extends ToolbarGroupProps { xl?: 'spaceItemsNone' | 'spaceItemsSm' | 'spaceItemsMd' | 'spaceItemsLg'; '2xl'?: 'spaceItemsNone' | 'spaceItemsSm' | 'spaceItemsMd' | 'spaceItemsLg'; }; + /** Reference to a custom expandable content group, for non-managed multiple toolbar toggle groups. */ + expandableContentRef?: React.RefObject; } class ToolbarToggleGroup extends React.Component { static displayName = 'ToolbarToggleGroup'; + isContentPopup = () => { const viewportSize = canUseDOM ? window.innerWidth : 1200; const lgBreakpointValue = parseInt(globalBreakpointLg.value); @@ -72,6 +75,7 @@ class ToolbarToggleGroup extends React.Component { className, children, isExpanded, + expandableContentRef, onToggle, ...props } = this.props; @@ -91,16 +95,19 @@ class ToolbarToggleGroup extends React.Component { return ( - {({ expandableContentRef, expandableContentId }) => { + {({ expandableContentRef: managedExpandableContentRef, expandableContentId }) => { + const _contentRef = + expandableContentRef !== undefined ? expandableContentRef : managedExpandableContentRef; + if ( isExpanded === undefined && - expandableContentRef.current && - expandableContentRef.current.classList + managedExpandableContentRef.current && + managedExpandableContentRef.current.classList ) { if (_isExpanded) { - expandableContentRef.current.classList.add(styles.modifiers.expanded); + managedExpandableContentRef.current.classList.add(styles.modifiers.expanded); } else { - expandableContentRef.current.classList.remove(styles.modifiers.expanded); + managedExpandableContentRef.current.classList.remove(styles.modifiers.expanded); } } @@ -143,7 +150,7 @@ class ToolbarToggleGroup extends React.Component { {_isExpanded ? (ReactDOM.createPortal( children, - expandableContentRef.current.firstElementChild + _contentRef.current.firstElementChild ) as React.ReactElement) : children}
diff --git a/packages/react-core/src/components/Toolbar/examples/Test.tsx b/packages/react-core/src/components/Toolbar/examples/Test.tsx index 0d3f9f63eb5..368e04bca75 100644 --- a/packages/react-core/src/components/Toolbar/examples/Test.tsx +++ b/packages/react-core/src/components/Toolbar/examples/Test.tsx @@ -12,33 +12,50 @@ import { SelectList, SelectOption, ToolbarFilter, + ToolbarExpandableContent, Badge } from '@patternfly/react-core'; import FilterIcon from '@patternfly/react-icons/dist/esm/icons/filter-icon'; export const Test: React.FunctionComponent = () => { - const [isExpanded, setIsExpanded] = React.useState(false); - const [isExpanded2, setIsExpanded2] = React.useState(false); + const statusExpandableContentRef = React.useRef(); + const statusChipContainerRef = React.useRef(); + const riskExpandableContentRef = React.useRef(); + const riskChipContainerRef = React.useRef(); + const [isStatusGroupExpanded, setIsStatusGroupExpanded] = React.useState(false); + const [isRiskGroupExpanded, setIsRiskGroupExpanded] = React.useState(false); + + const [isStatusMenuExpanded, setIsStatusExpanded] = React.useState(false); + const [isRiskMenuExpanded, setIsRiskExpanded] = React.useState(false); const [inputValue, setInputValue] = React.useState(''); const [filters, setFilters] = React.useState({ risk: ['Low'], status: ['New', 'Pending'] }); - const [isStatusExpanded, setIsStatusExpanded] = React.useState(false); - const [isRiskExpanded, setIsRiskExpanded] = React.useState(false); + const closeToggleGroups = () => { + setIsStatusGroupExpanded(false); + setIsRiskGroupExpanded(false); + }; + + React.useEffect(() => { + window.addEventListener('resize', closeToggleGroups); // Resize observer to toggle off expand groups is required to properly reformat toolbar when growing + return () => { + window.removeEventListener('resize', closeToggleGroups); + }; + }, []); const onInputChange = (newValue: string) => { setInputValue(newValue); }; const onStatusToggle = () => { - setIsStatusExpanded(!isStatusExpanded); + setIsStatusExpanded(!isStatusMenuExpanded); }; const onRiskToggle = () => { - setIsRiskExpanded(!isRiskExpanded); + setIsRiskExpanded(!isRiskMenuExpanded); }; const onSelect = (type: string, event: React.MouseEvent | React.ChangeEvent | undefined, selection: string) => { @@ -78,43 +95,7 @@ export const Test: React.FunctionComponent = () => { } }; - const statusMenuItems = ( - - - New - - - Pending - - - Running - - - Cancelled - - - ); - - const riskMenuItems = ( - - - Low - - - Medium - - - High - - - ); - - const toggleGroupItems = ( + const statusToggleGroupItems = ( { deleteChip={(category, chip) => onDelete(category as string, chip as string)} deleteChipGroup={(category) => onDeleteGroup(category as string)} categoryName="Status" - isExpanded={isExpanded} + isExpanded={isStatusGroupExpanded} + expandableChipContainerRef={statusChipContainerRef} // Required to link the toolbar filter chip group to the custom expandable group > ); - const toggleGroup2Items = ( + const riskToggleGroupItems = ( onDelete(category as string, chip as string)} categoryName="Risk" - isExpanded={isExpanded2} + isExpanded={isRiskGroupExpanded} + expandableChipContainerRef={riskChipContainerRef} // Required to link the toolbar filter chip group to the custom expandable group > ); + const toggleExpandGroups = ( + <> + onDeleteGroup('Status')} + clearFiltersButtonText="Clear status filter" + /> + onDeleteGroup('Risk')} + clearFiltersButtonText="Clear risk filter" + /> + + ); + return ( - + setIsExpanded(!isExpanded)} + isExpanded={isStatusGroupExpanded} // Required to control expanded state + onToggle={() => { + setIsStatusGroupExpanded(!isStatusGroupExpanded); + setIsRiskGroupExpanded(false); + }} // Required to control expanded state + expandableContentRef={statusExpandableContentRef} // Required to link the toggle group to a specific expandable content group toggleIcon={} breakpoint="md" > - {toggleGroupItems} + {statusToggleGroupItems} setIsExpanded2(!isExpanded2)} + isExpanded={isRiskGroupExpanded} // Required to control expanded state + onToggle={() => { + setIsRiskGroupExpanded(!isRiskGroupExpanded); + setIsStatusGroupExpanded(false); + }} // Required to control expanded state + expandableContentRef={riskExpandableContentRef} // Required to link the toggle group to a specific expandable content group toggleIcon={} breakpoint="xl" > - {toggleGroup2Items} + {riskToggleGroupItems} diff --git a/packages/react-core/src/components/Toolbar/index.ts b/packages/react-core/src/components/Toolbar/index.ts index 14a3b4ad0ae..e95ce798717 100644 --- a/packages/react-core/src/components/Toolbar/index.ts +++ b/packages/react-core/src/components/Toolbar/index.ts @@ -1,6 +1,7 @@ export * from './Toolbar'; export * from './ToolbarContent'; export * from './ToolbarExpandIconWrapper'; +export * from './ToolbarExpandableContent'; export * from './ToolbarGroup'; export * from './ToolbarItem'; export * from './ToolbarFilter'; From a3b7033f539f1a5bfb7b65a7f0086e51492981eb Mon Sep 17 00:00:00 2001 From: Katie McFaul Date: Mon, 17 Jul 2023 11:45:09 -0400 Subject: [PATCH 4/6] toggle group with poppers --- .../src/components/Toolbar/ToolbarContent.tsx | 25 ++-- .../Toolbar/ToolbarExpandableContent.tsx | 3 +- .../components/Toolbar/ToolbarToggleGroup.tsx | 107 +++++++++++------- .../src/components/Toolbar/ToolbarUtils.tsx | 7 +- .../ToolbarContent.test.tsx.snap | 8 -- .../Toolbar/__tests__/Toolbar.test.tsx | 7 +- .../__tests__/ToolbarToggleGroup.test.tsx | 23 ++-- .../__snapshots__/Toolbar.test.tsx.snap | 59 ---------- .../src/components/Toolbar/examples/Test.tsx | 40 ++----- 9 files changed, 108 insertions(+), 171 deletions(-) diff --git a/packages/react-core/src/components/Toolbar/ToolbarContent.tsx b/packages/react-core/src/components/Toolbar/ToolbarContent.tsx index f6c21b25f92..51f19cc15a4 100644 --- a/packages/react-core/src/components/Toolbar/ToolbarContent.tsx +++ b/packages/react-core/src/components/Toolbar/ToolbarContent.tsx @@ -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 { @@ -33,8 +32,6 @@ export interface ToolbarContentProps extends React.HTMLProps { clearFiltersButtonText?: string; /** Id of the parent Toolbar component */ toolbarId?: string; - /** Custom expandable content for the toolbar, for non-managed multiple toolbar toggle groups. */ - expandableContent?: React.ReactNode; } class ToolbarContent extends React.Component { @@ -60,7 +57,6 @@ class ToolbarContent extends React.Component { showClearFiltersButton, clearFiltersButtonText, alignSelf, - expandableContent, ...props } = this.props; @@ -73,6 +69,7 @@ class ToolbarContent extends React.Component { formatBreakpointMods(visibility, styles, '', getBreakpoint(width)), className )} + ref={this.expandableContentRef} {...props} > @@ -80,6 +77,7 @@ class ToolbarContent extends React.Component { clearAllFilters: clearAllFiltersContext, clearFiltersButtonText: clearFiltersButtonContext, showClearFiltersButton: showClearFiltersButtonContext, + isExpanded: isExpandedContext, toolbarId: toolbarIdContext }) => { const expandableContentId = `${ @@ -90,7 +88,11 @@ class ToolbarContent extends React.Component { value={{ expandableContentRef: this.expandableContentRef, expandableContentId, - chipContainerRef: this.chipContainerRef + chipContainerRef: this.chipContainerRef, + isExpanded: isExpanded || isExpandedContext, + clearAllFilters: clearAllFilters || clearAllFiltersContext, + clearFiltersButtonText: clearFiltersButtonText || clearFiltersButtonContext, + showClearFiltersButton: showClearFiltersButton || showClearFiltersButtonContext }} >
{ > {children}
- {expandableContent ? ( - expandableContent - ) : ( - - )} ); }} diff --git a/packages/react-core/src/components/Toolbar/ToolbarExpandableContent.tsx b/packages/react-core/src/components/Toolbar/ToolbarExpandableContent.tsx index a7d87059f68..e8bf6907566 100644 --- a/packages/react-core/src/components/Toolbar/ToolbarExpandableContent.tsx +++ b/packages/react-core/src/components/Toolbar/ToolbarExpandableContent.tsx @@ -37,6 +37,7 @@ class ToolbarExpandableContent extends React.Component - + {children} {numberOfFilters > 0 && ( diff --git a/packages/react-core/src/components/Toolbar/ToolbarToggleGroup.tsx b/packages/react-core/src/components/Toolbar/ToolbarToggleGroup.tsx index 0a5ed22f2fa..99c1418a175 100644 --- a/packages/react-core/src/components/Toolbar/ToolbarToggleGroup.tsx +++ b/packages/react-core/src/components/Toolbar/ToolbarToggleGroup.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -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'; @@ -8,6 +7,8 @@ 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'; +import { Popper } from '../../helpers'; export interface ToolbarToggleGroupProps extends ToolbarGroupProps { /** Flag indicating when toggle group is expanded for non-managed toolbar toggle groups. */ @@ -50,12 +51,20 @@ export interface ToolbarToggleGroupProps extends ToolbarGroupProps { xl?: 'spaceItemsNone' | 'spaceItemsSm' | 'spaceItemsMd' | 'spaceItemsLg'; '2xl'?: 'spaceItemsNone' | 'spaceItemsSm' | 'spaceItemsMd' | 'spaceItemsLg'; }; - /** Reference to a custom expandable content group, for non-managed multiple toolbar toggle groups. */ - expandableContentRef?: React.RefObject; + /** Reference to a chip container group for filters inside the toolbar toggle group */ + chipContainerRef?: React.RefObject; + /** 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 { static displayName = 'ToolbarToggleGroup'; + toggleRef = React.createRef(); + expandableContentRef = React.createRef(); isContentPopup = () => { const viewportSize = canUseDOM ? window.innerWidth : 1200; @@ -75,8 +84,11 @@ class ToolbarToggleGroup extends React.Component { className, children, isExpanded, - expandableContentRef, onToggle, + chipContainerRef, + clearAllFilters, + showClearFiltersButton, + clearFiltersButtonText, ...props } = this.props; @@ -89,27 +101,23 @@ class ToolbarToggleGroup extends React.Component { {({ width, getBreakpoint }) => ( - {({ isExpanded: managedIsExpanded, toggleIsExpanded: managedOnToggle }) => { - const _isExpanded = isExpanded !== undefined ? isExpanded : managedIsExpanded; + {({ toggleIsExpanded: managedOnToggle }) => { const _onToggle = onToggle !== undefined ? onToggle : managedOnToggle; return ( - {({ expandableContentRef: managedExpandableContentRef, expandableContentId }) => { - const _contentRef = - expandableContentRef !== undefined ? expandableContentRef : managedExpandableContentRef; - - if ( - isExpanded === undefined && - managedExpandableContentRef.current && - managedExpandableContentRef.current.classList - ) { - if (_isExpanded) { - managedExpandableContentRef.current.classList.add(styles.modifiers.expanded); - } else { - managedExpandableContentRef.current.classList.remove(styles.modifiers.expanded); - } - } + {({ + 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'; @@ -119,6 +127,36 @@ class ToolbarToggleGroup extends React.Component { } = {}; breakpointMod[breakpoint] = 'show'; + const expandableContent = ( + + {children} + + ); + + const toggleButton = ( +
+ +
+ ); + return (
{ )} {...props} > -
- -
- {_isExpanded - ? (ReactDOM.createPortal( - children, - _contentRef.current.firstElementChild - ) as React.ReactElement) - : children} + + {!_isExpanded && children}
); }} diff --git a/packages/react-core/src/components/Toolbar/ToolbarUtils.tsx b/packages/react-core/src/components/Toolbar/ToolbarUtils.tsx index 9ee37c99f85..6aec87bf74b 100644 --- a/packages/react-core/src/components/Toolbar/ToolbarUtils.tsx +++ b/packages/react-core/src/components/Toolbar/ToolbarUtils.tsx @@ -31,12 +31,17 @@ interface ToolbarContentContextProps { expandableContentRef: RefObject; expandableContentId: string; chipContainerRef: RefObject; + isExpanded?: boolean; + clearAllFilters?: () => void; + clearFiltersButtonText?: string; + showClearFiltersButton?: boolean; } export const ToolbarContentContext = React.createContext({ expandableContentRef: null, expandableContentId: '', - chipContainerRef: null + chipContainerRef: null, + clearAllFilters: () => {} }); export const globalBreakpoints = { diff --git a/packages/react-core/src/components/Toolbar/__tests__/Generated/__snapshots__/ToolbarContent.test.tsx.snap b/packages/react-core/src/components/Toolbar/__tests__/Generated/__snapshots__/ToolbarContent.test.tsx.snap index 044e3353e87..a6d292adf1f 100644 --- a/packages/react-core/src/components/Toolbar/__tests__/Generated/__snapshots__/ToolbarContent.test.tsx.snap +++ b/packages/react-core/src/components/Toolbar/__tests__/Generated/__snapshots__/ToolbarContent.test.tsx.snap @@ -12,14 +12,6 @@ exports[`ToolbarContent should match snapshot (auto-generated) 1`] = ` ReactNode -
-
-
`; diff --git a/packages/react-core/src/components/Toolbar/__tests__/Toolbar.test.tsx b/packages/react-core/src/components/Toolbar/__tests__/Toolbar.test.tsx index 582485299aa..57c83cdce8a 100644 --- a/packages/react-core/src/components/Toolbar/__tests__/Toolbar.test.tsx +++ b/packages/react-core/src/components/Toolbar/__tests__/Toolbar.test.tsx @@ -65,7 +65,7 @@ describe('Toolbar', () => { {}} - deleteChipGroup={category => {}} + deleteChipGroup={(category) => {}} categoryName="Status" > test content @@ -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); }); }); diff --git a/packages/react-core/src/components/Toolbar/__tests__/ToolbarToggleGroup.test.tsx b/packages/react-core/src/components/Toolbar/__tests__/ToolbarToggleGroup.test.tsx index a86b1da1c86..91aa6e4b217 100644 --- a/packages/react-core/src/components/Toolbar/__tests__/ToolbarToggleGroup.test.tsx +++ b/packages/react-core/src/components/Toolbar/__tests__/ToolbarToggleGroup.test.tsx @@ -1,23 +1,26 @@ import React from 'react'; import { render } from '@testing-library/react'; import { ToolbarToggleGroup } from '../ToolbarToggleGroup'; -import { ToolbarContentContext } from '../ToolbarUtils'; +import { Toolbar } from '../Toolbar'; +import { ToolbarContent } from '../ToolbarContent'; describe('ToolbarToggleGroup', () => { it('should warn on bad props', () => { const myMock = jest.fn() as any; global.console = { error: myMock } as any; + const items = ( + + + test + + + ); + render( - - - + + {items} + ); expect(myMock).toHaveBeenCalled(); diff --git a/packages/react-core/src/components/Toolbar/__tests__/__snapshots__/Toolbar.test.tsx.snap b/packages/react-core/src/components/Toolbar/__tests__/__snapshots__/Toolbar.test.tsx.snap index 31e809b78b6..13b971baed6 100644 --- a/packages/react-core/src/components/Toolbar/__tests__/__snapshots__/Toolbar.test.tsx.snap +++ b/packages/react-core/src/components/Toolbar/__tests__/__snapshots__/Toolbar.test.tsx.snap @@ -34,14 +34,6 @@ exports[`Toolbar should render inset 1`] = ` Test 3 -
-
-
-
-
-
-
-
- -
-
- -
-
-
-
-
-
{ - const statusExpandableContentRef = React.useRef(); const statusChipContainerRef = React.useRef(); - const riskExpandableContentRef = React.useRef(); const riskChipContainerRef = React.useRef(); const [isStatusGroupExpanded, setIsStatusGroupExpanded] = React.useState(false); @@ -222,43 +219,21 @@ export const Test: React.FunctionComponent = () => { ); - const toggleExpandGroups = ( - <> - onDeleteGroup('Status')} - clearFiltersButtonText="Clear status filter" - /> - onDeleteGroup('Risk')} - clearFiltersButtonText="Clear risk filter" - /> - - ); - return ( - + { setIsStatusGroupExpanded(!isStatusGroupExpanded); setIsRiskGroupExpanded(false); }} // Required to control expanded state - expandableContentRef={statusExpandableContentRef} // Required to link the toggle group to a specific expandable content group toggleIcon={} breakpoint="md" + chipContainerRef={statusChipContainerRef} + showClearFiltersButton + clearAllFilters={() => onDeleteGroup('Status')} + clearFiltersButtonText="Clear status filter" > {statusToggleGroupItems} @@ -268,9 +243,12 @@ export const Test: React.FunctionComponent = () => { setIsRiskGroupExpanded(!isRiskGroupExpanded); setIsStatusGroupExpanded(false); }} // Required to control expanded state - expandableContentRef={riskExpandableContentRef} // Required to link the toggle group to a specific expandable content group toggleIcon={} breakpoint="xl" + chipContainerRef={riskChipContainerRef} + showClearFiltersButton + clearAllFilters={() => onDeleteGroup('Risk')} + clearFiltersButtonText={'Clear risk filter'} > {riskToggleGroupItems} From 470f38981325e7dfc33248a7413a03413b742987 Mon Sep 17 00:00:00 2001 From: Katie McFaul Date: Tue, 15 Aug 2023 10:26:29 -0400 Subject: [PATCH 5/6] swap back to createPortal --- .../src/components/Toolbar/ToolbarToggleGroup.tsx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/react-core/src/components/Toolbar/ToolbarToggleGroup.tsx b/packages/react-core/src/components/Toolbar/ToolbarToggleGroup.tsx index 99c1418a175..3a69a25ad7b 100644 --- a/packages/react-core/src/components/Toolbar/ToolbarToggleGroup.tsx +++ b/packages/react-core/src/components/Toolbar/ToolbarToggleGroup.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +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'; @@ -8,7 +9,6 @@ import globalBreakpointLg from '@patternfly/react-tokens/dist/esm/global_breakpo import { formatBreakpointMods, toCamel, canUseDOM } from '../../helpers/util'; import { PageContext } from '../Page/PageContext'; import { ToolbarExpandableContent } from './ToolbarExpandableContent'; -import { Popper } from '../../helpers'; export interface ToolbarToggleGroupProps extends ToolbarGroupProps { /** Flag indicating when toggle group is expanded for non-managed toolbar toggle groups. */ @@ -173,14 +173,8 @@ class ToolbarToggleGroup extends React.Component { )} {...props} > - + {toggleButton} + {_isExpanded && ReactDOM.createPortal(expandableContent, expandableContentRef.current)} {!_isExpanded && children}
); From 2ed203a9b4c2bbec260111f5d9ba76591a784359 Mon Sep 17 00:00:00 2001 From: Katie McFaul Date: Thu, 17 Aug 2023 11:24:26 -0400 Subject: [PATCH 6/6] update integration --- .../react-integration/cypress/integration/toolbar.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-integration/cypress/integration/toolbar.spec.ts b/packages/react-integration/cypress/integration/toolbar.spec.ts index 50955bcf95a..35ff79a7402 100644 --- a/packages/react-integration/cypress/integration/toolbar.spec.ts +++ b/packages/react-integration/cypress/integration/toolbar.spec.ts @@ -45,7 +45,7 @@ describe('Data Toolbar Demo Test', () => { it('displays toggle group contents', () => { cy.get('#demo-toggle-group #toolbar-demo-search').should('be.visible'); cy.get('#demo-toggle-group #toolbar-demo-filters').should('be.visible'); - cy.get('.pf-v5-c-toolbar__expandable-content').should('not.be.visible'); + cy.get('.pf-v5-c-toolbar__expandable-content').should('not.exist'); }); it('displays filter chips', () => { @@ -66,7 +66,7 @@ describe('Data Toolbar Demo Test', () => { cy.get('#demo-toggle-group .pf-v5-c-toolbar__toggle').should('be.visible'); cy.get('#demo-toggle-group #toolbar-demo-search').should('not.be.visible'); cy.get('#demo-toggle-group #toolbar-demo-filters').should('not.be.visible'); - cy.get('.pf-v5-c-toolbar__expandable-content').should('not.be.visible'); + cy.get('.pf-v5-c-toolbar__expandable-content').should('not.exist'); }); it('displays X filters applied message', () => {