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 22, 2023
1 parent 40ac949 commit 780a390
Show file tree
Hide file tree
Showing 11 changed files with 2,324 additions and 137 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,31 @@ 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 */
leftAlignmentState?: boolean;
/** @ignore @private */
setCaretState?: (value: boolean) => void;
/** @ignore @private */
setIconState?: (value: boolean) => void;
/** @ignore @private */
setLeftAlignmentState?: (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 +94,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 +105,14 @@ export const ActionPopoverItem = ({
download,
href,
horizontalAlignment,
submenuPosition,
caretState,
iconState,
leftAlignmentState,
setCaretState,
setIconState,
setLeftAlignmentState,
isASubmenu = false,
...rest
}: ActionPopoverItemProps) => {
const l = useLocale();
Expand Down Expand Up @@ -139,19 +143,106 @@ export const ActionPopoverItem = ({
const mouseEnterTimer = useRef<NodeJS.Timeout | null>(null);
const mouseLeaveTimer = useRef<NodeJS.Timeout | null>(null);

const currentSubmenuAlignment = () => {
if (submenuPosition === "left") {
if (leftAlignmentState) {
return "right";
}
return "left";
}
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]);

useEffect(() => {
if (!isLeftAligned) {
setLeftAlignmentState?.(true);
}

if (isLeftAligned) {
setLeftAlignmentState?.(false);
}
}, [setLeftAlignmentState, isLeftAligned]);

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, placement, currentSubmenuAlignment()]);

useEffect(() => {
alignSubmenu();
Expand Down Expand Up @@ -205,7 +296,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 +333,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 +382,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 +409,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 +455,8 @@ export const ActionPopoverItem = ({
setOpen,
setFocusIndex,
focusIndex,
isASubmenu: true,
horizontalAlignment,
}
)
: null}
Expand Down
Loading

0 comments on commit 780a390

Please sign in to comment.