Skip to content

Commit

Permalink
feat(action-popover): align component with design system
Browse files Browse the repository at this point in the history
aligns component with the sage design system, to ensure consumers can use the component with
expected styling and functionality. Nine different styling variations have been added
which change the alignment and padding of each child to ensure designs are correct. Also,
support for submenu's opening on the right has been included with the addition of the
`submenuPosition` prop. When set to "right" the menu-item chevron will appear on the right
of the action popover menu, and any submenu's will open from the right-hand side. Also,
when the `submenuPosition` prop is "left", either by default, or set by the consumer and
a content is re-sized the `submenuPosition` will be temporarily set to "right" if the
window is deemed to be too small. Finally, icon's are now supported in submenu's and can
be included by passing the `icon` prop to the submenu's menu item.

fix #5801
  • Loading branch information
tomdavies73 committed Jun 21, 2023
1 parent 40ac949 commit e9acae8
Show file tree
Hide file tree
Showing 10 changed files with 2,291 additions and 125 deletions.
650 changes: 640 additions & 10 deletions cypress/components/action-popover/action-popover.cy.js

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions cypress/locators/action-popover/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import {
ACTION_POPOVER_BUTTON,
ACTION_POPOVER_DATA_COMPONENT,
ACTION_POPOVER_SUBMENU,
ACTION_POPOVER_MENU_ITEM_ICON,
ACTION_POPOVER_MENU_ITEM_INNER_TEXT,
ACTION_POPOVER_MENU_ITEM_CHEVRON,
ACTION_POPOVER_WRAPPER,
} from "./locators";

Expand All @@ -18,6 +21,12 @@ export const actionPopoverInnerItem = (index) =>
.first();
export const actionPopoverSubmenu = (index) =>
cy.get(ACTION_POPOVER_SUBMENU).eq(1).children().eq(index).find("button");
export const actionPopoverMenuItemIcon = () =>
cy.get(ACTION_POPOVER_MENU_ITEM_ICON);
export const actionPopoverMenuItemInnerText = () =>
cy.get(ACTION_POPOVER_MENU_ITEM_INNER_TEXT);
export const actionPopoverMenuItemChevron = () =>
cy.get(ACTION_POPOVER_MENU_ITEM_CHEVRON);
export const actionPopoverSubmenuByIndex = () =>
cy.get(ACTION_POPOVER_SUBMENU).eq(1);
export const actionPopoverWrapper = () => cy.get(ACTION_POPOVER_WRAPPER);
6 changes: 6 additions & 0 deletions cypress/locators/action-popover/locators.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,11 @@ export const ACTION_POPOVER_BUTTON = '[data-element="action-popover-button"]';
export const ACTION_POPOVER_DATA_COMPONENT =
'[data-component="action-popover"]';
export const ACTION_POPOVER_SUBMENU = '[data-element="action-popover-submenu"]';
export const ACTION_POPOVER_MENU_ITEM_ICON =
'[data-element="action-popover-menu-item-icon"]';
export const ACTION_POPOVER_MENU_ITEM_INNER_TEXT =
'[data-element="action-popover-menu-item-inner-text"]';
export const ACTION_POPOVER_MENU_ITEM_CHEVRON =
'[data-element="action-popover-menu-item-chevron"]';
export const ACTION_POPOVER_WRAPPER =
'[data-component="action-popover-wrapper"]';
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
MenuItemIcon,
SubMenuItemIcon,
StyledMenuItem,
StyledMenuItemInnerText,
StyledMenuItemOuterContainer,
StyledMenuItemWrapper,
} from "../action-popover.style";
import Events from "../../../__internal__/utils/helpers/events";
Expand Down Expand Up @@ -48,15 +50,27 @@ export interface ActionPopoverItemProps {
focusItem?: boolean;
/** @ignore @private */
horizontalAlignment?: "left" | "right";
/** @ignore @private */
submenuPosition?: "left" | "right";
/** @ignore @private */
caretState?: boolean;
/** @ignore @private */
iconState?: boolean;
/** @ignore @private */
setCaretState?: (value: boolean) => void;
/** @ignore @private */
setIconState?: (value: boolean) => void;
/** @ignore @private */
isASubmenu?: boolean;
}

const INTERVAL = 150;

type ContainerPosition = {
left: number;
left: string | number;
top?: string;
bottom?: string;
right: "auto";
right: string | number;
};

