Skip to content

Commit 8062a9d

Browse files
committed
fix(ResponsiveActions): Disable kebab when all actions are disabled
Fixes #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)
1 parent fcdf86f commit 8062a9d

3 files changed

Lines changed: 455 additions & 23 deletions

File tree

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

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,82 @@ 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+
const { container } = render(
62+
<ResponsiveActions breakpoint="lg">
63+
<ResponsiveAction isDisabled>Disabled action 1</ResponsiveAction>
64+
<ResponsiveAction isDisabled>Disabled action 2</ResponsiveAction>
65+
</ResponsiveActions>);
66+
67+
// Kebab toggle should be disabled when all dropdown items are disabled
68+
const kebabToggle = container.querySelector('[data-ouia-component-id="ResponsiveActions-menu-dropdown-toggle"]');
69+
expect(kebabToggle).toHaveAttribute('disabled');
70+
expect(container).toMatchSnapshot();
71+
});
72+
73+
test('ResponsiveActions with some enabled dropdown items should not disable kebab', () => {
74+
const { container } = render(
75+
<ResponsiveActions breakpoint="lg">
76+
<ResponsiveAction isDisabled>Disabled action</ResponsiveAction>
77+
<ResponsiveAction>Enabled action</ResponsiveAction>
78+
</ResponsiveActions>);
79+
80+
// Kebab toggle should be enabled when at least one dropdown item is enabled
81+
const kebabToggle = container.querySelector('[data-ouia-component-id="ResponsiveActions-menu-dropdown-toggle"]');
82+
expect(kebabToggle).not.toHaveAttribute('disabled');
83+
expect(container).toMatchSnapshot();
84+
});
85+
86+
test('ResponsiveActions with enabled pinned item and disabled regular item should disable kebab above breakpoint', () => {
87+
const { container } = render(
88+
<ResponsiveActions breakpoint="lg">
89+
<ResponsiveAction isPinned>Enabled pinned action</ResponsiveAction>
90+
<ResponsiveAction isDisabled>Disabled regular action</ResponsiveAction>
91+
</ResponsiveActions>);
92+
93+
// Above breakpoint: pinned items show as buttons, so kebab is disabled if regular items are disabled
94+
// (When resized below breakpoint, the pinned item moves into kebab and it becomes enabled)
95+
const kebabToggle = container.querySelector('[data-ouia-component-id="ResponsiveActions-menu-dropdown-toggle"]');
96+
expect(kebabToggle).toHaveAttribute('disabled');
97+
expect(container).toMatchSnapshot();
98+
});
99+
100+
test('ResponsiveActions with enabled pinned item and enabled regular item should not disable kebab', () => {
101+
const { container } = render(
102+
<ResponsiveActions breakpoint="lg">
103+
<ResponsiveAction isPinned>Enabled pinned action</ResponsiveAction>
104+
<ResponsiveAction>Enabled regular action</ResponsiveAction>
105+
</ResponsiveActions>);
106+
107+
// Kebab should be enabled because there's an enabled regular action
108+
const kebabToggle = container.querySelector('[data-ouia-component-id="ResponsiveActions-menu-dropdown-toggle"]');
109+
expect(kebabToggle).not.toHaveAttribute('disabled');
110+
expect(container).toMatchSnapshot();
111+
});
112+
113+
test('ResponsiveActions with all dropdown items disabled including pinned should disable kebab', () => {
114+
const { container } = render(
115+
<ResponsiveActions breakpoint="lg">
116+
<ResponsiveAction isPinned isDisabled>Disabled pinned action</ResponsiveAction>
117+
<ResponsiveAction isDisabled>Disabled action</ResponsiveAction>
118+
</ResponsiveActions>);
119+
120+
// Kebab toggle should be disabled when all dropdown items (including pinned) are disabled
121+
const kebabToggle = container.querySelector('[data-ouia-component-id="ResponsiveActions-menu-dropdown-toggle"]');
122+
expect(kebabToggle).toHaveAttribute('disabled');
123+
expect(container).toMatchSnapshot();
124+
});
125+
126+
test('ResponsiveActions with only persistent items should not render kebab', () => {
127+
const { container } = render(
128+
<ResponsiveActions breakpoint="lg">
129+
<ResponsiveAction isPersistent>Persistent action</ResponsiveAction>
130+
</ResponsiveActions>);
131+
132+
// Should not have kebab when only persistent items exist
133+
const kebabToggle = container.querySelector('[data-ouia-component-id="ResponsiveActions-menu-dropdown-toggle"]');
134+
expect(kebabToggle).toBeNull();
135+
});
59136
});
60137
});

