Skip to content

Commit eb32c46

Browse files
rhamiltoclaude
andauthored
fix(ResponsiveActions): Disable kebab when all actions are disabled (patternfly#928)
* fix(ResponsiveActions): Disable kebab when all actions are disabled Fixes patternfly#927 - Uses OverflowMenuContext to access isBelowBreakpoint state - Kebab disabled state is now responsive to viewport width: - Above breakpoint: disabled if all regular items are disabled - Below breakpoint: disabled if all items (pinned + regular) are disabled - Created ResponsiveActionsDropdown component to access context - Tracks disabled state separately for pinned vs regular items - Added comprehensive test coverage for all scenarios - Fully backward compatible (no breaking changes) * refactor: remove unnecessary comments from ResponsiveActions Address PR review feedback by removing unnecessary comments that don't add value beyond what the code already expresses. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * test(ResponsiveActions): use RTL conventions and remove unnecessary snapshots Address PR review feedback: - Replace container.querySelector with screen.getByRole('button') queries - Remove snapshot tests from disabled state tests (structure is tested by other tests) - Remove container destructuring where no longer needed - Use toBeDisabled()/toBeEnabled() instead of toHaveAttribute('disabled') Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent c42df1f commit eb32c46

2 files changed

Lines changed: 130 additions & 25 deletions

File tree

packages/module/src/ResponsiveActions/ResponsiveActions.test.tsx

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { render } from '@testing-library/react';
1+
import { render, screen } from '@testing-library/react';
22
import ResponsiveActions from './ResponsiveActions';
33
import ResponsiveAction from '../ResponsiveAction';
44

@@ -56,5 +56,71 @@ describe('ResponsiveActions component', () => {
5656
expect(buttons).toHaveLength(2);
5757
expect(container).toMatchSnapshot();
5858
});
59+
60+
test('ResponsiveActions with all dropdown items disabled should disable kebab', () => {
61+
render(
62+
<ResponsiveActions breakpoint="lg">
63+
<ResponsiveAction isDisabled>Disabled action 1</ResponsiveAction>
64+
<ResponsiveAction isDisabled>Disabled action 2</ResponsiveAction>
65+
</ResponsiveActions>);
66+
67+
const kebabToggle = screen.getByRole('button', { name: /actions overflow menu/i });
68+
expect(kebabToggle).toBeDisabled();
69+
});
70+
71+
test('ResponsiveActions with some enabled dropdown items should not disable kebab', () => {
72+
render(
73+
<ResponsiveActions breakpoint="lg">
74+
<ResponsiveAction isDisabled>Disabled action</ResponsiveAction>
75+
<ResponsiveAction>Enabled action</ResponsiveAction>
76+
</ResponsiveActions>);
77+
78+
const kebabToggle = screen.getByRole('button', { name: /actions overflow menu/i });
79+
expect(kebabToggle).toBeEnabled();
80+
});
81+
82+
test('ResponsiveActions with enabled pinned item and disabled regular item should disable kebab above breakpoint', () => {
83+
render(
84+
<ResponsiveActions breakpoint="lg">
85+
<ResponsiveAction isPinned>Enabled pinned action</ResponsiveAction>
86+
<ResponsiveAction isDisabled>Disabled regular action</ResponsiveAction>
87+
</ResponsiveActions>);
88+
89+
const kebabToggle = screen.getByRole('button', { name: /actions overflow menu/i });
90+
expect(kebabToggle).toBeDisabled();
91+
});
92+
93+
test('ResponsiveActions with enabled pinned item and enabled regular item should not disable kebab', () => {
94+
render(
95+
<ResponsiveActions breakpoint="lg">
96+
<ResponsiveAction isPinned>Enabled pinned action</ResponsiveAction>
97+
<ResponsiveAction>Enabled regular action</ResponsiveAction>
98+
</ResponsiveActions>);
99+
100+
const kebabToggle = screen.getByRole('button', { name: /actions overflow menu/i });
101+
expect(kebabToggle).toBeEnabled();
102+
});
103+
104+
test('ResponsiveActions with all dropdown items disabled including pinned should disable kebab', () => {
105+
render(
106+
<ResponsiveActions breakpoint="lg">
107+
<ResponsiveAction isPinned isDisabled>Disabled pinned action</ResponsiveAction>
108+
<ResponsiveAction isDisabled>Disabled action</ResponsiveAction>
109+
</ResponsiveActions>);
110+
111+
const kebabToggle = screen.getByRole('button', { name: /actions overflow menu/i });
112+
expect(kebabToggle).toBeDisabled();
113+
});
114+
115+
test('ResponsiveActions with only persistent items should not render kebab', () => {
116+
const { container } = render(
117+
<ResponsiveActions breakpoint="lg">
118+
<ResponsiveAction isPersistent>Persistent action</ResponsiveAction>
119+
</ResponsiveActions>);
120+
121+
// Should not have kebab when only persistent items exist
122+
const kebabToggle = container.querySelector('[data-ouia-component-id="ResponsiveActions-menu-dropdown-toggle"]');
123+
expect(kebabToggle).toBeNull();
124+
});
59125
});
60126
});

