diff --git a/cypress/components/action-popover/action-popover.cy.js b/cypress/components/action-popover/action-popover.cy.js index 198177daef..422ffb128a 100644 --- a/cypress/components/action-popover/action-popover.cy.js +++ b/cypress/components/action-popover/action-popover.cy.js @@ -1,4 +1,3 @@ -/* eslint-disable react/prop-types */ import React from "react"; import path from "path"; @@ -11,6 +10,9 @@ import { actionPopoverButton, actionPopover, actionPopoverSubmenu, + actionPopoverMenuItemIcon, + actionPopoverMenuItemInnerText, + actionPopoverMenuItemChevron, actionPopoverSubmenuByIndex, actionPopoverInnerItem, actionPopoverWrapper, @@ -27,6 +29,7 @@ import { ActionPopoverCustom, ActionPopoverWithProps, ActionPopoverMenuWithProps, + ActionPopoverWithDesignVariants, ActionPopoverProps, } from "../../../src/components/action-popover/action-popover-test.stories"; import { @@ -35,11 +38,11 @@ import { ActionPopoverComponentDisabledItems, ActionPopoverComponentMenuRightAligned, ActionPopoverComponentContentAlignedRight, + ActionPopoverComponentSubmenuPositionedRight, ActionPopoverComponentNoIcons, ActionPopoverComponentCustomMenuButton, ActionPopoverComponentSubmenu, ActionPopoverComponentDisabledSubmenu, - ActionPopoverComponentSubmenuAlignedRight, ActionPopoverComponentMenuOpeningAbove, ActionPopoverComponentKeyboardNavigation, ActionPopoverComponentKeyboardNaviationLeftAlignedSubmenu, @@ -444,6 +447,15 @@ context("Test for ActionPopover component", () => { .should("be.visible"); }); + it("should render ActionPopover with icon's within a submenu", () => { + CypressMountWithProviders( + + ); + + actionPopoverButton().eq(0).click(); + actionPopoverMenuItemIcon().eq(0).should("exist"); + }); + it.each([ ["left", "start"], ["right", "end"], @@ -461,6 +473,24 @@ context("Test for ActionPopover component", () => { } ); + it.each([ + ["left", "chevron_left_thick"], + ["right", "chevron_right_thick"], + ])( + "should render ActionPopover with submenuPosition prop set to %s", + (position, chevronType) => { + CypressMountWithProviders( + + ); + + actionPopoverButton().eq(0).click(); + actionPopoverMenuItemChevron().should("have.attr", "type", chevronType); + } + ); + it("should render ActionPopoverMenu with menuID", () => { CypressMountWithProviders( @@ -574,6 +604,605 @@ context("Test for ActionPopover component", () => { }); }); + describe("padding checks on 'StyledMenuItemInnerText'", () => { + it.each([ + ["left", "left"], + ["left", "right"], + ["right", "left"], + ["right", "right"], + ])( + "when horizontalAlignment is %s, submenuPosition is %s and iconState, caretState, icon and submenu are all false is left and right padding is: --spacing100", + (alignment, position) => { + CypressMountWithProviders( + + ); + + actionPopoverButton() + .eq(0) + .click() + .then(() => { + actionPopoverMenuItemInnerText() + .should("have.css", "padding-left", "8px") + .getDesignTokensByCssProperty("padding-left") + .then(($el) => { + expect($el[0]).to.equal("--spacing100"); + }); + + actionPopoverMenuItemInnerText() + .should("have.css", "padding-right", "8px") + .getDesignTokensByCssProperty("padding-right") + .then(($el) => { + expect($el[0]).to.equal("--spacing100"); + }); + }); + } + ); + + it.each([ + ["left", "left", "--spacing400", "32px"], + ["left", "right", "--spacing100", "8px"], + ["right", "left", "--spacing100", "8px"], + ["right", "right", "--spacing100", "8px"], + ])( + "when horizontalAlignment is %s, submenuPosition is %s and iconState is false, caretState is true, icon is false and submenu is false padding-left is: %s", + (alignment, position, spacingLeft, paddingLeft) => { + CypressMountWithProviders( + + ); + + actionPopoverButton() + .eq(0) + .click() + .then(() => { + actionPopoverMenuItemInnerText() + .eq(0) + .should("have.css", "padding-left", paddingLeft) + .getDesignTokensByCssProperty("padding-left") + .then(($el) => { + expect($el[0]).to.equal(spacingLeft); + }); + }); + } + ); + + it.each([ + ["left", "left", "--spacing100", "8px"], + ["left", "right", "--spacing100", "8px"], + ["right", "left", "--spacing100", "8px"], + ["right", "right", "--spacing400", "32px"], + ])( + "when horizontalAlignment is %s, submenuPosition is %s and iconState is false, caretState is true, icon is false and submenu is false padding-right is %s", + (alignment, position, spacingRight, paddingRight) => { + CypressMountWithProviders( + + ); + + actionPopoverButton() + .eq(0) + .click() + .then(() => { + actionPopoverMenuItemInnerText() + .eq(0) + .should("have.css", "padding-right", paddingRight) + .getDesignTokensByCssProperty("padding-right") + .then(($el) => { + expect($el[0]).to.equal(spacingRight); + }); + }); + } + ); + + it.each([ + ["left", "left", "--spacing100", "8px"], + ["left", "right", "--spacing100", "8px"], + ["right", "left", "--spacing100", "8px"], + ["right", "right", "--spacing100", "8px"], + ])( + "when horizontalAlignment is %s, submenuPosition is %s and iconState is false, caretState is true, icon is false and submenu is true padding-left is: %s and padding-right is --spacing100", + (alignment, position, spacingLeft, paddingLeft) => { + CypressMountWithProviders( + + ); + + actionPopoverButton() + .eq(0) + .click() + .then(() => { + actionPopoverMenuItemInnerText() + .eq(0) + .should("have.css", "padding-left", paddingLeft) + .getDesignTokensByCssProperty("padding-left") + .then(($el) => { + expect($el[0]).to.equal(spacingLeft); + }); + + actionPopoverMenuItemInnerText() + .eq(0) + .should("have.css", "padding-right", "8px") + .getDesignTokensByCssProperty("padding-right") + .then(($el) => { + expect($el[0]).to.equal("--spacing100"); + }); + }); + } + ); + + it.each([ + ["left", "left"], + ["left", "right"], + ["right", "left"], + ["right", "right"], + ])( + "when horizontalAlignment is %s, submenuPosition is %s and iconState is true, caretState is false, icon is false and submenu is false left and right padding is: --spacing100", + (alignment, position) => { + CypressMountWithProviders( + + ); + + actionPopoverButton() + .eq(0) + .click() + .then(() => { + actionPopoverMenuItemInnerText() + .eq(0) + .should("have.css", "padding-left", "8px") + .getDesignTokensByCssProperty("padding-left") + .then(($el) => { + expect($el[0]).to.equal("--spacing100"); + }); + + actionPopoverMenuItemInnerText() + .eq(0) + .should("have.css", "padding-right", "8px") + .getDesignTokensByCssProperty("padding-right") + .then(($el) => { + expect($el[0]).to.equal("--spacing100"); + }); + }); + } + ); + + it.each([ + ["left", "left", "--spacing000", "0px"], + ["left", "right", "--spacing100", "8px"], + ["right", "left", "--spacing100", "8px"], + ["right", "right", "--spacing100", "8px"], + ])( + "when horizontalAlignment is %s, submenuPosition is %s and iconState is true, caretState is false, icon is true and submenu is false padding-left is: %s and padding-right is: --spacing100", + (alignment, position, spacingLeft, paddingLeft) => { + CypressMountWithProviders( + + ); + + actionPopoverButton() + .eq(0) + .click() + .then(() => { + actionPopoverMenuItemInnerText() + .eq(0) + .should("have.css", "padding-left", paddingLeft) + .getDesignTokensByCssProperty("padding-left") + .then(($el) => { + expect($el[0]).to.equal(spacingLeft); + }); + + actionPopoverMenuItemInnerText() + .eq(0) + .should("have.css", "padding-right", "8px") + .getDesignTokensByCssProperty("padding-right") + .then(($el) => { + expect($el[0]).to.equal("--spacing100"); + }); + }); + } + ); + + it.each([ + ["left", "left", "--spacing000", "0px"], + ["left", "right", "--spacing100", "8px"], + ["right", "left", "--spacing100", "8px"], + ["right", "right", "--spacing100", "8px"], + ])( + "when horizontalAlignment is %s, submenuPosition is %s and iconState, caretState, icon and submenu are all true, padding-left is: %s and padding-right is --spacing100", + (alignment, position, spacingLeft, paddingLeft) => { + CypressMountWithProviders( + + ); + + actionPopoverButton() + .eq(0) + .click() + .then(() => { + actionPopoverMenuItemInnerText() + .eq(0) + .should("have.css", "padding-left", paddingLeft) + .getDesignTokensByCssProperty("padding-left") + .then(($el) => { + expect($el[0]).to.equal(spacingLeft); + }); + + actionPopoverMenuItemInnerText() + .eq(0) + .should("have.css", "padding-right", "8px") + .getDesignTokensByCssProperty("padding-right") + .then(($el) => { + expect($el[0]).to.equal("--spacing100"); + }); + }); + } + ); + + it.each([ + ["left", "left", "--spacing000", "0px"], + ["left", "right", "--spacing100", "8px"], + ["right", "left", "--spacing100", "8px"], + ["right", "right", "--spacing100", "8px"], + ])( + "when horizontalAlignment is %s, submenuPosition is %s and iconState is true, caretState is true, icon is true and submenu is false padding-left is: %s and padding-right is: --spacing100", + (alignment, position, spacingLeft, paddingLeft) => { + CypressMountWithProviders( + + ); + + actionPopoverButton() + .eq(0) + .click() + .then(() => { + actionPopoverMenuItemInnerText() + .eq(0) + .should("have.css", "padding-left", paddingLeft) + .getDesignTokensByCssProperty("padding-left") + .then(($el) => { + expect($el[0]).to.equal(spacingLeft); + }); + + actionPopoverMenuItemInnerText() + .eq(0) + .should("have.css", "padding-right", "8px") + .getDesignTokensByCssProperty("padding-right") + .then(($el) => { + expect($el[0]).to.equal("--spacing100"); + }); + }); + } + ); + + it.each([ + ["left", "left", "--spacing500", "40px"], + ["left", "right", "--spacing100", "8px"], + ["right", "left", "--spacing100", "8px"], + ["right", "right", "--spacing100", "8px"], + ])( + "when horizontalAlignment is %s, submenuPosition is %s and iconState is true, caretState is true, icon is false and submenu is true padding-left is: %s", + (alignment, position, spacingLeft, paddingLeft) => { + CypressMountWithProviders( + + ); + + actionPopoverButton() + .eq(0) + .click() + .then(() => { + actionPopoverMenuItemInnerText() + .eq(0) + .should("have.css", "padding-left", paddingLeft) + .getDesignTokensByCssProperty("padding-left") + .then(($el) => { + expect($el[0]).to.equal(spacingLeft); + }); + }); + } + ); + + it.each([ + ["left", "left", "--spacing100", "8px"], + ["left", "right", "--spacing100", "8px"], + ["right", "left", "--spacing100", "8px"], + ["right", "right", "--spacing600", "48px"], + ])( + "when horizontalAlignment is %s, submenuPosition is %s and iconState is true, caretState is true, icon is false and submenu is true padding-right is: %s", + (alignment, position, spacingRight, paddingRight) => { + CypressMountWithProviders( + + ); + + actionPopoverButton() + .eq(0) + .click() + .then(() => { + actionPopoverMenuItemInnerText() + .eq(0) + .should("have.css", "padding-right", paddingRight) + .getDesignTokensByCssProperty("padding-right") + .then(($el) => { + expect($el[0]).to.equal(spacingRight); + }); + }); + } + ); + + it.each([ + ["left", "left", "--spacing800", "64px"], + ["left", "right", "--spacing100", "8px"], + ["right", "left", "--spacing100", "8px"], + ["right", "right", "--spacing100", "8px"], + ])( + "when horizontalAlignment is %s, submenuPosition is %s and iconState is true, caretState is true, icon is false and submenu is false padding-left is: %s", + (alignment, position, spacingLeft, paddingLeft) => { + CypressMountWithProviders( + + ); + + actionPopoverButton() + .eq(0) + .click() + .then(() => { + actionPopoverMenuItemInnerText() + .eq(0) + .should("have.css", "padding-left", paddingLeft) + .getDesignTokensByCssProperty("padding-left") + .then(($el) => { + expect($el[0]).to.equal(spacingLeft); + }); + }); + } + ); + + it.each([ + ["left", "left", "--spacing100", "8px"], + ["left", "right", "--spacing100", "8px"], + ["right", "left", "--spacing100", "8px"], + ["right", "right", "--spacing900", "72px"], + ])( + "when horizontalAlignment is %s, submenuPosition is %s and iconState is true, caretState is true, icon is false and submenu is false padding-right is: %s", + (alignment, position, spacingRight, paddingRight) => { + CypressMountWithProviders( + + ); + + actionPopoverButton() + .eq(0) + .click() + .then(() => { + actionPopoverMenuItemInnerText() + .eq(0) + .should("have.css", "padding-right", paddingRight) + .getDesignTokensByCssProperty("padding-right") + .then(($el) => { + expect($el[0]).to.equal(spacingRight); + }); + }); + } + ); + + it.each([ + ["left", "left", 1], + ["left", "right", 2], + ["right", "left", 3], + ["right", "right", 4], + ])( + "when horizontalAlignment is %s, submenuPosition is %s and child is a submenu, left and right padding is: --spacing000", + (alignment, position, index) => { + CypressMountWithProviders( + + ); + + actionPopoverButton() + .eq(0) + .click() + .then(() => { + actionPopoverMenuItemInnerText() + .eq(index) + .should("have.css", "padding-left", "0px") + .getDesignTokensByCssProperty("padding-left") + .then(($el) => { + expect($el[0]).to.equal("--spacing000"); + }); + + actionPopoverMenuItemInnerText() + .eq(index) + .should("have.css", "padding-right", "0px") + .getDesignTokensByCssProperty("padding-right") + .then(($el) => { + expect($el[0]).to.equal("--spacing000"); + }); + }); + } + ); + }); + + describe("justify-content checks on 'StyledMenuItem'", () => { + it.each([ + ["left", "flex-start"], + ["right", "flex-end"], + ])( + "when horizontalAlignment is %s, content should be justified %s", + (alignment, itemAlignment) => { + CypressMountWithProviders( + + ); + + actionPopoverButton().eq(0).click(); + getDataElementByValue("menu-item1").should( + "have.css", + "justify-content", + itemAlignment + ); + } + ); + + it.each([ + ["left", "right", "space-between"], + ["right", "left", "flex-end"], + ])( + "when horizontalAlignment is %s, and submenuPosition is %s without submenu, content should be justified %s", + (alignment, position, itemAlignment) => { + CypressMountWithProviders( + + ); + + actionPopoverButton().eq(0).click(); + getDataElementByValue("menu-item1").should( + "have.css", + "justify-content", + itemAlignment + ); + } + ); + + it.each([ + ["left", "right", "space-between"], + ["right", "left", "space-between"], + ])( + "when horizontalAlignment is %s, and submenuPosition is %s with submenu, content should be justified %s", + (alignment, position, itemAlignment) => { + CypressMountWithProviders( + + ); + + actionPopoverButton().eq(0).click(); + getDataElementByValue("menu-item1").should( + "have.css", + "justify-content", + itemAlignment + ); + } + ); + }); + + describe("padding checks on 'MenuItemIcon'", () => { + it.each([ + [ + "left", + "left", + "--spacing100 --spacing100 --spacing100 --spacing400", + "8px 8px 8px 32px", + ], + [ + "left", + "right", + "--spacing100 --spacing100 --spacing100 --spacing100", + "8px", + ], + [ + "right", + "left", + "--spacing100 --spacing100 --spacing100 --spacing100", + "8px", + ], + [ + "right", + "right", + "--spacing100 --spacing400 --spacing100 --spacing100", + "8px 32px 8px 8px", + ], + ])( + "when iconState, caretState and icon are all is true and submenu is false, submenuPosition is %s and horizontalAlignment is %s, padding is %s", + (position, alignment, spacing, padding) => { + CypressMountWithProviders( + + ); + + actionPopoverButton() + .eq(0) + .click() + .then(() => { + actionPopoverMenuItemIcon() + .eq(0) + .should("have.css", "padding", padding) + .getDesignTokensByCssProperty("padding") + .then(($el) => { + expect($el.join(" ")).to.deep.equal(spacing); + }); + }); + } + ); + + it("when iconState, caretState and icon are all true and submenu is false, padding is: --spacing100", () => { + CypressMountWithProviders( + + ); + + actionPopoverButton() + .eq(0) + .click() + .then(() => { + actionPopoverMenuItemIcon() + .eq(0) + .should("have.css", "padding", "8px") + .getDesignTokensByCssProperty("padding") + .then(($el) => { + expect($el.join(" ")).to.deep.equal( + "--spacing100 --spacing100 --spacing100 --spacing100" + ); + }); + }); + }); + }); + describe("Accessibility tests for ActionPopover", () => { it("should pass accessibility tests for ActionPopover with custom button", () => { CypressMountWithProviders( @@ -629,6 +1258,15 @@ context("Test for ActionPopover component", () => { cy.checkAccessibility(); }); + it("should pass accessibility tests for ActionPopover with a right submenu position", () => { + CypressMountWithProviders( + + ); + + actionPopoverButton().eq(0).click(); + cy.checkAccessibility(); + }); + it("should pass accessibility tests for ActionPopover with no icons", () => { CypressMountWithProviders(); @@ -658,14 +1296,6 @@ context("Test for ActionPopover component", () => { cy.checkAccessibility(); }); - it("should pass accessibility tests for ActionPopover with submenu aligned right", () => { - CypressMountWithProviders(); - - actionPopoverButton().eq(0).click(); - actionPopoverInnerItem(0).click(); - cy.checkAccessibility(); - }); - it("should pass accessibility tests for ActionPopover with menu opening above", () => { CypressMountWithProviders(); diff --git a/cypress/locators/action-popover/index.js b/cypress/locators/action-popover/index.js index 05e3400b0c..a9dc1ffbc6 100644 --- a/cypress/locators/action-popover/index.js +++ b/cypress/locators/action-popover/index.js @@ -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"; @@ -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); diff --git a/cypress/locators/action-popover/locators.js b/cypress/locators/action-popover/locators.js index ffbdfed094..6cd044eb2f 100644 --- a/cypress/locators/action-popover/locators.js +++ b/cypress/locators/action-popover/locators.js @@ -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"]'; diff --git a/src/components/action-popover/action-popover-item/action-popover-item.component.tsx b/src/components/action-popover/action-popover-item/action-popover-item.component.tsx index 4b35bb9a14..e7a60238c0 100644 --- a/src/components/action-popover/action-popover-item/action-popover-item.component.tsx +++ b/src/components/action-popover/action-popover-item/action-popover-item.component.tsx @@ -11,6 +11,8 @@ import { MenuItemIcon, SubMenuItemIcon, StyledMenuItem, + StyledMenuItemInnerText, + StyledMenuItemOuterContainer, StyledMenuItemWrapper, } from "../action-popover.style"; import Events from "../../../__internal__/utils/helpers/events"; @@ -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) { @@ -76,28 +90,6 @@ function leftAlignSubmenu( return left >= offsetWidth; } -function getContainerPosition( - itemRef: React.RefObject, - submenuRef: React.RefObject, - 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, @@ -109,6 +101,12 @@ export const ActionPopoverItem = ({ download, href, horizontalAlignment, + submenuPosition, + caretState, + iconState, + setCaretState, + setIconState, + isASubmenu = false, ...rest }: ActionPopoverItemProps) => { const l = useLocale(); @@ -139,19 +137,94 @@ export const ActionPopoverItem = ({ const mouseEnterTimer = useRef(null); const mouseLeaveTimer = useRef(null); + const currentSubmenuAlignment = () => { + if (submenuPosition === "left") { + if (isLeftAligned) { + return "left"; + } + return "right"; + } + return "right"; + }; + + function getContainerPosition( + itemRef: React.RefObject, + containerSubmenuRef: React.RefObject, + 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(); @@ -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); @@ -242,7 +315,15 @@ export const ActionPopoverItem = ({ e.stopPropagation(); } }, - [disabled, download, isHref, isLeftAligned, onClick, submenu] + [ + disabled, + download, + isHref, + isLeftAligned, + onClick, + submenu, + currentSubmenuAlignment, + ] ); const itemSubmenuProps = { @@ -283,7 +364,19 @@ export const ActionPopoverItem = ({ }; const renderMenuItemIcon = () => { - return icon && ; + return ( + icon && ( + + ) + ); }; return ( @@ -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 ? ( - + {submenu && checkRef(ref) && currentSubmenuAlignment() === "left" ? ( + ) : null} - {horizontalAlignment === "left" ? renderMenuItemIcon() : null} - {children} - {horizontalAlignment === "right" ? renderMenuItemIcon() : null} - {submenu && checkRef(ref) && !isLeftAligned ? ( - + + {horizontalAlignment === "left" ? renderMenuItemIcon() : null} + + {children} + + {horizontalAlignment === "right" ? renderMenuItemIcon() : null} + + {submenu && checkRef(ref) && currentSubmenuAlignment() === "right" ? ( + ) : null} {React.isValidElement(submenu) @@ -325,6 +437,8 @@ export const ActionPopoverItem = ({ setOpen, setFocusIndex, focusIndex, + isASubmenu: true, + horizontalAlignment, } ) : null} diff --git a/src/components/action-popover/action-popover-menu/action-popover-menu.component.tsx b/src/components/action-popover/action-popover-menu/action-popover-menu.component.tsx index 1ab095c8ab..39cc2acdea 100644 --- a/src/components/action-popover/action-popover-menu/action-popover-menu.component.tsx +++ b/src/components/action-popover/action-popover-menu/action-popover-menu.component.tsx @@ -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"; @@ -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; }; } @@ -58,6 +62,8 @@ const ActionPopoverMenu = React.forwardRef< setFocusIndex, placement = "bottom", horizontalAlignment, + submenuPosition, + isASubmenu, ...rest }: ActionPopoverMenuBaseProps, ref @@ -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) => { @@ -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 ( { Doe - {}}> + {}} + > Email Invoice - {}} icon="delete"> + {}} + icon="delete" + > Delete @@ -285,6 +295,435 @@ export const ActionPopoverWithProps = ({ ...props }) => { ); }; +export const ActionPopoverWithDesignVariants = ({ ...props }) => { + const { variant } = props; + + const submenu = ( + + {}}> + Sub Menu 1 + + {}}> + Sub Menu 2 + + {}}> + {" "} + Sub Menu 3 + + {}}> + {" "} + Sub Menu 4 + + + ); + + let defaultProps; + + if (variant === 1) { + defaultProps = { + children: [ + {}}> + Business + , + {}}> + Email Invoice + , + {}}> + Print Invoice + , + {}}> + Download PDF + , + {}}> + Download CSV + , + , + {}}> + Delete + , + {}}> + Return Home + , + null, + undefined, + ], + ...props, + }; + } else if (variant === 2) { + defaultProps = { + children: [ + {}}> + Business + , + {}} submenu={submenu}> + Email Invoice + , + {}}> + Print Invoice + , + {}}> + Download PDF + , + {}} submenu={submenu}> + Download CSV + , + , + {}} submenu={submenu}> + Delete + , + {}} submenu={submenu}> + Return Home + , + null, + undefined, + ], + ...props, + }; + } else if (variant === 3) { + defaultProps = { + children: [ + {}} submenu={submenu}> + Business + , + {}} submenu={submenu}> + Email Invoice + , + {}} submenu={submenu}> + Print Invoice + , + {}} submenu={submenu}> + Download PDF + , + {}} submenu={submenu}> + Download CSV + , + , + {}} submenu={submenu}> + Delete + , + {}} submenu={submenu}> + Return Home + , + null, + undefined, + ], + ...props, + }; + } else if (variant === 4) { + defaultProps = { + children: [ + {}}> + Business + , + {}}> + Email Invoice + , + {}}> + Print Invoice + , + {}}> + Download PDF + , + {}}> + Download CSV + , + , + {}}> + Delete + , + {}}> + Return Home + , + null, + undefined, + ], + ...props, + }; + } else if (variant === 5) { + defaultProps = { + children: [ + {}}> + Business + , + {}}> + Email Invoice + , + {}}> + Print Invoice + , + {}}> + Download PDF + , + {}}> + Download CSV + , + , + {}}> + Delete + , + {}}> + Return Home + , + null, + undefined, + ], + ...props, + }; + } else if (variant === 6) { + defaultProps = { + children: [ + {}} + submenu={submenu} + > + Business + , + {}} + submenu={submenu} + > + Email Invoice + , + {}} + submenu={submenu} + > + Print Invoice + , + {}} + submenu={submenu} + > + Download PDF + , + {}} + submenu={submenu} + > + Download CSV + , + , + {}} + submenu={submenu} + > + Delete + , + {}} + submenu={submenu} + > + Return Home + , + null, + undefined, + ], + ...props, + }; + } else if (variant === 7) { + defaultProps = { + children: [ + {}}> + Business + , + {}}> + Email Invoice + , + {}}> + Print Invoice + , + {}}> + Download PDF + , + {}} + submenu={submenu} + > + Download CSV + , + , + {}} + submenu={submenu} + > + Delete + , + {}} + submenu={submenu} + > + Return Home + , + null, + undefined, + ], + ...props, + }; + } else if (variant === 8) { + defaultProps = { + children: [ + {}} submenu={submenu}> + Business + , + {}} submenu={submenu}> + Email Invoice + , + {}} submenu={submenu}> + Print Invoice + , + {}} submenu={submenu}> + Download PDF + , + {}} + submenu={submenu} + > + Download CSV + , + , + {}} + submenu={submenu} + > + Delete + , + {}} + submenu={submenu} + > + Return Home + , + null, + undefined, + ], + ...props, + }; + } else if (variant === 9) { + defaultProps = { + children: [ + {}}> + Business + , + {}}> + Email Invoice + , + {}}> + Print Invoice + , + {}}> + Download PDF + , + {}} + submenu={submenu} + > + Download CSV + , + , + {}} + submenu={submenu} + > + Delete + , + {}} + submenu={submenu} + > + Return Home + , + null, + undefined, + ], + ...props, + }; + } else if (variant === 10) { + defaultProps = { + children: [ + {}} + > + Business + , + {}}> + Email Invoice + , + null, + undefined, + ], + ...props, + }; + } else { + defaultProps = { + children: [ + {}}> + Business + , + {}}> + Email Invoice + , + {}}> + Print Invoice + , + {}}> + Download PDF + , + {}}> + Download CSV + , + , + {}}> + Delete + , + {}}> + Return Home + , + null, + undefined, + ], + ...props, + }; + } + + return ; +}; + export const ActionPopoverMenuWithProps = ({ ...props }) => { return ( diff --git a/src/components/action-popover/action-popover.component.tsx b/src/components/action-popover/action-popover.component.tsx index 3d923735aa..e43cf471f9 100644 --- a/src/components/action-popover/action-popover.component.tsx +++ b/src/components/action-popover/action-popover.component.tsx @@ -39,6 +39,8 @@ export interface ActionPopoverProps extends MarginProps { children?: React.ReactNode; /** Horizontal alignment of menu items content */ horizontalAlignment?: "left" | "right"; + /** Sets submenu position */ + submenuPosition?: "left" | "right"; /** Unique ID */ id?: string; /** Callback to be called on menu open */ @@ -65,6 +67,7 @@ export const ActionPopover = ({ renderButton, placement = "bottom", horizontalAlignment = "left", + submenuPosition = "left", ...rest }: ActionPopoverProps) => { const l = useLocale(); @@ -254,6 +257,7 @@ export const ActionPopover = ({ rightAlignMenu, placement, horizontalAlignment, + submenuPosition, }; return ( diff --git a/src/components/action-popover/action-popover.spec.tsx b/src/components/action-popover/action-popover.spec.tsx index a8086a8971..db378ac98d 100644 --- a/src/components/action-popover/action-popover.spec.tsx +++ b/src/components/action-popover/action-popover.spec.tsx @@ -3,7 +3,6 @@ import ReactDOM from "react-dom"; import { act } from "react-dom/test-utils"; import { ThemeProvider } from "styled-components"; import { mount as enzymeMount, ReactWrapper } from "enzyme"; - import { simulate, assertStyleMatch, @@ -25,6 +24,7 @@ import { MenuItemIcon, SubMenuItemIcon, StyledMenuItem, + StyledMenuItemInnerText, StyledButtonIcon, StyledMenuItemWrapper, } from "./action-popover.style"; @@ -116,6 +116,7 @@ describe("ActionPopover", () => { Sub Menu 1 @@ -180,6 +181,421 @@ describe("ActionPopover", () => { ); } + function renderDesignVariants(variant: number, props = {}, renderer = mount) { + const submenu = ( + + + Sub Menu 1 + + + Sub Menu 2 + + + ); + + let defaultProps; + + if (variant === 1) { + defaultProps = { + children: [ + {}}> + Business + , + {}}> + Email Invoice + , + {}}> + Print Invoice + , + {}}> + Download PDF + , + {}}> + Download CSV + , + , + {}}> + Delete + , + {}}> + Return Home + , + null, + undefined, + ], + ...props, + }; + } else if (variant === 2) { + defaultProps = { + children: [ + {}}> + Business + , + {}} submenu={submenu}> + Email Invoice + , + {}}> + Print Invoice + , + {}}> + Download PDF + , + {}} submenu={submenu}> + Download CSV + , + , + {}} submenu={submenu}> + Delete + , + {}} submenu={submenu}> + Return Home + , + null, + undefined, + ], + ...props, + }; + } else if (variant === 3) { + defaultProps = { + children: [ + {}} submenu={submenu}> + Business + , + {}} submenu={submenu}> + Email Invoice + , + {}} submenu={submenu}> + Print Invoice + , + {}} submenu={submenu}> + Download PDF + , + {}} submenu={submenu}> + Download CSV + , + , + {}} submenu={submenu}> + Delete + , + {}} submenu={submenu}> + Return Home + , + null, + undefined, + ], + ...props, + }; + } else if (variant === 4) { + defaultProps = { + children: [ + {}}> + Business + , + {}}> + Email Invoice + , + {}}> + Print Invoice + , + {}}> + Download PDF + , + {}}> + Download CSV + , + , + {}}> + Delete + , + {}}> + Return Home + , + null, + undefined, + ], + ...props, + }; + } else if (variant === 5) { + defaultProps = { + children: [ + {}}> + Business + , + {}}> + Email Invoice + , + {}}> + Print Invoice + , + {}}> + Download PDF + , + {}}> + Download CSV + , + , + {}}> + Delete + , + {}}> + Return Home + , + null, + undefined, + ], + ...props, + }; + } else if (variant === 6) { + defaultProps = { + children: [ + {}} + submenu={submenu} + > + Business + , + {}} + submenu={submenu} + > + Email Invoice + , + {}} + submenu={submenu} + > + Print Invoice + , + {}} + submenu={submenu} + > + Download PDF + , + {}} + submenu={submenu} + > + Download CSV + , + , + {}} + submenu={submenu} + > + Delete + , + {}} + submenu={submenu} + > + Return Home + , + null, + undefined, + ], + ...props, + }; + } else if (variant === 7) { + defaultProps = { + children: [ + {}}> + Business + , + {}}> + Email Invoice + , + {}}> + Print Invoice + , + {}}> + Download PDF + , + {}} + submenu={submenu} + > + Download CSV + , + , + {}} + submenu={submenu} + > + Delete + , + {}} + submenu={submenu} + > + Return Home + , + null, + undefined, + ], + ...props, + }; + } else if (variant === 8) { + defaultProps = { + children: [ + {}} submenu={submenu}> + Business + , + {}} submenu={submenu}> + Email Invoice + , + {}} submenu={submenu}> + Print Invoice + , + {}} submenu={submenu}> + Download PDF + , + {}} + submenu={submenu} + > + Download CSV + , + , + {}} + submenu={submenu} + > + Delete + , + {}} + submenu={submenu} + > + Return Home + , + null, + undefined, + ], + ...props, + }; + } else if (variant === 9) { + defaultProps = { + children: [ + {}}> + Business + , + {}}> + Email Invoice + , + {}}> + Print Invoice + , + {}}> + Download PDF + , + {}} + submenu={submenu} + > + Download CSV + , + , + {}} + submenu={submenu} + > + Delete + , + {}} + submenu={submenu} + > + Return Home + , + null, + undefined, + ], + ...props, + }; + } else { + defaultProps = { + children: [ + {}}> + Business + , + {}}> + Email Invoice + , + {}}> + Print Invoice + , + {}}> + Download PDF + , + {}}> + Download CSV + , + , + {}}> + Delete + , + {}}> + Return Home + , + null, + undefined, + ], + ...props, + }; + } + + renderer( + + <> + + + + + + ); + } + function getElements() { return { items: wrapper.find(ActionPopoverItem), @@ -876,7 +1292,7 @@ describe("ActionPopover", () => { }, submenuIcon ); - expect(submenuIcon.props().type).toEqual("chevron_left"); + expect(submenuIcon.props().type).toEqual("chevron_left_thick"); }); it("opens the submenu on mouseenter", () => { @@ -1247,7 +1663,7 @@ describe("ActionPopover", () => { }); }); - describe("right aligned", () => { + describe("submenu aligns right when parent container is too small", () => { let getBoundingClientRectSpy: jest.SpyInstance; beforeEach(() => { // Mock the parent boundingRect @@ -1262,7 +1678,7 @@ describe("ActionPopover", () => { right: "200", top: "100", })); - renderWithSubmenu(); + renderWithSubmenu({ submenuPosition: "left" }); }); afterEach(() => { @@ -1281,7 +1697,74 @@ describe("ActionPopover", () => { }, submenuIcon ); - expect(submenuIcon.props().type).toEqual("chevron_right"); + expect(submenuIcon.props().type).toEqual("chevron_right_thick"); + }); + + it("opens the submenu and focuses the first item when right key is pressed", () => { + openMenu(); + const { items } = getElements(); + const item = items.at(1); + + act(() => { + simulate.keydown.pressArrowRight(item.find(StyledMenuItem).at(0)); + }); + jest.runAllTimers(); + + act(() => { + expect( + item + .find(ActionPopoverMenu) + .find(ActionPopoverItem) + .at(0) + .find(StyledMenuItem) + .at(0) + .getDOMNode() + ).toBeFocused(); + }); + }); + + it("closes the submenu and returns focus to parent item when left key is pressed", () => { + openMenu(); + const { items } = getElements(); + const item = items.at(1); + act(() => { + simulate.keydown.pressArrowRight(item.find(StyledMenuItem).at(0)); + }); + act(() => { + simulate.keydown.pressArrowLeft(item.find(StyledMenuItem).at(0)); + }); + act(() => { + expect( + item + .find(ActionPopoverMenu) + .find(ActionPopoverItem) + .at(0) + .find(StyledMenuItem) + .at(0) + .getDOMNode() + ).not.toBeFocused(); + expect(item.find(StyledMenuItem).at(0).getDOMNode()).toBeFocused(); + }); + }); + }); + + describe("submenu aligns right when submenuPosition is 'right'", () => { + beforeEach(() => { + renderWithSubmenu({ submenuPosition: "right" }); + }); + + it("renders an icon to indicate when a item has a submenu and is right aligned", () => { + openMenu(); + const { items } = getElements(); + const item = items.at(1); + const submenuIcon = item.find(SubMenuItemIcon); + assertStyleMatch( + { + right: "-5px", + }, + submenuIcon + ); + expect(submenuIcon.props().type).toEqual("chevron_right_thick"); }); it("opens the submenu and focuses the first item when right key is pressed", () => { @@ -1519,36 +2002,353 @@ describe("ActionPopover", () => { }); }); - describe("when the horizontalAlignment prop is set to right", () => { - beforeEach(() => { - wrapper = enzymeMount( - - - - test download - - - - ); - }); + describe("padding checks on 'StyledMenuItemInnerText'", () => { + it.each([ + ["left", "left"], + ["left", "right"], + ["right", "left"], + ["right", "right"], + ])( + "when horizontalAlignment is %s, submenuPosition is % and iconState, caretState, icon and submenu are all false, left and right padding is: var(--spacing100)", + (alignment, position) => { + renderDesignVariants(1, { + submenuPosition: position, + horizontalAlignment: alignment, + }); + openMenu(); + assertStyleMatch( + { + paddingLeft: "var(--spacing100)", + paddingRight: "var(--spacing100)", + }, + wrapper.find(StyledMenuItemInnerText).at(0) + ); + } + ); - it("then menu item content should be right aligned", () => { - openMenu(); - assertStyleMatch( - { - justifyContent: "flex-end", - }, - wrapper.find(StyledMenuItem) - ); - }); + it.each([ + ["left", "left", "var(--spacing400)", "var(--spacing100)"], + ["left", "right", "var(--spacing100)", "var(--spacing100)"], + ["right", "left", "var(--spacing100)", "var(--spacing100)"], + ["right", "right", "var(--spacing100)", "var(--spacing400)"], + ])( + "when horizontalAlignment is %s, submenuPosition is %s and iconState is false, caretState is true, icon is false and submenu is false padding-left is: %s and padding-right is: %s", + (alignment, position, paddingLeft, paddingRight) => { + renderDesignVariants(2, { + submenuPosition: position, + horizontalAlignment: alignment, + }); + openMenu(); + assertStyleMatch( + { + paddingLeft, + paddingRight, + }, + wrapper.find(StyledMenuItemInnerText).at(0) + ); + } + ); + + it.each([ + ["left", "left", "var(--spacing100)"], + ["left", "right", "var(--spacing100)"], + ["right", "left", "var(--spacing100)"], + ["right", "right", "var(--spacing100)"], + ])( + "when horizontalAlignment is %s, submenuPosition is %s and iconState is false, caretState is true, icon is false and submenu is true padding-left is: %s and padding-right is: var(--spacing100)", + (alignment, position, paddingLeft) => { + renderDesignVariants(3, { + submenuPosition: position, + horizontalAlignment: alignment, + }); + openMenu(); + assertStyleMatch( + { + paddingLeft, + paddingRight: "var(--spacing100)", + }, + wrapper.find(StyledMenuItemInnerText).at(0) + ); + } + ); + + it.each([ + ["left", "left"], + ["left", "right"], + ["right", "left"], + ["right", "right"], + ])( + "when horizontalAlignment is %s, submenuPosition is %s and iconState is true, caretState is false, icon is false and submenu is false, left and right padding is: var(--spacing100)", + (alignment, position) => { + renderDesignVariants(4, { + submenuPosition: position, + horizontalAlignment: alignment, + }); + openMenu(); + assertStyleMatch( + { + paddingLeft: "var(--spacing100)", + paddingRight: "var(--spacing100)", + }, + wrapper.find(StyledMenuItemInnerText).at(0) + ); + } + ); + + it.each([ + ["left", "left", "var(--spacing000)"], + ["left", "right", "var(--spacing100)"], + ["right", "left", "var(--spacing100)"], + ["right", "right", "var(--spacing100)"], + ])( + "when horizontalAlignment is %s, submenuPosition is %s and iconState is true, caretState is false, icon is true and submenu is false padding-left is: %s and padding-right is: var(--spacing100)", + (alignment, position, paddingLeft) => { + renderDesignVariants(5, { + submenuPosition: position, + horizontalAlignment: alignment, + }); + openMenu(); + assertStyleMatch( + { + paddingLeft, + paddingRight: "var(--spacing100)", + }, + wrapper.find(StyledMenuItemInnerText).at(0) + ); + } + ); + + it.each([ + ["left", "left", "var(--spacing000)"], + ["left", "right", "var(--spacing100)"], + ["right", "left", "var(--spacing100)"], + ["right", "right", "var(--spacing100)"], + ])( + "when horizontalAlignment is %s, submenuPosition is %s and iconState, caretState, icon and submenu are all true, padding-left is: %s and padding-right is: var(--spacing100)", + (alignment, position, paddingLeft) => { + renderDesignVariants(6, { + submenuPosition: position, + horizontalAlignment: alignment, + }); + openMenu(); + assertStyleMatch( + { + paddingLeft, + paddingRight: "var(--spacing100)", + }, + wrapper.find(StyledMenuItemInnerText).at(0) + ); + } + ); + + it.each([ + ["left", "left", "var(--spacing000)"], + ["left", "right", "var(--spacing100)"], + ["right", "left", "var(--spacing100)"], + ["right", "right", "var(--spacing100)"], + ])( + "when horizontalAlignment is %s, submenuPosition is %s and iconState is true, caretState is true, icon is true and submenu is false padding-left is: %s and padding-right is: var(--spacing100)", + (alignment, position, paddingLeft) => { + renderDesignVariants(7, { + submenuPosition: position, + horizontalAlignment: alignment, + }); + openMenu(); + assertStyleMatch( + { + paddingLeft, + paddingRight: "var(--spacing100)", + }, + wrapper.find(StyledMenuItemInnerText).at(0) + ); + } + ); + + it.each([ + ["left", "left", "var(--spacing500)", "var(--spacing100)"], + ["left", "right", "var(--spacing100)", "var(--spacing100)"], + ["right", "left", "var(--spacing100)", "var(--spacing100)"], + ["right", "right", "var(--spacing100)", "var(--spacing600)"], + ])( + "when horizontalAlignment is %s, submenuPosition is %s and iconState is true, caretState is true, icon is false and submenu is true padding-left is: %s and padding-right is: %s", + (alignment, position, paddingLeft, paddingRight) => { + renderDesignVariants(8, { + submenuPosition: position, + horizontalAlignment: alignment, + }); + openMenu(); + assertStyleMatch( + { + paddingLeft, + paddingRight, + }, + wrapper.find(StyledMenuItemInnerText).at(0) + ); + } + ); + + it.each([ + ["left", "left", "var(--spacing800)", "var(--spacing100)"], + ["left", "right", "var(--spacing100)", "var(--spacing100)"], + ["right", "left", "var(--spacing100)", "var(--spacing100)"], + ["right", "right", "var(--spacing100)", "var(--spacing900)"], + ])( + "when horizontalAlignment is %s, submenuPosition is %s and iconState is true, caretState is true, icon is false and submenu is false padding-left is: %s and padding-right is: %s", + (alignment, position, paddingLeft, paddingRight) => { + renderDesignVariants(9, { + submenuPosition: position, + horizontalAlignment: alignment, + }); + openMenu(); + assertStyleMatch( + { + paddingLeft, + paddingRight, + }, + wrapper.find(StyledMenuItemInnerText).at(0) + ); + } + ); + + it.each([ + ["left", "left"], + ["left", "right"], + ["right", "left"], + ["right", "right"], + ])( + "when horizontalAlignment is %s, submenuPosition is %s and child is a submenu, left and right padding is: var(--spacing000)", + (alignment, position) => { + renderDesignVariants(8, { + submenuPosition: position, + horizontalAlignment: alignment, + }); + openMenu(); + const { items } = getElements(); + const item = items.at(0); + const submenu = item.find(ActionPopoverMenu); + const disabledSubmenuItem = submenu + .find(ActionPopoverItem) + .at(1) + .find(StyledMenuItem) + .at(0); + + assertStyleMatch( + { + paddingLeft: "var(--spacing000)", + paddingRight: "var(--spacing000)", + }, + disabledSubmenuItem.find(StyledMenuItemInnerText) + ); + } + ); + }); + + describe("justify-content checks on 'StyledMenuItem'", () => { + it.each([ + ["left", "flex-start"], + ["right", "flex-end"], + ])( + "when horizontalAlignment is %s, content should be justified %s", + (alignment, itemAlignment) => { + render({ horizontalAlignment: alignment }); + openMenu(); + assertStyleMatch( + { + justifyContent: itemAlignment, + }, + wrapper.find(StyledMenuItem) + ); + } + ); + + it.each([ + ["left", "right", "space-between", 0], + ["right", "left", "flex-end", 1], + ])( + "when horizontalAlignment is %s, and submenuPosition is %s, without submenu content should be justified: flex-end", + (alignment, position, itemAlignment, index) => { + render({ horizontalAlignment: alignment, submenuPosition: position }); + + openMenu(); + assertStyleMatch( + { + justifyContent: itemAlignment, + }, + wrapper.find(StyledMenuItem).at(index) + ); + } + ); + + it.each([ + ["left", "right", 0], + ["right", "left", 1], + ])( + "when horizontalAlignment is %s, and submenuPosition is %s, with submenu content should be justified: space-between", + (alignment, position, index) => { + renderWithSubmenu({ + horizontalAlignment: alignment, + submenuPosition: position, + }); + + openMenu(); + assertStyleMatch( + { + justifyContent: "space-between", + }, + wrapper.find(StyledMenuItem).at(index) + ); + } + ); + }); + + describe("padding checks on 'MenuItemIcon'", () => { + it.each([ + [ + "left", + "left", + "var(--spacing100) var(--spacing100) var(--spacing100) var(--spacing400)", + ], + [ + "left", + "right", + "var(--spacing100) var(--spacing100) var(--spacing100) var(--spacing100)", + ], + [ + "right", + "left", + "var(--spacing100) var(--spacing100) var(--spacing100) var(--spacing100)", + ], + [ + "right", + "right", + "var(--spacing100) var(--spacing400) var(--spacing100) var(--spacing100)", + ], + ])( + "when iconState, caretState and icon are all is true and submenu is false, submenuPosition is %s and horizontalAlignment is %s, padding is %s", + (position, alignment, padding) => { + renderWithSubmenu({ + submenuPosition: position, + horizontalAlignment: alignment, + }); + openMenu(); + assertStyleMatch( + { + padding, + }, + wrapper.find(MenuItemIcon) + ); + } + ); - it("then menu item icon should have correct left padding", () => { + it("when iconState, caretState and icon are all true and submenu is false, padding is var(--spacing100)", () => { + renderWithSubmenu(); openMenu(); assertStyleMatch( { - padding: "var(--spacing100)", + padding: + "var(--spacing100) var(--spacing100) var(--spacing100) var(--spacing100)", }, - wrapper.find(MenuItemIcon) + wrapper.find(MenuItemIcon).at(1) ); }); }); diff --git a/src/components/action-popover/action-popover.stories.tsx b/src/components/action-popover/action-popover.stories.tsx index 5d10018d29..ba0b932c3b 100644 --- a/src/components/action-popover/action-popover.stories.tsx +++ b/src/components/action-popover/action-popover.stories.tsx @@ -170,6 +170,22 @@ export const ActionPopoverComponentContentAlignedRight: ComponentStory< ); }; +export const ActionPopoverComponentSubmenuPositionedRight: ComponentStory< + typeof ActionPopover +> = () => { + return ( +
+ + + Email Invoice + + Delete + + +
+ ); +}; + export const ActionPopoverComponentNoIcons: ComponentStory< typeof ActionPopover > = () => { @@ -300,39 +316,6 @@ export const ActionPopoverComponentDisabledSubmenu: ComponentStory< ); }; -export const ActionPopoverComponentSubmenuAlignedRight: ComponentStory< - typeof ActionPopover -> = () => { - return ( -
- - - {}} - submenu={ - - {}}> - CSV - - {}}> - PDF - - - } - > - Print - - - {}} icon="delete"> - Delete - - - -
- ); -}; - export const ActionPopoverComponentMenuOpeningAbove: ComponentStory< typeof ActionPopover > = () => { diff --git a/src/components/action-popover/action-popover.style.ts b/src/components/action-popover/action-popover.style.ts index 2ef4c51d47..53c956fa4e 100644 --- a/src/components/action-popover/action-popover.style.ts +++ b/src/components/action-popover/action-popover.style.ts @@ -1,3 +1,4 @@ +import React from "react"; import styled, { css } from "styled-components"; import { margin } from "styled-system"; @@ -19,12 +20,143 @@ const Menu = styled.div` `${theme.zIndex?.popover}`}; // TODO (tokens): implement elevation tokens - FE-4437 `; +type variants = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10; + +function getLeftPaddingValues(variant: variants) { + switch (variant) { + case 1: + return "var(--spacing100)"; + case 2: + return "var(--spacing400)"; + case 3: + return "var(--spacing100)"; + case 4: + return "var(--spacing100)"; + case 5: + return "var(--spacing000)"; + case 6: + return "var(--spacing000)"; + case 7: + return "var(--spacing000)"; + case 8: + return "var(--spacing500)"; + case 9: + return "var(--spacing800)"; + default: + return "var(--spacing000)"; + } +} + +function getRightPaddingValues(variant: variants) { + switch (variant) { + case 1: + return "var(--spacing100)"; + case 2: + return "var(--spacing400)"; + case 3: + return "var(--spacing100)"; + case 4: + return "var(--spacing100)"; + case 5: + return "var(--spacing100)"; + case 6: + return "var(--spacing100)"; + case 7: + return "var(--spacing100)"; + case 8: + return "var(--spacing600)"; + case 9: + return "var(--spacing900)"; + default: + return "var(--spacing000)"; + } +} + +function getIconPaddingValues( + index: 1 | 2, + variant: number, + horizontalAlignment: string, + submenuPosition: string, + isASubmenu: boolean +) { + const sameAlignment = + (horizontalAlignment === "left" && submenuPosition === "left") || + (horizontalAlignment === "right" && submenuPosition === "right"); + + if (variant === 7 && sameAlignment) { + if (horizontalAlignment === "left") { + return index === 1 ? "var(--spacing100)" : "var(--spacing400)"; + } + return index === 1 ? "var(--spacing400)" : "var(--spacing100)"; + } + + if (isASubmenu) { + return "var(--spacing000)"; + } + + return "var(--spacing100)"; +} + type StyledMenuItemProps = { isDisabled: boolean; - horizontalAlignment: "left" | "right"; + horizontalAlignment?: "left" | "right"; + caretState?: boolean; + hasSubmenu: React.ReactNode; + submenuPosition: "left" | "right"; + isASubmenu: boolean; + variant: variants; }; -const StyledMenuItem = styled.button` +const StyledMenuItemInnerText = styled.div< + Omit +>` + ${({ variant, submenuPosition, horizontalAlignment, isASubmenu }) => css` + padding-left: ${isASubmenu ? `var(--spacing000)` : `var(--spacing100)`}; + padding-right: ${isASubmenu ? `var(--spacing000)` : `var(--spacing100)`}; + + ${horizontalAlignment === "left" && + submenuPosition === "left" && + !isASubmenu && + css` + padding-left: ${getLeftPaddingValues(variant)}; + `} + + ${horizontalAlignment === "right" && + submenuPosition === "right" && + !isASubmenu && + css` + padding-right: ${getRightPaddingValues(variant)}; + `} + `} +`; + +const StyledMenuItemOuterContainer = styled.div` + display: inherit; +`; +const StyledMenuItem = styled.button>` + ${({ horizontalAlignment, submenuPosition, caretState, hasSubmenu }) => + css` + justify-content: ${horizontalAlignment === "left" + ? "flex-start" + : "flex-end"}; + + ${horizontalAlignment === "left" && + submenuPosition === "right" && + css` + justify-content: space-between; + `} + + ${horizontalAlignment === "right" && + submenuPosition === "left" && + css` + ${caretState && + hasSubmenu && + css` + justify-content: space-between; + `} + `} + `} + text-decoration: none; background-color: var(--colorsActionMajorYang100); cursor: pointer; @@ -41,8 +173,6 @@ const StyledMenuItem = styled.button` color: var(--colorsUtilityYin090); font-size: 14px; font-weight: 700; - justify-content: ${({ horizontalAlignment }) => - horizontalAlignment === "left" ? "flex-start" : "flex-end"}; &:focus { outline: var(--borderWidth300) solid var(--colorsSemanticFocus500); @@ -112,20 +242,42 @@ const StyledButtonIcon = styled.div` } `; -const MenuItemIcon = styled(Icon)` - padding: var(--spacing100); - color: var(--colorsUtilityYin065); +const MenuItemIcon = styled(Icon)< + Pick< + StyledMenuItemProps, + "horizontalAlignment" | "submenuPosition" | "variant" | "isASubmenu" + > +>` + ${({ horizontalAlignment, submenuPosition, variant, isASubmenu }) => css` + justify-content: ${horizontalAlignment}; + padding: var(--spacing100) + ${getIconPaddingValues( + 1, + variant, + horizontalAlignment!, + submenuPosition, + isASubmenu + )} + var(--spacing100) + ${getIconPaddingValues( + 2, + variant, + horizontalAlignment!, + submenuPosition, + isASubmenu + )}; + color: var(--colorsUtilityYin065); + `} `; const SubMenuItemIcon = styled(ButtonIcon)` ${({ type }) => css` - position: absolute; - ${type === "chevron_left" && + ${type === "chevron_left_thick" && css` left: -2px; `} - ${type === "chevron_right" && + ${type === "chevron_right_thick" && css` right: -5px; ${isSafari(navigator) && @@ -162,6 +314,8 @@ export { MenuItemDivider, SubMenuItemIcon, MenuButtonOverrideWrapper, + StyledMenuItemInnerText, + StyledMenuItemOuterContainer, StyledMenuItem, StyledMenuItemWrapper, };