packages/module/src/ResponsiveActions/ResponsiveActions.tsx

Lines changed: 70 additions & 23 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,68 @@ 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+
// Inner component that has access to OverflowMenuContext
19+
const ResponsiveActionsDropdown: FunctionComponent<{
20+
ouiaId: string;
21+
dropdownItems: ReactNode[];
22+
pinnedItemsDisabled: boolean[];
23+
regularItemsDisabled: boolean[];
24+
}> = ({ ouiaId, dropdownItems, pinnedItemsDisabled, regularItemsDisabled }) => {
1825
const [ isOpen, setIsOpen ] = useState(false);
26+
const { isBelowBreakpoint } = useContext(OverflowMenuContext);
27+
28+
// Determine if kebab should be disabled based on breakpoint
29+
const isKebabDisabled = (() => {
30+
const allPinnedDisabled = pinnedItemsDisabled.length > 0 && pinnedItemsDisabled.every(disabled => disabled);
31+
const allRegularDisabled = regularItemsDisabled.length > 0 && regularItemsDisabled.every(disabled => disabled);
32+
33+
if (isBelowBreakpoint) {
34+
// Below breakpoint: pinned items are IN the dropdown, so check all dropdown items
35+
// Disabled only if both pinned AND regular items exist and are all disabled
36+
return (pinnedItemsDisabled.length > 0 || regularItemsDisabled.length > 0) &&
37+
(pinnedItemsDisabled.length === 0 || allPinnedDisabled) &&
38+
(regularItemsDisabled.length === 0 || allRegularDisabled);
39+
} else {
40+
// Above breakpoint: pinned items are shown as buttons, only check regular items
41+
// Disabled only if there are regular items and they're all disabled
42+
return allRegularDisabled;
43+
}
44+
})();
45+
46+
return (
47+
<Dropdown
48+
ouiaId={`${ouiaId}-menu-dropdown`}
49+
onSelect={() => setIsOpen(false)}
50+
toggle={(toggleRef) => (
51+
<MenuToggle
52+
ouiaId={`${ouiaId}-menu-dropdown-toggle`}
53+
ref={toggleRef}
54+
aria-label="Actions overflow menu"
55+
variant="plain"
56+
icon={<EllipsisVIcon />}
57+
onClick={() => setIsOpen(!isOpen)}
58+
isExpanded={isOpen}
59+
isDisabled={isKebabDisabled}
60+
/>
61+
)}
62+
isOpen={isOpen}
63+
onOpenChange={setIsOpen}
64+
>
65+
<DropdownList data-ouia-component-id={`${ouiaId}-menu-dropdown-list`}>
66+
{dropdownItems}
67+
</DropdownList>
68+
</Dropdown>
69+
);
70+
};
71+
72+
export const ResponsiveActions: FunctionComponent<ResponsiveActionsProps> = ({ ouiaId = 'ResponsiveActions', breakpoint = 'lg', children, ...props }: ResponsiveActionsProps) => {
1973

2074
// separate persistent, pinned and collapsed actions
2175
const persistentActions: ReactNode[] = [];
2276
const pinnedActions: ReactNode[] = [];
2377
const dropdownItems: ReactNode[] = [];
78+
const pinnedItemsDisabled: boolean[] = [];
79+
const regularItemsDisabled: boolean[] = [];
2480
let hasRegularActions = false;
2581

2682
Children.forEach(children, (child, index) => {
@@ -47,6 +103,12 @@ export const ResponsiveActions: FunctionComponent<ResponsiveActionsProps> = ({ o
47103
{children}
48104
</OverflowMenuDropdownItem>
49105
);
106+
// Track disabled state separately for pinned vs regular items
107+
if (isPinned) {
108+
pinnedItemsDisabled.push(!!actionProps.isDisabled);
109+
} else {
110+
regularItemsDisabled.push(!!actionProps.isDisabled);
111+
}
50112
}
51113
}
52114
});
@@ -74,27 +136,12 @@ export const ResponsiveActions: FunctionComponent<ResponsiveActionsProps> = ({ o
74136
) : null}
75137
{dropdownItems.length > 0 && (
76138
<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>
139+
<ResponsiveActionsDropdown
140+
ouiaId={ouiaId}
141+
dropdownItems={dropdownItems}
142+
pinnedItemsDisabled={pinnedItemsDisabled}
143+
regularItemsDisabled={regularItemsDisabled}
144+
/>
98145
</OverflowMenuControl>
99146
)}
100147
</OverflowMenu>

0 commit comments

Comments
 (0)