Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions packages/module/src/ResponsiveActions/ResponsiveActions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<ResponsiveActions breakpoint="lg">
<ResponsiveAction isDisabled>Disabled action 1</ResponsiveAction>
<ResponsiveAction isDisabled>Disabled action 2</ResponsiveAction>
</ResponsiveActions>);

// 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(
<ResponsiveActions breakpoint="lg">
<ResponsiveAction isDisabled>Disabled action</ResponsiveAction>
<ResponsiveAction>Enabled action</ResponsiveAction>
</ResponsiveActions>);

// 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(
<ResponsiveActions breakpoint="lg">
<ResponsiveAction isPinned>Enabled pinned action</ResponsiveAction>
<ResponsiveAction isDisabled>Disabled regular action</ResponsiveAction>
</ResponsiveActions>);

// 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(
<ResponsiveActions breakpoint="lg">
<ResponsiveAction isPinned>Enabled pinned action</ResponsiveAction>
<ResponsiveAction>Enabled regular action</ResponsiveAction>
</ResponsiveActions>);

// 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(
<ResponsiveActions breakpoint="lg">
<ResponsiveAction isPinned isDisabled>Disabled pinned action</ResponsiveAction>
<ResponsiveAction isDisabled>Disabled action</ResponsiveAction>
</ResponsiveActions>);

// 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(
<ResponsiveActions breakpoint="lg">
<ResponsiveAction isPersistent>Persistent action</ResponsiveAction>
</ResponsiveActions>);

// Should not have kebab when only persistent items exist
const kebabToggle = container.querySelector('[data-ouia-component-id="ResponsiveActions-menu-dropdown-toggle"]');
expect(kebabToggle).toBeNull();
});
});
});
93 changes: 70 additions & 23 deletions packages/module/src/ResponsiveActions/ResponsiveActions.tsx
Original file line number Diff line number Diff line change
@@ -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<OverflowMenuProps, 'ref' | 'breakpoint'> {
Expand All @@ -14,13 +15,68 @@ export interface ResponsiveActionsProps extends Omit<OverflowMenuProps, 'ref' |
children: React.ReactNode;
}

export const ResponsiveActions: FunctionComponent<ResponsiveActionsProps> = ({ 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 (
<Dropdown
ouiaId={`${ouiaId}-menu-dropdown`}
onSelect={() => setIsOpen(false)}
toggle={(toggleRef) => (
<MenuToggle
ouiaId={`${ouiaId}-menu-dropdown-toggle`}
ref={toggleRef}
aria-label="Actions overflow menu"
variant="plain"
icon={<EllipsisVIcon />}
onClick={() => setIsOpen(!isOpen)}
isExpanded={isOpen}
isDisabled={isKebabDisabled}
/>
)}
isOpen={isOpen}
onOpenChange={setIsOpen}
>
<DropdownList data-ouia-component-id={`${ouiaId}-menu-dropdown-list`}>
{dropdownItems}
</DropdownList>
</Dropdown>
);
};

export const ResponsiveActions: FunctionComponent<ResponsiveActionsProps> = ({ 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) => {
Expand All @@ -47,6 +103,12 @@ export const ResponsiveActions: FunctionComponent<ResponsiveActionsProps> = ({ o
{children}
</OverflowMenuDropdownItem>
);
// Track disabled state separately for pinned vs regular items
if (isPinned) {
pinnedItemsDisabled.push(!!actionProps.isDisabled);
} else {
regularItemsDisabled.push(!!actionProps.isDisabled);
}
}
}
});
Expand Down Expand Up @@ -74,27 +136,12 @@ export const ResponsiveActions: FunctionComponent<ResponsiveActionsProps> = ({ o
) : null}
{dropdownItems.length > 0 && (
<OverflowMenuControl hasAdditionalOptions={hasRegularActions} data-ouia-component-id={`${ouiaId}-menu-control`}>
<Dropdown
ouiaId={`${ouiaId}-menu-dropdown`}
onSelect={() => setIsOpen(false)}
toggle={(toggleRef) => (
<MenuToggle
ouiaId={`${ouiaId}-menu-dropdown-toggle`}
ref={toggleRef}
aria-label="Actions overflow menu"
variant="plain"
icon={<EllipsisVIcon />}
onClick={() => setIsOpen(!isOpen)}
isExpanded={isOpen}
/>
)}
isOpen={isOpen}
onOpenChange={setIsOpen}
>
<DropdownList data-ouia-component-id={`${ouiaId}-menu-dropdown-list`}>
{dropdownItems}
</DropdownList>
</Dropdown>
<ResponsiveActionsDropdown
ouiaId={ouiaId}
dropdownItems={dropdownItems}
pinnedItemsDisabled={pinnedItemsDisabled}
regularItemsDisabled={regularItemsDisabled}
/>
</OverflowMenuControl>
)}
</OverflowMenu>
Expand Down
Loading
Loading