function checkRef(ref: React.RefObject<HTMLElement>) {
Expand All @@ -76,28 +90,6 @@ function leftAlignSubmenu(
return left >= offsetWidth;
}

function getContainerPosition(
itemRef: React.RefObject<HTMLElement>,
submenuRef: React.RefObject<HTMLElement>,
placement: "bottom" | "top"
): ContainerPosition | undefined {
/* istanbul ignore if */
if (!itemRef.current || !submenuRef.current) return undefined;

const { offsetWidth: parentWidth } = itemRef.current;
const { offsetWidth: submenuWidth } = submenuRef.current;
const xPositionValue = leftAlignSubmenu(itemRef, submenuRef)
? -submenuWidth
: parentWidth;
const yPositionName = placement === "top" ? "bottom" : "top";

return {
left: xPositionValue,
[yPositionName]: "calc(-1 * var(--spacing100))",
right: "auto",
};
}

export const ActionPopoverItem = ({
children,
icon,
Expand All @@ -109,6 +101,12 @@ export const ActionPopoverItem = ({
download,
href,
horizontalAlignment,
submenuPosition,
caretState,
iconState,
setCaretState,
setIconState,
isASubmenu = false,
...rest
}: ActionPopoverItemProps) => {
const l = useLocale();
Expand Down Expand Up @@ -139,19 +137,94 @@ export const ActionPopoverItem = ({
const mouseEnterTimer = useRef<NodeJS.Timeout | null>(null);
const mouseLeaveTimer = useRef<NodeJS.Timeout | null>(null);

const currentSubmenuAlignment = () => {
if (submenuPosition === "left") {
if (isLeftAligned) {
return "left";
}
return "right";
}
return "right";
};

function getContainerPosition(
itemRef: React.RefObject<HTMLElement>,
containerSubmenuRef: React.RefObject<HTMLElement>,
containerPlacement: "bottom" | "top"
): ContainerPosition | undefined {
/* istanbul ignore if */
if (!itemRef.current || !containerSubmenuRef.current) return undefined;

const { offsetWidth: submenuWidth } = containerSubmenuRef.current;
const horizontalAlignmentValue = currentSubmenuAlignment() === "left";
const leftValue = horizontalAlignmentValue ? -submenuWidth : "auto";
const rightValue = horizontalAlignmentValue ? "auto" : -submenuWidth;
const yPositionName = containerPlacement === "top" ? "bottom" : "top";

return {
left: leftValue,
[yPositionName]: "calc(-1 * var(--spacing100))",
right: rightValue,
};
}

const retrieveVariantType = () => {
if (!iconState && !caretState && !icon && !submenu) {
return 1;
}
if (!iconState && caretState && !icon && !submenu) {
return 2;
}
if (!iconState && caretState && !icon && submenu) {
return 3;
}
if (iconState && !caretState && !icon && !submenu) {
return 4;
}
if (iconState && !caretState && icon && !submenu) {
return 5;
}
if (iconState && caretState && icon && submenu) {
return 6;
}
if (iconState && caretState && icon && !submenu) {
return 7;
}
if (iconState && caretState && !icon && submenu) {
return 8;
}
if (iconState && caretState && !icon && !submenu) {
return 9;
}
return 1;
};

useEffect(() => {
if (!isOpenPopover) {
setOpen(false);
}
}, [isOpenPopover]);

useEffect(() => {
if (icon) {
setIconState?.(true);
}
if (submenu) {
setCaretState?.(true);
}
}, [icon, setCaretState, setIconState, submenu, focusIndex]);

const alignSubmenu = useCallback(() => {
const align = leftAlignSubmenu(ref, submenuRef);
setIsLeftAligned(align);
return checkRef(ref) && checkRef(submenuRef) && submenu;
}, [submenu, placement]);

useEffect(() => {
if (checkRef(ref) && checkRef(submenuRef) && submenu) {
const align = leftAlignSubmenu(ref, submenuRef);
setIsLeftAligned(align);
setContainerPosition(getContainerPosition(ref, submenuRef, placement));
}
}, [submenu, placement]);
}, [submenu, getContainerPosition]);

useEffect(() => {
alignSubmenu();
Expand Down Expand Up @@ -205,7 +278,7 @@ export const ActionPopoverItem = ({
e.stopPropagation();
} else if (!disabled) {
if (submenu) {
if (isLeftAligned) {
if (currentSubmenuAlignment() === "left") {
// LEFT: open if has submenu and left aligned otherwise close submenu
if (Events.isLeftKey(e) || Events.isEnterKey(e)) {
setOpen(true);
Expand Down Expand Up @@ -242,7 +315,15 @@ export const ActionPopoverItem = ({
e.stopPropagation();
}
},
[disabled, download, isHref, isLeftAligned, onClick, submenu]
[
disabled,
download,
isHref,
isLeftAligned,
onClick,
submenu,
currentSubmenuAlignment,
]
);

const itemSubmenuProps = {
Expand Down Expand Up @@ -283,7 +364,19 @@ export const ActionPopoverItem = ({
};

const renderMenuItemIcon = () => {
return icon && <MenuItemIcon as={undefined} type={icon} />;
return (
icon && (
<MenuItemIcon
as={undefined}
type={icon}
data-element="action-popover-menu-item-icon"
horizontalAlignment={horizontalAlignment}
submenuPosition={currentSubmenuAlignment()}
isASubmenu={isASubmenu}
variant={retrieveVariantType()}
/>
)
);
};

return (
Expand All @@ -298,18 +391,37 @@ export const ActionPopoverItem = ({
tabIndex={0}
isDisabled={disabled}
horizontalAlignment={horizontalAlignment}
submenuPosition={currentSubmenuAlignment()}
hasSubmenu={submenu}
caretState={caretState}
{...(disabled && { "aria-disabled": true })}
{...(isHref && { as: ("a" as unknown) as undefined, download, href })}
{...(submenu && itemSubmenuProps)}
>
{submenu && checkRef(ref) && isLeftAligned ? (
<SubMenuItemIcon type="chevron_left" />
{submenu && checkRef(ref) && currentSubmenuAlignment() === "left" ? (
<SubMenuItemIcon
data-element="action-popover-menu-item-chevron"
type="chevron_left_thick"
/>
) : null}
{horizontalAlignment === "left" ? renderMenuItemIcon() : null}
{children}
{horizontalAlignment === "right" ? renderMenuItemIcon() : null}
{submenu && checkRef(ref) && !isLeftAligned ? (
<SubMenuItemIcon type="chevron_right" />
<StyledMenuItemOuterContainer>
{horizontalAlignment === "left" ? renderMenuItemIcon() : null}
<StyledMenuItemInnerText
data-element="action-popover-menu-item-inner-text"
horizontalAlignment={horizontalAlignment}
submenuPosition={currentSubmenuAlignment()}
isASubmenu={isASubmenu}
variant={retrieveVariantType()}
>
{children}
</StyledMenuItemInnerText>
{horizontalAlignment === "right" ? renderMenuItemIcon() : null}
</StyledMenuItemOuterContainer>
{submenu && checkRef(ref) && currentSubmenuAlignment() === "right" ? (
<SubMenuItemIcon
data-element="action-popover-menu-item-chevron"
type="chevron_right_thick"
/>
) : null}
</StyledMenuItem>
{React.isValidElement(submenu)
Expand All @@ -325,6 +437,8 @@ export const ActionPopoverItem = ({
setOpen,
setFocusIndex,
focusIndex,
isASubmenu: true,
horizontalAlignment,
}
)
: null}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useMemo, useContext } from "react";
import React, { useCallback, useMemo, useContext, useState } from "react";
import invariant from "invariant";

import { Menu } from "../action-popover.style";
Expand All @@ -24,18 +24,22 @@ export interface ActionPopoverMenuBaseProps {
parentID?: string;
/** Horizontal alignment of menu items content */
horizontalAlignment?: "left" | "right";
/** Sets submenu position */
submenuPosition?: "left" | "right";
/** Set whether the menu should open above or below the button */
placement?: "bottom" | "top";
/** @ignore @private */
role?: string;
/** @ignore @private */
isASubmenu?: boolean;
/** @ignore @private */
"data-element"?: string;
/** @ignore @private */
style?: {
left: number;
left: string | number;
top?: string;
bottom?: string;
right: "auto";
right: string | number;
};
}

Expand All @@ -58,6 +62,8 @@ const ActionPopoverMenu = React.forwardRef<
setFocusIndex,
placement = "bottom",
horizontalAlignment,
submenuPosition,
isASubmenu,
...rest
}: ActionPopoverMenuBaseProps,
ref
Expand Down Expand Up @@ -162,6 +168,9 @@ const ActionPopoverMenu = React.forwardRef<
[focusButton, setOpen, focusIndex, items, setFocusIndex]
);

const [caretState, setCaretState] = useState(undefined);
const [iconState, setIconState] = useState(undefined);

const clonedChildren = useMemo(() => {
let index = 0;
return React.Children.map(children, (child) => {
Expand All @@ -171,12 +180,30 @@ const ActionPopoverMenu = React.forwardRef<
focusItem: isOpen && focusIndex === index - 1,
placement: child.props.submenu ? placement : undefined,
horizontalAlignment,
submenuPosition,
caretState,
setCaretState,
iconState,
setIconState,
isASubmenu,
});
}

return child;
});
}, [children, focusIndex, isOpen, placement, horizontalAlignment]);
}, [
children,
focusIndex,
isOpen,
placement,
horizontalAlignment,
submenuPosition,
caretState,
setCaretState,
iconState,
setIconState,
isASubmenu,
]);

return (
<Menu
Expand Down
Loading

0 comments on commit e9acae8

Please sign in to comment.