packages/module/src/ResponsiveActions/ResponsiveActions.tsx

Lines changed: 63 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import type { ReactNode, FunctionComponent } from 'react';
2-
import { Children, isValidElement, useState } from 'react';
2+
import { Children, isValidElement, useState, useContext } from 'react';
33
import { Button, Dropdown, DropdownList, MenuToggle, OverflowMenu, OverflowMenuContent, OverflowMenuControl, OverflowMenuDropdownItem, OverflowMenuGroup, OverflowMenuItem, OverflowMenuProps } from '@patternfly/react-core';
44
import { EllipsisVIcon } from '@patternfly/react-icons';
55
import { ResponsiveActionProps } from '../ResponsiveAction';
6+
import { OverflowMenuContext } from '@patternfly/react-core/dist/esm/components/OverflowMenu/OverflowMenuContext';
67

78
/** extends OverflowMenuProps */
89
export interface ResponsiveActionsProps extends Omit<OverflowMenuProps, 'ref' | 'breakpoint'> {
@@ -14,13 +15,62 @@ export interface ResponsiveActionsProps extends Omit<OverflowMenuProps, 'ref' |
1415
children: React.ReactNode;
1516
}
1617

17-
export const ResponsiveActions: FunctionComponent<ResponsiveActionsProps> = ({ ouiaId = 'ResponsiveActions', breakpoint = 'lg', children, ...props }: ResponsiveActionsProps) => {
18+
const ResponsiveActionsDropdown: FunctionComponent<{
19+
ouiaId: string;
20+
dropdownItems: ReactNode[];
21+
pinnedItemsDisabled: boolean[];
22+
regularItemsDisabled: boolean[];
23+
}> = ({ ouiaId, dropdownItems, pinnedItemsDisabled, regularItemsDisabled }) => {
1824
const [ isOpen, setIsOpen ] = useState(false);
25+
const { isBelowBreakpoint } = useContext(OverflowMenuContext);
26+
27+
const isKebabDisabled = (() => {
28+
const allPinnedDisabled = pinnedItemsDisabled.length > 0 && pinnedItemsDisabled.every(disabled => disabled);
29+
const allRegularDisabled = regularItemsDisabled.length > 0 && regularItemsDisabled.every(disabled => disabled);
30+
31+
if (isBelowBreakpoint) {
32+
return (pinnedItemsDisabled.length > 0 || regularItemsDisabled.length > 0) &&
33+
(pinnedItemsDisabled.length === 0 || allPinnedDisabled) &&
34+
(regularItemsDisabled.length === 0 || allRegularDisabled);
35+
} else {
36+
return allRegularDisabled;
37+
}
38+
})();
39+
40+
return (
41+
<Dropdown
42+
ouiaId={`${ouiaId}-menu-dropdown`}
43+
onSelect={() => setIsOpen(false)}
44+
toggle={(toggleRef) => (
45+
<MenuToggle
46+
ouiaId={`${ouiaId}-menu-dropdown-toggle`}
47+
ref={toggleRef}
48+
aria-label="Actions overflow menu"
49+
variant="plain"
50+
icon={<EllipsisVIcon />}
51+
onClick={() => setIsOpen(!isOpen)}
52+
isExpanded={isOpen}
53+
isDisabled={isKebabDisabled}
54+
/>
55+
)}
56+
isOpen={isOpen}
57+
onOpenChange={setIsOpen}
58+
>
59+
<DropdownList data-ouia-component-id={`${ouiaId}-menu-dropdown-list`}>
60+
{dropdownItems}
61+
</DropdownList>
62+
</Dropdown>
63+
);
64+
};
65+
66+
export const ResponsiveActions: FunctionComponent<ResponsiveActionsProps> = ({ ouiaId = 'ResponsiveActions', breakpoint = 'lg', children, ...props }: ResponsiveActionsProps) => {
1967

2068
// separate persistent, pinned and collapsed actions
2169
const persistentActions: ReactNode[] = [];
2270
const pinnedActions: ReactNode[] = [];
2371
const dropdownItems: ReactNode[] = [];
72+
const pinnedItemsDisabled: boolean[] = [];
73+
const regularItemsDisabled: boolean[] = [];
2474
let hasRegularActions = false;
2575

2676
Children.forEach(children, (child, index) => {
@@ -37,7 +87,6 @@ export const ResponsiveActions: FunctionComponent<ResponsiveActionsProps> = ({ o
3787
</OverflowMenuItem>
3888
);
3989
} else {
40-
// Track if there are any regular (non-persistent, non-pinned) actions
4190
hasRegularActions = true;
4291
}
4392

@@ -47,6 +96,11 @@ export const ResponsiveActions: FunctionComponent<ResponsiveActionsProps> = ({ o
4796
{children}
4897
</OverflowMenuDropdownItem>
4998
);
99+
if (isPinned) {
100+
pinnedItemsDisabled.push(!!actionProps.isDisabled);
101+
} else {
102+
regularItemsDisabled.push(!!actionProps.isDisabled);
103+
}
50104
}
51105
}
52106
});
@@ -74,27 +128,12 @@ export const ResponsiveActions: FunctionComponent<ResponsiveActionsProps> = ({ o
74128
) : null}
75129
{dropdownItems.length > 0 && (
76130
<OverflowMenuControl hasAdditionalOptions={hasRegularActions} data-ouia-component-id={`${ouiaId}-menu-control`}>
77-
<Dropdown
78-
ouiaId={`${ouiaId}-menu-dropdown`}
79-
onSelect={() => setIsOpen(false)}
80-
toggle={(toggleRef) => (
81-
<MenuToggle
82-
ouiaId={`${ouiaId}-menu-dropdown-toggle`}
83-
ref={toggleRef}
84-
aria-label="Actions overflow menu"
85-
variant="plain"
86-
icon={<EllipsisVIcon />}
87-
onClick={() => setIsOpen(!isOpen)}
88-
isExpanded={isOpen}
89-
/>
90-
)}
91-
isOpen={isOpen}
92-
onOpenChange={setIsOpen}
93-
>
94-
<DropdownList data-ouia-component-id={`${ouiaId}-menu-dropdown-list`}>
95-
{dropdownItems}
96-
</DropdownList>
97-
</Dropdown>
131+
<ResponsiveActionsDropdown
132+
ouiaId={ouiaId}
133+
dropdownItems={dropdownItems}
134+
pinnedItemsDisabled={pinnedItemsDisabled}
135+
regularItemsDisabled={regularItemsDisabled}
136+
/>
98137
</OverflowMenuControl>
99138
)}
100139
</OverflowMenu>

0 commit comments

Comments
 (0)