From 782d07fa176c3db8dec04509ec0882dbcf033d72 Mon Sep 17 00:00:00 2001 From: Filip Hlavac Date: Mon, 7 Oct 2024 15:17:53 +0200 Subject: [PATCH 1/2] feat(actions): Add ResponsiveActions component --- .../ResponsiveActions/ResponsiveActions.md | 44 +++++++++ .../ResponsiveActionsBreakpointExample.tsx | 60 ++++++++++++ .../ResponsiveActionsExample.tsx | 17 ++++ .../src/ResponsiveAction/ResponsiveAction.tsx | 18 ++++ packages/module/src/ResponsiveAction/index.ts | 2 + .../ResponsiveActions/ResponsiveActions.tsx | 92 +++++++++++++++++++ .../module/src/ResponsiveActions/index.ts | 2 + packages/module/src/index.ts | 6 ++ 8 files changed, 241 insertions(+) create mode 100644 packages/module/patternfly-docs/content/extensions/component-groups/examples/ResponsiveActions/ResponsiveActions.md create mode 100644 packages/module/patternfly-docs/content/extensions/component-groups/examples/ResponsiveActions/ResponsiveActionsBreakpointExample.tsx create mode 100644 packages/module/patternfly-docs/content/extensions/component-groups/examples/ResponsiveActions/ResponsiveActionsExample.tsx create mode 100644 packages/module/src/ResponsiveAction/ResponsiveAction.tsx create mode 100644 packages/module/src/ResponsiveAction/index.ts create mode 100644 packages/module/src/ResponsiveActions/ResponsiveActions.tsx create mode 100644 packages/module/src/ResponsiveActions/index.ts diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/ResponsiveActions/ResponsiveActions.md b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ResponsiveActions/ResponsiveActions.md new file mode 100644 index 000000000..35e80be8f --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ResponsiveActions/ResponsiveActions.md @@ -0,0 +1,44 @@ +--- +# Sidenav top-level section +# should be the same for all markdown files +section: extensions +subsection: Component groups +# Sidenav secondary level section +# should be the same for all markdown files +id: Responsive actions +# Tab (react | react-demos | html | html-demos | design-guidelines | accessibility) +source: react +# If you use typescript, the name of the interface to display props for +# These are found through the sourceProps function provided in patternfly-docs.source.js +propComponents: ['ResponsiveAction', 'ResponsiveActions'] +sourceLink: https://github.com/patternfly/react-component-groups/blob/main/packages/module/patternfly-docs/content/extensions/component-groups/examples/ResponsiveActions/ResponsiveActions.md +--- +import { useState } from 'react'; +import { ResponsiveAction } from '@patternfly/react-component-groups/dist/dynamic/ResponsiveAction'; +import { ResponsiveActions } from '@patternfly/react-component-groups/dist/dynamic/ResponsiveActions'; + +The **responsive actions** component allows for the display of actions in a responsive layout. Actions can be presented as persistent, pinned or collapsed to dropdown. + +The `ResponsiveAction` component is used to declare individual actions within the `ResponsiveActions` wrapper. Each action can be displayed as a standalone button or dropdown based on `isPinned` and `isPersistent` properties. Persistent actions are always separate buttons no matter of the screen size. Pinned actions are rendered as buttons as well, but when the screen size is below the defined breakpoint, they get collapsed to the actions dropdown. Other actions render in a dropdown on all screen sizes. + +## Examples + +### Basic responsive actions + +This example demonstrates how to create responsive actions with persistent and pinned actions. + + +```js file="./ResponsiveActionsExample.tsx" + +``` + +### Breakpoint on container + +By passing in the `breakpointReference` property, the overflow menu's breakpoint will be relative to the width of the reference container rather than the viewport width. + +You can change the container width in this example by adjusting the slider. As the container width changes, the actions will change their layout despite the viewport width not changing. + + +```js file="./ResponsiveActionsBreakpointExample.tsx" + +``` diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/ResponsiveActions/ResponsiveActionsBreakpointExample.tsx b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ResponsiveActions/ResponsiveActionsBreakpointExample.tsx new file mode 100644 index 000000000..6fe7bf4cd --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ResponsiveActions/ResponsiveActionsBreakpointExample.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { + Slider, + SliderOnChangeEvent, +} from '@patternfly/react-core'; +import { ResponsiveActions } from '@patternfly/react-component-groups/dist/dynamic/ResponsiveActions'; +import { ResponsiveAction } from '@patternfly/react-component-groups/dist/dynamic/ResponsiveAction'; + +export const ResponsiveActionsBreakpointExample: React.FunctionComponent = () => { + const [ containerWidth, setContainerWidth ] = React.useState(100); + const containerRef = React.useRef(null); + + const onChange = (_event: SliderOnChangeEvent, value: number) => { + setContainerWidth(value); + }; + + const containerStyles = { + width: `${containerWidth}%`, + padding: '1rem', + borderWidth: '2px', + borderStyle: 'dashed' + }; + + return ( + <> +
+
+ Current container width: {containerWidth} + % +
+ +
+
+ + + Persistent Action + + + Pinned Action 1 + + + Pinned Action 2 + + + Overflow Action + + +
+ + ); +}; diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/ResponsiveActions/ResponsiveActionsExample.tsx b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ResponsiveActions/ResponsiveActionsExample.tsx new file mode 100644 index 000000000..14522bc72 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/ResponsiveActions/ResponsiveActionsExample.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { ResponsiveAction } from '@patternfly/react-component-groups/dist/dynamic/ResponsiveAction'; +import { ResponsiveActions } from '@patternfly/react-component-groups/dist/dynamic/ResponsiveActions'; + +export const TagCountDisabledExample: React.FunctionComponent = () => ( + + + Persistent Action + + + Pinned Action + + + Overflow Action + + +); diff --git a/packages/module/src/ResponsiveAction/ResponsiveAction.tsx b/packages/module/src/ResponsiveAction/ResponsiveAction.tsx new file mode 100644 index 000000000..bf47dbe4f --- /dev/null +++ b/packages/module/src/ResponsiveAction/ResponsiveAction.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { ButtonProps } from '@patternfly/react-core'; + +export interface ResponsiveActionProps extends ButtonProps { + /** Determines whether the action should be displayed next to dropdown if possible */ + isPinned?: boolean; + /** Determines whether the action should always be displayed as pinned */ + isPersistent?: boolean; + /** Key for the action */ + key?: string; + /** Action label */ + children: React.ReactNode; +}; + +// This component is only used declaratively - rendering ishandled by ResponsiveActions +export const ResponsiveAction: React.FunctionComponent = (_props: ResponsiveActionProps) => null; + +export default ResponsiveAction; \ No newline at end of file diff --git a/packages/module/src/ResponsiveAction/index.ts b/packages/module/src/ResponsiveAction/index.ts new file mode 100644 index 000000000..97ef0d230 --- /dev/null +++ b/packages/module/src/ResponsiveAction/index.ts @@ -0,0 +1,2 @@ +export { default } from './ResponsiveAction'; +export * from './ResponsiveAction'; diff --git a/packages/module/src/ResponsiveActions/ResponsiveActions.tsx b/packages/module/src/ResponsiveActions/ResponsiveActions.tsx new file mode 100644 index 000000000..4f1b27938 --- /dev/null +++ b/packages/module/src/ResponsiveActions/ResponsiveActions.tsx @@ -0,0 +1,92 @@ +import React, { useState } from 'react'; +import { Button, Dropdown, DropdownList, MenuToggle, OverflowMenu, OverflowMenuContent, OverflowMenuControl, OverflowMenuDropdownItem, OverflowMenuGroup, OverflowMenuItem, OverflowMenuProps } from '@patternfly/react-core'; +import { EllipsisVIcon } from '@patternfly/react-icons'; +import { ResponsiveActionProps } from '../ResponsiveAction'; + +export interface ResponsiveActionsProps extends Omit { + /** Indicates breakpoint at which to switch between horizontal menu and vertical dropdown */ + breakpoint?: OverflowMenuProps['breakpoint']; + /** Custom OUIA ID */ + ouiaId?: string; + /** Child actions to be displayed */ + children: React.ReactNode; +} + +export const ResponsiveActions: React.FunctionComponent = ({ ouiaId = 'ResponsiveActions', breakpoint = 'lg', children, ...props }: ResponsiveActionsProps) => { + const [ isOpen, setIsOpen ] = useState(false); + + // separate persistent, pinned and collapsed actions + const persistentActions: React.ReactNode[] = []; + const pinnedActions: React.ReactNode[] = []; + const dropdownItems: React.ReactNode[] = []; + + React.Children.forEach(children, (child, index) => { + if (React.isValidElement(child)) { + const { isPersistent, isPinned, key = index, children, onClick, ...actionProps } = child.props; + + if (isPersistent || isPinned) { + (isPersistent ? persistentActions : pinnedActions).push( + + + + ); + } + if (!isPersistent) { + dropdownItems.push( + + {children} + + ); + } + } + }); + + return ( + + {persistentActions.length > 0 ? ( + + + {persistentActions} + + + ) : null} + {pinnedActions.length > 0 ? ( + + + {pinnedActions} + + + ) : null} + {dropdownItems.length > 0 && ( + + setIsOpen(false)} + toggle={(toggleRef) => ( + setIsOpen(!isOpen)} + isExpanded={isOpen} + > + + + )} + isOpen={isOpen} + onOpenChange={setIsOpen} + > + + {dropdownItems} + + + + )} + + ); +}; + +export default ResponsiveActions; \ No newline at end of file diff --git a/packages/module/src/ResponsiveActions/index.ts b/packages/module/src/ResponsiveActions/index.ts new file mode 100644 index 000000000..6c694dbd3 --- /dev/null +++ b/packages/module/src/ResponsiveActions/index.ts @@ -0,0 +1,2 @@ +export { default } from './ResponsiveActions'; +export * from './ResponsiveActions'; diff --git a/packages/module/src/index.ts b/packages/module/src/index.ts index 2cb9f7c27..52b726775 100644 --- a/packages/module/src/index.ts +++ b/packages/module/src/index.ts @@ -27,6 +27,12 @@ export * from './Shortcut'; export { default as ServiceCard } from './ServiceCard'; export * from './ServiceCard'; +export { default as ResponsiveActions } from './ResponsiveActions'; +export * from './ResponsiveActions'; + +export { default as ResponsiveAction } from './ResponsiveAction'; +export * from './ResponsiveAction'; + export { default as NotFoundIcon } from './NotFoundIcon'; export * from './NotFoundIcon'; From e5689861b7b20d9fc5e8d8c8dea7fe1846407500 Mon Sep 17 00:00:00 2001 From: Filip Hlavac Date: Mon, 7 Oct 2024 15:18:07 +0200 Subject: [PATCH 2/2] feat(actions): Test ResponsiveActions component --- cypress/component/ResponsiveActions.cy.tsx | 76 +++++++++++++ .../ResponsiveActions.test.tsx | 19 ++++ .../ResponsiveActions.test.tsx.snap | 89 +++++++++++++++ .../__snapshots__/SkeletonTable.test.tsx.snap | 88 ++++++++------- .../SkeletonTableHead.test.tsx.snap | 106 +++++++++++++++++- 5 files changed, 336 insertions(+), 42 deletions(-) create mode 100644 cypress/component/ResponsiveActions.cy.tsx create mode 100644 packages/module/src/ResponsiveActions/ResponsiveActions.test.tsx create mode 100644 packages/module/src/ResponsiveActions/__snapshots__/ResponsiveActions.test.tsx.snap diff --git a/cypress/component/ResponsiveActions.cy.tsx b/cypress/component/ResponsiveActions.cy.tsx new file mode 100644 index 000000000..adaa506b6 --- /dev/null +++ b/cypress/component/ResponsiveActions.cy.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { ResponsiveActions } from '@patternfly/react-component-groups/dist/dynamic/ResponsiveActions'; +import { ResponsiveAction } from '@patternfly/react-component-groups/dist/dynamic/ResponsiveAction'; + +describe('ResponsiveActions', () => { + beforeEach(() => { + cy.viewport(1280, 2000); + }) + + it('renders persistent, pinned, and overflow actions', () => { + cy.mount( + + + Persistent action + + + Pinned action + + + Overflow action + + + ); + + cy.get('[data-ouia-component-id="ResponsiveActions-action-0"]').should('be.visible'); + cy.get('[data-ouia-component-id="ResponsiveActions-action-1"]').should('be.visible'); + cy.get('[data-ouia-component-id="ResponsiveActions-action-2"]').should('not.exist'); + + cy.get('[data-ouia-component-id="ResponsiveActions-menu-dropdown-toggle"]').click(); + cy.get('[data-ouia-component-id="ResponsiveActions-action-2"]').should('be.visible'); + }); + + it('handles click events on actions', () => { + const onClickSpy = cy.spy().as('actionClickSpy'); + + cy.mount( + + + Persistent action + + + Pinned action + + + Overflow action + + + ); + + cy.get('[data-ouia-component-id="ResponsiveActions-action-0"]').click(); + cy.get('@actionClickSpy').should('have.been.calledOnce'); + + cy.get('[data-ouia-component-id="ResponsiveActions-action-1"]').click(); + cy.get('@actionClickSpy').should('have.been.calledTwice'); + + cy.get('[data-ouia-component-id="ResponsiveActions-menu-dropdown-toggle"]').click(); + cy.get('[data-ouia-component-id="ResponsiveActions-action-2"]').click(); + cy.get('@actionClickSpy').should('have.been.calledThrice'); + }); + + it('renders no persistent or pinned actions without flags', () => { + cy.mount( + + + Overflow action + + + ); + + cy.get('[data-ouia-component-id="menu-persistent-content"]').should('not.exist'); + cy.get('[data-ouia-component-id="menu-pinned-content"]').should('not.exist'); + + cy.get('[data-ouia-component-id="ResponsiveActions-menu-dropdown-toggle"]').click(); + cy.contains('Overflow action').should('be.visible'); + }); +}); diff --git a/packages/module/src/ResponsiveActions/ResponsiveActions.test.tsx b/packages/module/src/ResponsiveActions/ResponsiveActions.test.tsx new file mode 100644 index 000000000..2fc6bc6bd --- /dev/null +++ b/packages/module/src/ResponsiveActions/ResponsiveActions.test.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import ResponsiveActions from './ResponsiveActions'; +import ResponsiveAction from '../ResponsiveAction'; + +describe('ResponsiveActions component', () => { + describe('should render correctly', () => { + + test('ResponsiveActions', () => { + const { container } = render( + + Persistent action + Pinned action + Overflow action + ); + expect(container).toMatchSnapshot(); + }); + }); +}); \ No newline at end of file diff --git a/packages/module/src/ResponsiveActions/__snapshots__/ResponsiveActions.test.tsx.snap b/packages/module/src/ResponsiveActions/__snapshots__/ResponsiveActions.test.tsx.snap new file mode 100644 index 000000000..a8568da4b --- /dev/null +++ b/packages/module/src/ResponsiveActions/__snapshots__/ResponsiveActions.test.tsx.snap @@ -0,0 +1,89 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ResponsiveActions component should render correctly ResponsiveActions 1`] = ` +
+
+
+
+
+ + + +
+
+
+
+
+
+ + + +
+
+
+
+ + + +
+
+
+`; diff --git a/packages/module/src/SkeletonTable/__snapshots__/SkeletonTable.test.tsx.snap b/packages/module/src/SkeletonTable/__snapshots__/SkeletonTable.test.tsx.snap index 6995a4721..53e605d57 100644 --- a/packages/module/src/SkeletonTable/__snapshots__/SkeletonTable.test.tsx.snap +++ b/packages/module/src/SkeletonTable/__snapshots__/SkeletonTable.test.tsx.snap @@ -15,27 +15,29 @@ exports[`SkeletonTable component should render correctly 1`] = ` > -
- -
+ First + + + Second @@ -224,27 +226,29 @@ exports[`SkeletonTable component should render correctly 1`] = ` > -
- -
+ First + + + Second @@ -491,27 +495,29 @@ exports[`SkeletonTable component should render correctly with rows 1`] = ` > -
- -
+ First + + + Second @@ -701,27 +707,29 @@ exports[`SkeletonTable component should render correctly with rows 1`] = ` > -
- -
+ First + + + Second diff --git a/packages/module/src/SkeletonTableHead/__snapshots__/SkeletonTableHead.test.tsx.snap b/packages/module/src/SkeletonTableHead/__snapshots__/SkeletonTableHead.test.tsx.snap index 93178b103..69859762b 100644 --- a/packages/module/src/SkeletonTableHead/__snapshots__/SkeletonTableHead.test.tsx.snap +++ b/packages/module/src/SkeletonTableHead/__snapshots__/SkeletonTableHead.test.tsx.snap @@ -14,7 +14,58 @@ exports[`SkeletonTableHead component should render correctly with count 1`] = ` data-ouia-component-id="SkeletonTableHeader-tr-head" data-ouia-component-type="PF5/TableRow" data-ouia-safe="true" - /> + > + + + Data expansion table header cell + + + + + Data selection table header cell + + + +
+ +
+ + +
+ +
+ + , @@ -28,7 +79,58 @@ exports[`SkeletonTableHead component should render correctly with count 1`] = ` data-ouia-component-id="SkeletonTableHeader-tr-head" data-ouia-component-type="PF5/TableRow" data-ouia-safe="true" - /> + > + + + Data expansion table header cell + + + + + Data selection table header cell + + + +
+ +
+ + +
+ +
+ + , "debug": [Function],