Skip to content

Commit 782d07f

Browse files
committed
feat(actions): Add ResponsiveActions component
1 parent 88a929b commit 782d07f

File tree

8 files changed

+241
-0
lines changed

8 files changed

+241
-0
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
# Sidenav top-level section
3+
# should be the same for all markdown files
4+
section: extensions
5+
subsection: Component groups
6+
# Sidenav secondary level section
7+
# should be the same for all markdown files
8+
id: Responsive actions
9+
# Tab (react | react-demos | html | html-demos | design-guidelines | accessibility)
10+
source: react
11+
# If you use typescript, the name of the interface to display props for
12+
# These are found through the sourceProps function provided in patternfly-docs.source.js
13+
propComponents: ['ResponsiveAction', 'ResponsiveActions']
14+
sourceLink: https://github.com/patternfly/react-component-groups/blob/main/packages/module/patternfly-docs/content/extensions/component-groups/examples/ResponsiveActions/ResponsiveActions.md
15+
---
16+
import { useState } from 'react';
17+
import { ResponsiveAction } from '@patternfly/react-component-groups/dist/dynamic/ResponsiveAction';
18+
import { ResponsiveActions } from '@patternfly/react-component-groups/dist/dynamic/ResponsiveActions';
19+
20+
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.
21+
22+
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.
23+
24+
## Examples
25+
26+
### Basic responsive actions
27+
28+
This example demonstrates how to create responsive actions with persistent and pinned actions.
29+
30+
31+
```js file="./ResponsiveActionsExample.tsx"
32+
33+
```
34+
35+
### Breakpoint on container
36+
37+
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.
38+
39+
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.
40+
41+
42+
```js file="./ResponsiveActionsBreakpointExample.tsx"
43+
44+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import React from 'react';
2+
import {
3+
Slider,
4+
SliderOnChangeEvent,
5+
} from '@patternfly/react-core';
6+
import { ResponsiveActions } from '@patternfly/react-component-groups/dist/dynamic/ResponsiveActions';
7+
import { ResponsiveAction } from '@patternfly/react-component-groups/dist/dynamic/ResponsiveAction';
8+
9+
export const ResponsiveActionsBreakpointExample: React.FunctionComponent = () => {
10+
const [ containerWidth, setContainerWidth ] = React.useState(100);
11+
const containerRef = React.useRef<HTMLDivElement>(null);
12+
13+
const onChange = (_event: SliderOnChangeEvent, value: number) => {
14+
setContainerWidth(value);
15+
};
16+
17+
const containerStyles = {
18+
width: `${containerWidth}%`,
19+
padding: '1rem',
20+
borderWidth: '2px',
21+
borderStyle: 'dashed'
22+
};
23+
24+
return (
25+
<>
26+
<div style={{ width: '100%', maxWidth: '400px' }}>
27+
<div>
28+
<span id="responsiveActions-hasBreakpointOnContainer-slider-label">Current container width</span>: {containerWidth}
29+
%
30+
</div>
31+
<Slider
32+
value={containerWidth}
33+
onChange={onChange}
34+
max={100}
35+
min={40}
36+
step={20}
37+
showTicks
38+
showBoundaries={false}
39+
aria-labelledby="responsiveActions-hasBreakpointOnContainer-slider-label"
40+
/>
41+
</div>
42+
<div ref={containerRef} id="breakpoint-reference-container" style={containerStyles}>
43+
<ResponsiveActions breakpoint="sm" breakpointReference={containerRef}>
44+
<ResponsiveAction isPersistent>
45+
Persistent Action
46+
</ResponsiveAction>
47+
<ResponsiveAction isPinned variant='secondary'>
48+
Pinned Action 1
49+
</ResponsiveAction>
50+
<ResponsiveAction isPinned variant='secondary'>
51+
Pinned Action 2
52+
</ResponsiveAction>
53+
<ResponsiveAction>
54+
Overflow Action
55+
</ResponsiveAction>
56+
</ResponsiveActions>
57+
</div>
58+
</>
59+
);
60+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import React from 'react';
2+
import { ResponsiveAction } from '@patternfly/react-component-groups/dist/dynamic/ResponsiveAction';
3+
import { ResponsiveActions } from '@patternfly/react-component-groups/dist/dynamic/ResponsiveActions';
4+
5+
export const TagCountDisabledExample: React.FunctionComponent = () => (
6+
<ResponsiveActions breakpoint="lg">
7+
<ResponsiveAction isPersistent>
8+
Persistent Action
9+
</ResponsiveAction>
10+
<ResponsiveAction isPinned variant='secondary'>
11+
Pinned Action
12+
</ResponsiveAction>
13+
<ResponsiveAction>
14+
Overflow Action
15+
</ResponsiveAction>
16+
</ResponsiveActions>
17+
);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import React from 'react';
2+
import { ButtonProps } from '@patternfly/react-core';
3+
4+
export interface ResponsiveActionProps extends ButtonProps {
5+
/** Determines whether the action should be displayed next to dropdown if possible */
6+
isPinned?: boolean;
7+
/** Determines whether the action should always be displayed as pinned */
8+
isPersistent?: boolean;
9+
/** Key for the action */
10+
key?: string;
11+
/** Action label */
12+
children: React.ReactNode;
13+
};
14+
15+
// This component is only used declaratively - rendering ishandled by ResponsiveActions
16+
export const ResponsiveAction: React.FunctionComponent<ResponsiveActionProps> = (_props: ResponsiveActionProps) => null;
17+
18+
export default ResponsiveAction;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default } from './ResponsiveAction';
2+
export * from './ResponsiveAction';
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import React, { useState } from 'react';
2+
import { Button, Dropdown, DropdownList, MenuToggle, OverflowMenu, OverflowMenuContent, OverflowMenuControl, OverflowMenuDropdownItem, OverflowMenuGroup, OverflowMenuItem, OverflowMenuProps } from '@patternfly/react-core';
3+
import { EllipsisVIcon } from '@patternfly/react-icons';
4+
import { ResponsiveActionProps } from '../ResponsiveAction';
5+
6+
export interface ResponsiveActionsProps extends Omit<OverflowMenuProps, 'ref' | 'breakpoint'> {
7+
/** Indicates breakpoint at which to switch between horizontal menu and vertical dropdown */
8+
breakpoint?: OverflowMenuProps['breakpoint'];
9+
/** Custom OUIA ID */
10+
ouiaId?: string;
11+
/** Child actions to be displayed */
12+
children: React.ReactNode;
13+
}
14+
15+
export const ResponsiveActions: React.FunctionComponent<ResponsiveActionsProps> = ({ ouiaId = 'ResponsiveActions', breakpoint = 'lg', children, ...props }: ResponsiveActionsProps) => {
16+
const [ isOpen, setIsOpen ] = useState(false);
17+
18+
// separate persistent, pinned and collapsed actions
19+
const persistentActions: React.ReactNode[] = [];
20+
const pinnedActions: React.ReactNode[] = [];
21+
const dropdownItems: React.ReactNode[] = [];
22+
23+
React.Children.forEach(children, (child, index) => {
24+
if (React.isValidElement<ResponsiveActionProps>(child)) {
25+
const { isPersistent, isPinned, key = index, children, onClick, ...actionProps } = child.props;
26+
27+
if (isPersistent || isPinned) {
28+
(isPersistent ? persistentActions : pinnedActions).push(
29+
<OverflowMenuItem key={key} isPersistent={isPersistent}>
30+
<Button onClick={onClick} ouiaId={`${ouiaId}-action-${key}`} {...actionProps}>
31+
{children}
32+
</Button>
33+
</OverflowMenuItem>
34+
);
35+
}
36+
if (!isPersistent) {
37+
dropdownItems.push(
38+
<OverflowMenuDropdownItem key={key} onClick={onClick} isShared={isPinned} ouiaId={`${ouiaId}-action-${key}`}>
39+
{children}
40+
</OverflowMenuDropdownItem>
41+
);
42+
}
43+
}
44+
});
45+
46+
return (
47+
<OverflowMenu breakpoint={breakpoint} data-ouia-component-id={`${ouiaId}-menu`} {...props}>
48+
{persistentActions.length > 0 ? (
49+
<OverflowMenuContent isPersistent data-ouia-component-id={`${ouiaId}-menu-persistent-content`}>
50+
<OverflowMenuGroup groupType="button" data-ouia-component-id={`${ouiaId}-menu-persistent-group`} isPersistent>
51+
{persistentActions}
52+
</OverflowMenuGroup>
53+
</OverflowMenuContent>
54+
) : null}
55+
{pinnedActions.length > 0 ? (
56+
<OverflowMenuContent data-ouia-component-id={`${ouiaId}-menu-pinned-content`}>
57+
<OverflowMenuGroup groupType="button" data-ouia-component-id={`${ouiaId}-menu-pinned-group`}>
58+
{pinnedActions}
59+
</OverflowMenuGroup>
60+
</OverflowMenuContent>
61+
) : null}
62+
{dropdownItems.length > 0 && (
63+
<OverflowMenuControl hasAdditionalOptions data-ouia-component-id={`${ouiaId}-menu-control`}>
64+
<Dropdown
65+
ouiaId={`${ouiaId}-menu-dropdown`}
66+
onSelect={() => setIsOpen(false)}
67+
toggle={(toggleRef) => (
68+
<MenuToggle
69+
data-ouia-component-id={`${ouiaId}-menu-dropdown-toggle`}
70+
ref={toggleRef}
71+
aria-label="Actions overflow menu"
72+
variant="plain"
73+
onClick={() => setIsOpen(!isOpen)}
74+
isExpanded={isOpen}
75+
>
76+
<EllipsisVIcon />
77+
</MenuToggle>
78+
)}
79+
isOpen={isOpen}
80+
onOpenChange={setIsOpen}
81+
>
82+
<DropdownList data-ouia-component-id={`${ouiaId}-menu-dropdown-list`}>
83+
{dropdownItems}
84+
</DropdownList>
85+
</Dropdown>
86+
</OverflowMenuControl>
87+
)}
88+
</OverflowMenu>
89+
);
90+
};
91+
92+
export default ResponsiveActions;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default } from './ResponsiveActions';
2+
export * from './ResponsiveActions';

packages/module/src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ export * from './Shortcut';
2727
export { default as ServiceCard } from './ServiceCard';
2828
export * from './ServiceCard';
2929

30+
export { default as ResponsiveActions } from './ResponsiveActions';
31+
export * from './ResponsiveActions';
32+
33+
export { default as ResponsiveAction } from './ResponsiveAction';
34+
export * from './ResponsiveAction';
35+
3036
export { default as NotFoundIcon } from './NotFoundIcon';
3137
export * from './NotFoundIcon';
3238

0 commit comments

Comments
 (0)