-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add ResponsiveActions component (#361)
* feat(actions): Add ResponsiveActions component * feat(actions): Test ResponsiveActions component
- Loading branch information
1 parent
f2d04a7
commit f9a0ab6
Showing
11 changed files
with
425 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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( | ||
<ResponsiveActions breakpoint="lg"> | ||
<ResponsiveAction isPersistent> | ||
Persistent action | ||
</ResponsiveAction> | ||
<ResponsiveAction isPinned variant='secondary'> | ||
Pinned action | ||
</ResponsiveAction> | ||
<ResponsiveAction> | ||
Overflow action | ||
</ResponsiveAction> | ||
</ResponsiveActions> | ||
); | ||
|
||
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( | ||
<ResponsiveActions breakpoint="lg"> | ||
<ResponsiveAction isPersistent onClick={onClickSpy}> | ||
Persistent action | ||
</ResponsiveAction> | ||
<ResponsiveAction isPinned variant='secondary' onClick={onClickSpy}> | ||
Pinned action | ||
</ResponsiveAction> | ||
<ResponsiveAction onClick={onClickSpy}> | ||
Overflow action | ||
</ResponsiveAction> | ||
</ResponsiveActions> | ||
); | ||
|
||
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( | ||
<ResponsiveActions breakpoint="lg"> | ||
<ResponsiveAction> | ||
Overflow action | ||
</ResponsiveAction> | ||
</ResponsiveActions> | ||
); | ||
|
||
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'); | ||
}); | ||
}); |
44 changes: 44 additions & 0 deletions
44
...ent/extensions/component-groups/examples/ResponsiveActions/ResponsiveActions.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
|
||
``` |
60 changes: 60 additions & 0 deletions
60
...nsions/component-groups/examples/ResponsiveActions/ResponsiveActionsBreakpointExample.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<HTMLDivElement>(null); | ||
|
||
const onChange = (_event: SliderOnChangeEvent, value: number) => { | ||
setContainerWidth(value); | ||
}; | ||
|
||
const containerStyles = { | ||
width: `${containerWidth}%`, | ||
padding: '1rem', | ||
borderWidth: '2px', | ||
borderStyle: 'dashed' | ||
}; | ||
|
||
return ( | ||
<> | ||
<div style={{ width: '100%', maxWidth: '400px' }}> | ||
<div> | ||
<span id="responsiveActions-hasBreakpointOnContainer-slider-label">Current container width</span>: {containerWidth} | ||
% | ||
</div> | ||
<Slider | ||
value={containerWidth} | ||
onChange={onChange} | ||
max={100} | ||
min={40} | ||
step={20} | ||
showTicks | ||
showBoundaries={false} | ||
aria-labelledby="responsiveActions-hasBreakpointOnContainer-slider-label" | ||
/> | ||
</div> | ||
<div ref={containerRef} id="breakpoint-reference-container" style={containerStyles}> | ||
<ResponsiveActions breakpoint="sm" breakpointReference={containerRef}> | ||
<ResponsiveAction isPersistent> | ||
Persistent Action | ||
</ResponsiveAction> | ||
<ResponsiveAction isPinned variant='secondary'> | ||
Pinned Action 1 | ||
</ResponsiveAction> | ||
<ResponsiveAction isPinned variant='secondary'> | ||
Pinned Action 2 | ||
</ResponsiveAction> | ||
<ResponsiveAction> | ||
Overflow Action | ||
</ResponsiveAction> | ||
</ResponsiveActions> | ||
</div> | ||
</> | ||
); | ||
}; |
17 changes: 17 additions & 0 deletions
17
...ntent/extensions/component-groups/examples/ResponsiveActions/ResponsiveActionsExample.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 = () => ( | ||
<ResponsiveActions breakpoint="lg"> | ||
<ResponsiveAction isPersistent> | ||
Persistent Action | ||
</ResponsiveAction> | ||
<ResponsiveAction isPinned variant='secondary'> | ||
Pinned Action | ||
</ResponsiveAction> | ||
<ResponsiveAction> | ||
Overflow Action | ||
</ResponsiveAction> | ||
</ResponsiveActions> | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<ResponsiveActionProps> = (_props: ResponsiveActionProps) => null; | ||
|
||
export default ResponsiveAction; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export { default } from './ResponsiveAction'; | ||
export * from './ResponsiveAction'; |
19 changes: 19 additions & 0 deletions
19
packages/module/src/ResponsiveActions/ResponsiveActions.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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( | ||
<ResponsiveActions breakpoint="lg"> | ||
<ResponsiveAction isPersistent>Persistent action</ResponsiveAction> | ||
<ResponsiveAction isPinned variant='secondary'>Pinned action</ResponsiveAction> | ||
<ResponsiveAction>Overflow action</ResponsiveAction> | ||
</ResponsiveActions>); | ||
expect(container).toMatchSnapshot(); | ||
}); | ||
}); | ||
}); |
92 changes: 92 additions & 0 deletions
92
packages/module/src/ResponsiveActions/ResponsiveActions.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<OverflowMenuProps, 'ref' | 'breakpoint'> { | ||
/** 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<ResponsiveActionsProps> = ({ 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<ResponsiveActionProps>(child)) { | ||
const { isPersistent, isPinned, key = index, children, onClick, ...actionProps } = child.props; | ||
|
||
if (isPersistent || isPinned) { | ||
(isPersistent ? persistentActions : pinnedActions).push( | ||
<OverflowMenuItem key={key} isPersistent={isPersistent}> | ||
<Button onClick={onClick} ouiaId={`${ouiaId}-action-${key}`} {...actionProps}> | ||
{children} | ||
</Button> | ||
</OverflowMenuItem> | ||
); | ||
} | ||
if (!isPersistent) { | ||
dropdownItems.push( | ||
<OverflowMenuDropdownItem key={key} onClick={onClick} isShared={isPinned} ouiaId={`${ouiaId}-action-${key}`}> | ||
{children} | ||
</OverflowMenuDropdownItem> | ||
); | ||
} | ||
} | ||
}); | ||
|
||
return ( | ||
<OverflowMenu breakpoint={breakpoint} data-ouia-component-id={`${ouiaId}-menu`} {...props}> | ||
{persistentActions.length > 0 ? ( | ||
<OverflowMenuContent isPersistent data-ouia-component-id={`${ouiaId}-menu-persistent-content`}> | ||
<OverflowMenuGroup groupType="button" data-ouia-component-id={`${ouiaId}-menu-persistent-group`} isPersistent> | ||
{persistentActions} | ||
</OverflowMenuGroup> | ||
</OverflowMenuContent> | ||
) : null} | ||
{pinnedActions.length > 0 ? ( | ||
<OverflowMenuContent data-ouia-component-id={`${ouiaId}-menu-pinned-content`}> | ||
<OverflowMenuGroup groupType="button" data-ouia-component-id={`${ouiaId}-menu-pinned-group`}> | ||
{pinnedActions} | ||
</OverflowMenuGroup> | ||
</OverflowMenuContent> | ||
) : null} | ||
{dropdownItems.length > 0 && ( | ||
<OverflowMenuControl hasAdditionalOptions data-ouia-component-id={`${ouiaId}-menu-control`}> | ||
<Dropdown | ||
ouiaId={`${ouiaId}-menu-dropdown`} | ||
onSelect={() => setIsOpen(false)} | ||
toggle={(toggleRef) => ( | ||
<MenuToggle | ||
data-ouia-component-id={`${ouiaId}-menu-dropdown-toggle`} | ||
ref={toggleRef} | ||
aria-label="Actions overflow menu" | ||
variant="plain" | ||
onClick={() => setIsOpen(!isOpen)} | ||
isExpanded={isOpen} | ||
> | ||
<EllipsisVIcon /> | ||
</MenuToggle> | ||
)} | ||
isOpen={isOpen} | ||
onOpenChange={setIsOpen} | ||
> | ||
<DropdownList data-ouia-component-id={`${ouiaId}-menu-dropdown-list`}> | ||
{dropdownItems} | ||
</DropdownList> | ||
</Dropdown> | ||
</OverflowMenuControl> | ||
)} | ||
</OverflowMenu> | ||
); | ||
}; | ||
|
||
export default ResponsiveActions; |
Oops, something went wrong.