From 8062a9dd353fb502037c861b420d26a79cd5b59c Mon Sep 17 00:00:00 2001 From: Robb Hamilton Date: Wed, 6 May 2026 17:19:57 -0400 Subject: [PATCH] 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) --- .../ResponsiveActions.test.tsx | 77 +++++ .../ResponsiveActions/ResponsiveActions.tsx | 93 ++++-- .../ResponsiveActions.test.tsx.snap | 308 ++++++++++++++++++ 3 files changed, 455 insertions(+), 23 deletions(-) diff --git a/packages/module/src/ResponsiveActions/ResponsiveActions.test.tsx b/packages/module/src/ResponsiveActions/ResponsiveActions.test.tsx index f3c29fec..41f2bfae 100644 --- a/packages/module/src/ResponsiveActions/ResponsiveActions.test.tsx +++ b/packages/module/src/ResponsiveActions/ResponsiveActions.test.tsx @@ -56,5 +56,82 @@ describe('ResponsiveActions component', () => { expect(buttons).toHaveLength(2); expect(container).toMatchSnapshot(); }); + + test('ResponsiveActions with all dropdown items disabled should disable kebab', () => { + const { container } = render( + + Disabled action 1 + Disabled action 2 + ); + + // Kebab toggle should be disabled when all dropdown items are disabled + const kebabToggle = container.querySelector('[data-ouia-component-id="ResponsiveActions-menu-dropdown-toggle"]'); + expect(kebabToggle).toHaveAttribute('disabled'); + expect(container).toMatchSnapshot(); + }); + + test('ResponsiveActions with some enabled dropdown items should not disable kebab', () => { + const { container } = render( + + Disabled action + Enabled action + ); + + // Kebab toggle should be enabled when at least one dropdown item is enabled + const kebabToggle = container.querySelector('[data-ouia-component-id="ResponsiveActions-menu-dropdown-toggle"]'); + expect(kebabToggle).not.toHaveAttribute('disabled'); + expect(container).toMatchSnapshot(); + }); + + test('ResponsiveActions with enabled pinned item and disabled regular item should disable kebab above breakpoint', () => { + const { container } = render( + + Enabled pinned action + Disabled regular action + ); + + // Above breakpoint: pinned items show as buttons, so kebab is disabled if regular items are disabled + // (When resized below breakpoint, the pinned item moves into kebab and it becomes enabled) + const kebabToggle = container.querySelector('[data-ouia-component-id="ResponsiveActions-menu-dropdown-toggle"]'); + expect(kebabToggle).toHaveAttribute('disabled'); + expect(container).toMatchSnapshot(); + }); + + test('ResponsiveActions with enabled pinned item and enabled regular item should not disable kebab', () => { + const { container } = render( + + Enabled pinned action + Enabled regular action + ); + + // Kebab should be enabled because there's an enabled regular action + const kebabToggle = container.querySelector('[data-ouia-component-id="ResponsiveActions-menu-dropdown-toggle"]'); + expect(kebabToggle).not.toHaveAttribute('disabled'); + expect(container).toMatchSnapshot(); + }); + + test('ResponsiveActions with all dropdown items disabled including pinned should disable kebab', () => { + const { container } = render( + + Disabled pinned action + Disabled action + ); + + // Kebab toggle should be disabled when all dropdown items (including pinned) are disabled + const kebabToggle = container.querySelector('[data-ouia-component-id="ResponsiveActions-menu-dropdown-toggle"]'); + expect(kebabToggle).toHaveAttribute('disabled'); + expect(container).toMatchSnapshot(); + }); + + test('ResponsiveActions with only persistent items should not render kebab', () => { + const { container } = render( + + Persistent action + ); + + // Should not have kebab when only persistent items exist + const kebabToggle = container.querySelector('[data-ouia-component-id="ResponsiveActions-menu-dropdown-toggle"]'); + expect(kebabToggle).toBeNull(); + }); }); }); \ No newline at end of file diff --git a/packages/module/src/ResponsiveActions/ResponsiveActions.tsx b/packages/module/src/ResponsiveActions/ResponsiveActions.tsx index a7e1c5f7..53ce101b 100644 --- a/packages/module/src/ResponsiveActions/ResponsiveActions.tsx +++ b/packages/module/src/ResponsiveActions/ResponsiveActions.tsx @@ -1,8 +1,9 @@ import type { ReactNode, FunctionComponent } from 'react'; -import { Children, isValidElement, useState } from 'react'; +import { Children, isValidElement, useState, useContext } 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'; +import { OverflowMenuContext } from '@patternfly/react-core/dist/esm/components/OverflowMenu/OverflowMenuContext'; /** extends OverflowMenuProps */ export interface ResponsiveActionsProps extends Omit { @@ -14,13 +15,68 @@ export interface ResponsiveActionsProps extends Omit = ({ ouiaId = 'ResponsiveActions', breakpoint = 'lg', children, ...props }: ResponsiveActionsProps) => { +// Inner component that has access to OverflowMenuContext +const ResponsiveActionsDropdown: FunctionComponent<{ + ouiaId: string; + dropdownItems: ReactNode[]; + pinnedItemsDisabled: boolean[]; + regularItemsDisabled: boolean[]; +}> = ({ ouiaId, dropdownItems, pinnedItemsDisabled, regularItemsDisabled }) => { const [ isOpen, setIsOpen ] = useState(false); + const { isBelowBreakpoint } = useContext(OverflowMenuContext); + + // Determine if kebab should be disabled based on breakpoint + const isKebabDisabled = (() => { + const allPinnedDisabled = pinnedItemsDisabled.length > 0 && pinnedItemsDisabled.every(disabled => disabled); + const allRegularDisabled = regularItemsDisabled.length > 0 && regularItemsDisabled.every(disabled => disabled); + + if (isBelowBreakpoint) { + // Below breakpoint: pinned items are IN the dropdown, so check all dropdown items + // Disabled only if both pinned AND regular items exist and are all disabled + return (pinnedItemsDisabled.length > 0 || regularItemsDisabled.length > 0) && + (pinnedItemsDisabled.length === 0 || allPinnedDisabled) && + (regularItemsDisabled.length === 0 || allRegularDisabled); + } else { + // Above breakpoint: pinned items are shown as buttons, only check regular items + // Disabled only if there are regular items and they're all disabled + return allRegularDisabled; + } + })(); + + return ( + setIsOpen(false)} + toggle={(toggleRef) => ( + } + onClick={() => setIsOpen(!isOpen)} + isExpanded={isOpen} + isDisabled={isKebabDisabled} + /> + )} + isOpen={isOpen} + onOpenChange={setIsOpen} + > + + {dropdownItems} + + + ); +}; + +export const ResponsiveActions: FunctionComponent = ({ ouiaId = 'ResponsiveActions', breakpoint = 'lg', children, ...props }: ResponsiveActionsProps) => { // separate persistent, pinned and collapsed actions const persistentActions: ReactNode[] = []; const pinnedActions: ReactNode[] = []; const dropdownItems: ReactNode[] = []; + const pinnedItemsDisabled: boolean[] = []; + const regularItemsDisabled: boolean[] = []; let hasRegularActions = false; Children.forEach(children, (child, index) => { @@ -47,6 +103,12 @@ export const ResponsiveActions: FunctionComponent = ({ o {children} ); + // Track disabled state separately for pinned vs regular items + if (isPinned) { + pinnedItemsDisabled.push(!!actionProps.isDisabled); + } else { + regularItemsDisabled.push(!!actionProps.isDisabled); + } } } }); @@ -74,27 +136,12 @@ export const ResponsiveActions: FunctionComponent = ({ o ) : null} {dropdownItems.length > 0 && ( - setIsOpen(false)} - toggle={(toggleRef) => ( - } - onClick={() => setIsOpen(!isOpen)} - isExpanded={isOpen} - /> - )} - isOpen={isOpen} - onOpenChange={setIsOpen} - > - - {dropdownItems} - - + )} diff --git a/packages/module/src/ResponsiveActions/__snapshots__/ResponsiveActions.test.tsx.snap b/packages/module/src/ResponsiveActions/__snapshots__/ResponsiveActions.test.tsx.snap index 7527af80..d8100a72 100644 --- a/packages/module/src/ResponsiveActions/__snapshots__/ResponsiveActions.test.tsx.snap +++ b/packages/module/src/ResponsiveActions/__snapshots__/ResponsiveActions.test.tsx.snap @@ -100,6 +100,270 @@ exports[`ResponsiveActions component should render correctly ResponsiveActions 1 `; +exports[`ResponsiveActions component should render correctly ResponsiveActions with all dropdown items disabled including pinned should disable kebab 1`] = ` +
+
+
+
+
+ + + +
+
+
+
+ + + +
+
+
+`; + +exports[`ResponsiveActions component should render correctly ResponsiveActions with all dropdown items disabled should disable kebab 1`] = ` +
+
+
+ + + +
+
+
+`; + +exports[`ResponsiveActions component should render correctly ResponsiveActions with enabled pinned item and disabled regular item should disable kebab above breakpoint 1`] = ` +
+
+
+
+
+ + + +
+
+
+
+ + + +
+
+
+`; + +exports[`ResponsiveActions component should render correctly ResponsiveActions with enabled pinned item and enabled regular item should not disable kebab 1`] = ` +
+
+
+
+
+ + + +
+
+
+
+ + + +
+
+
+`; + exports[`ResponsiveActions component should render correctly ResponsiveActions with mix of isPersistent and isPinned actions 1`] = `
`; + +exports[`ResponsiveActions component should render correctly ResponsiveActions with some enabled dropdown items should not disable kebab 1`] = ` +
+
+
+ + + +
+
+
+`;