Skip to content

Commit

Permalink
Merge pull request #5996 from Sage/FE-5722
Browse files Browse the repository at this point in the history
feat(action-popover): align component with design system
  • Loading branch information
tomdavies73 authored Aug 2, 2023
2 parents 15a0520 + dbb1645 commit b03c99d
Show file tree
Hide file tree
Showing 12 changed files with 1,814 additions and 297 deletions.
398 changes: 388 additions & 10 deletions cypress/components/action-popover/action-popover.cy.tsx

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"]';
3 changes: 3 additions & 0 deletions src/components/action-popover/action-popover-context.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import React from "react";

export type Alignment = "left" | "right";

type ActionPopoverContextType = {
setOpenPopover: (isOpen: boolean) => void;
focusButton: () => void;
submenuPosition: Alignment;
isOpenPopover: boolean;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ import {
MenuItemIcon,
SubMenuItemIcon,
StyledMenuItem,
StyledMenuItemInnerText,
StyledMenuItemOuterContainer,
StyledMenuItemWrapper,
} from "../action-popover.style";
import Events from "../../../__internal__/utils/helpers/events";
import createGuid from "../../../__internal__/utils/helpers/guid";
import ActionPopoverContext from "../action-popover-context";
import ActionPopoverContext, { Alignment } from "../action-popover-context";
import useLocale from "../../../hooks/__internal__/useLocale";

import { IconType } from "../../icon";
Expand Down Expand Up @@ -47,55 +49,55 @@ export interface ActionPopoverItemProps {
/** @ignore @private */
focusItem?: boolean;
/** @ignore @private */
horizontalAlignment?: "left" | "right";
horizontalAlignment?: Alignment;
/** @ignore @private */
childHasSubmenu?: boolean;
/** @ignore @private */
childHasIcon?: boolean;
/** @ignore @private */
currentSubmenuPosition?: Alignment;
/** @ignore @private */
setChildHasSubmenu?: (value: boolean) => void;
/** @ignore @private */
setChildHasIcon?: (value: boolean) => void;
/** @ignore @private */
setCurrentSubmenuPosition?: (value: Alignment) => 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>) {
return Boolean(ref && ref.current);
}

function leftAlignSubmenu(
function calculateSubmenuPosition(
ref: React.RefObject<HTMLElement>,
submenuRef: React.RefObject<HTMLElement>
submenuRef: React.RefObject<HTMLElement>,
submenuPosition: Alignment,
currentSubmenuPosition?: Alignment
) {
/* istanbul ignore if */
if (!ref.current || !submenuRef.current) return true;

const { left } = ref.current.getBoundingClientRect();
if (!ref.current || !submenuRef.current)
return currentSubmenuPosition || submenuPosition;

const { left, right } = ref.current.getBoundingClientRect();
const { offsetWidth } = submenuRef.current;
const windowWidth = document.body.clientWidth;

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",
};
if (submenuPosition === "left") {
return left >= offsetWidth ? "left" : "right";
}
return windowWidth >= right + offsetWidth ? "right" : "left";
}

export const ActionPopoverItem = ({
Expand All @@ -109,6 +111,13 @@ export const ActionPopoverItem = ({
download,
href,
horizontalAlignment,
childHasSubmenu,
childHasIcon,
currentSubmenuPosition,
setChildHasSubmenu,
setChildHasIcon,
setCurrentSubmenuPosition,
isASubmenu = false,
...rest
}: ActionPopoverItemProps) => {
const l = useLocale();
Expand All @@ -124,15 +133,19 @@ export const ActionPopoverItem = ({
"ActionPopoverItem only accepts submenu of type `ActionPopoverMenu`"
);

const { setOpenPopover, isOpenPopover, focusButton } = context;
const {
setOpenPopover,
isOpenPopover,
focusButton,
submenuPosition,
} = context;
const isHref = !!href;
const [containerPosition, setContainerPosition] = useState<
ContainerPosition | undefined
>(undefined);
const [guid] = useState(createGuid());
const [isOpen, setOpen] = useState(false);
const [focusIndex, setFocusIndex] = useState<number>(0);
const [isLeftAligned, setIsLeftAligned] = useState(true);

const submenuRef = useRef<HTMLDivElement>(null);
const ref = useRef<HTMLButtonElement>(null);
Expand All @@ -145,21 +158,65 @@ export const ActionPopoverItem = ({
}
}, [isOpenPopover]);

const alignSubmenu = useCallback(() => {
if (checkRef(ref) && checkRef(submenuRef) && submenu) {
const align = leftAlignSubmenu(ref, submenuRef);
setIsLeftAligned(align);
setContainerPosition(getContainerPosition(ref, submenuRef, placement));
useEffect(() => {
if (icon) {
setChildHasIcon?.(true);
}
}, [submenu, placement]);
if (submenu) {
setChildHasSubmenu?.(true);
}
}, [icon, setChildHasSubmenu, setChildHasIcon, submenu]);

const alignSubmenu = useCallback(() => {
const checkCalculatedSubmenuPosition = calculateSubmenuPosition(
ref,
submenuRef,
submenuPosition,
currentSubmenuPosition
);

setCurrentSubmenuPosition?.(checkCalculatedSubmenuPosition);

return checkRef(ref) && checkRef(submenuRef) && submenu;
}, [
submenu,
setCurrentSubmenuPosition,
submenuPosition,
currentSubmenuPosition,
]);

useEffect(() => {
alignSubmenu();
const getContainerPosition = () => {
/* istanbul ignore if */
if (!ref.current || !submenuRef.current) return undefined;

const { offsetWidth: submenuWidth } = submenuRef.current;

const leftAlignedSubmenu = currentSubmenuPosition === "left";
const leftValue = leftAlignedSubmenu ? -submenuWidth : "auto";
const rightValue = leftAlignedSubmenu ? "auto" : -submenuWidth;
const yPositionName = placement === "top" ? "bottom" : "top";

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

useEffect(() => {
if (submenu) {
alignSubmenu();
}
}, [alignSubmenu, submenu]);

useEffect(() => {
if (focusItem) {
ref.current?.focus();
}
}, [alignSubmenu, focusItem]);
}, [focusItem]);

useEffect(() => {
return function cleanup() {
Expand Down Expand Up @@ -205,7 +262,7 @@ export const ActionPopoverItem = ({
e.stopPropagation();
} else if (!disabled) {
if (submenu) {
if (isLeftAligned) {
if (currentSubmenuPosition === "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 +299,7 @@ export const ActionPopoverItem = ({
e.stopPropagation();
}
},
[disabled, download, isHref, isLeftAligned, onClick, submenu]
[disabled, download, isHref, onClick, submenu, currentSubmenuPosition]
);

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

const renderMenuItemIcon = () => {
return icon && <MenuItemIcon as={undefined} type={icon} />;
return (
icon && (
<MenuItemIcon
type={icon}
data-element="action-popover-menu-item-icon"
horizontalAlignment={horizontalAlignment}
submenuPosition={currentSubmenuPosition}
childHasIcon={childHasIcon}
childHasSubmenu={childHasSubmenu}
hasIcon={!!icon}
hasSubmenu={!!submenu}
isASubmenu={isASubmenu}
/>
)
);
};

return (
Expand All @@ -298,18 +369,40 @@ export const ActionPopoverItem = ({
tabIndex={0}
isDisabled={disabled}
horizontalAlignment={horizontalAlignment}
submenuPosition={currentSubmenuPosition}
hasSubmenu={!!submenu}
childHasSubmenu={childHasSubmenu}
{...(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) && currentSubmenuPosition === "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={currentSubmenuPosition}
isASubmenu={isASubmenu}
childHasSubmenu={childHasSubmenu}
childHasIcon={childHasIcon}
hasIcon={!!icon}
hasSubmenu={!!submenu}
>
{children}
</StyledMenuItemInnerText>
{horizontalAlignment === "right" ? renderMenuItemIcon() : null}
</StyledMenuItemOuterContainer>
{submenu && checkRef(ref) && currentSubmenuPosition === "right" ? (
<SubMenuItemIcon
data-element="action-popover-menu-item-chevron"
type="chevron_right_thick"
/>
) : null}
</StyledMenuItem>
{React.isValidElement(submenu)
Expand All @@ -325,6 +418,8 @@ export const ActionPopoverItem = ({
setOpen,
setFocusIndex,
focusIndex,
isASubmenu: true,
horizontalAlignment,
}
)
: null}
Expand Down
Loading

0 comments on commit b03c99d

Please sign in to comment.