From 780a3902ecd017589e23762c1c495eabe721f6e2 Mon Sep 17 00:00:00 2001 From: "tom.davies" Date: Wed, 21 Jun 2023 12:32:29 +0100 Subject: [PATCH] feat(action-popover): align component with design system 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 --- .../action-popover/action-popover.cy.js | 650 ++++++++++++- cypress/locators/action-popover/index.js | 9 + cypress/locators/action-popover/locators.js | 6 + .../action-popover-item.component.tsx | 206 ++++- .../action-popover-menu.component.tsx | 40 +- .../action-popover-test.stories.tsx | 441 ++++++++- .../action-popover.component.tsx | 4 + .../action-popover/action-popover.spec.tsx | 858 +++++++++++++++++- .../action-popover/action-popover.stories.mdx | 16 +- .../action-popover/action-popover.stories.tsx | 66 +- .../action-popover/action-popover.style.ts | 165 +++- 11 files changed, 2324 insertions(+), 137 deletions(-) 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..6cf9a083f5 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,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) { @@ -76,28 +94,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 +105,14 @@ export const ActionPopoverItem = ({ download, href, horizontalAlignment, + submenuPosition, + caretState, + iconState, + leftAlignmentState, + setCaretState, + setIconState, + setLeftAlignmentState, + isASubmenu = false, ...rest }: ActionPopoverItemProps) => { const l = useLocale(); @@ -139,19 +143,106 @@ export const ActionPopoverItem = ({ const mouseEnterTimer = useRef(null); const mouseLeaveTimer = useRef(null); + const currentSubmenuAlignment = () => { + if (submenuPosition === "left") { + if (leftAlignmentState) { + return "right"; + } + return "left"; + } + 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]); + + 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(); @@ -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); @@ -242,7 +333,15 @@ export const ActionPopoverItem = ({ e.stopPropagation(); } }, - [disabled, download, isHref, isLeftAligned, onClick, submenu] + [ + disabled, + download, + isHref, + isLeftAligned, + onClick, + submenu, + currentSubmenuAlignment(), + ] ); const itemSubmenuProps = { @@ -283,7 +382,19 @@ export const ActionPopoverItem = ({ }; const renderMenuItemIcon = () => { - return icon && ; + return ( + icon && ( + + ) + ); }; return ( @@ -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 ? ( - + {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 +455,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..cdfc4c7d38 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,10 @@ const ActionPopoverMenu = React.forwardRef< [focusButton, setOpen, focusIndex, items, setFocusIndex] ); + const [caretState, setCaretState] = useState(undefined); + const [iconState, setIconState] = useState(undefined); + const [leftAlignmentState, setLeftAlignmentState] = useState(undefined); + const clonedChildren = useMemo(() => { let index = 0; return React.Children.map(children, (child) => { @@ -171,12 +181,34 @@ const ActionPopoverMenu = React.forwardRef< focusItem: isOpen && focusIndex === index - 1, placement: child.props.submenu ? placement : undefined, horizontalAlignment, + submenuPosition, + caretState, + setCaretState, + iconState, + setIconState, + leftAlignmentState, + setLeftAlignmentState, + isASubmenu, }); } return child; }); - }, [children, focusIndex, isOpen, placement, horizontalAlignment]); + }, [ + children, + focusIndex, + isOpen, + placement, + horizontalAlignment, + submenuPosition, + caretState, + setCaretState, + iconState, + setIconState, + leftAlignmentState, + setLeftAlignmentState, + isASubmenu, + ]); return ( { Doe - {}}> + {}} + > Email Invoice - {}} icon="delete"> + {}} + icon="delete" + > Delete @@ -285,6 +295,433 @@ 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.mdx b/src/components/action-popover/action-popover.stories.mdx index 929a6b1c98..cada646c03 100644 --- a/src/components/action-popover/action-popover.stories.mdx +++ b/src/components/action-popover/action-popover.stories.mdx @@ -133,24 +133,20 @@ leave. -### With disabled submenu +### With submenu positioned to right -A sub-menu will not open if the item is in a disabled state. +Submenu can be positioned on either the left of the right with the use of the `submenuPosition` prop. - + -### With submenu aligned right +### With disabled submenu -The sub-menu will open to the right side when there is no space to render the sub-menu to the left. +A sub-menu will not open if the item is in a disabled state. - + ### With menu opening above diff --git a/src/components/action-popover/action-popover.stories.tsx b/src/components/action-popover/action-popover.stories.tsx index 5d10018d29..6b7a731667 100644 --- a/src/components/action-popover/action-popover.stories.tsx +++ b/src/components/action-popover/action-popover.stories.tsx @@ -41,7 +41,7 @@ export const ActionPopoverComponent: ComponentStory< {}}> Sub Menu 2 - {}}> + {}}> Sub Menu 3 @@ -80,7 +80,7 @@ export const ActionPopoverComponent: ComponentStory< Download CSV - + = () => { + const submenu = ( + + {}}>Sub Menu 1 + {}}>Sub Menu 2 + {}}> + Sub Menu 3 + + + ); + return ( +
+ + + + Email Invoice + + + + Delete + + + +
+ ); +}; + export const ActionPopoverComponentNoIcons: ComponentStory< typeof ActionPopover > = () => { @@ -300,39 +329,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..d6636b2f21 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,134 @@ 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; + +function getLeftPaddingValues(variant: variants) { + if (variant === 1) { + return "var(--spacing100)"; + } + if (variant === 2) { + return "var(--spacing400)"; + } + if (variant === 3 || variant === 4) { + return "var(--spacing100)"; + } + if (variant === 5 || variant === 6 || variant === 7) { + return "var(--spacing000)"; + } + if (variant === 8) { + return "var(--spacing500)"; + } + return "var(--spacing800)"; +} + +function getRightPaddingValues(variant: variants) { + if (variant === 1) { + return "var(--spacing100)"; + } + if (variant === 2) { + return "var(--spacing400)"; + } + if (variant === 3 || variant === 4) { + return "var(--spacing100)"; + } + if (variant === 5 || variant === 6 || variant === 7) { + return "var(--spacing100)"; + } + if (variant === 8) { + return "var(--spacing600)"; + } + return "var(--spacing900)"; +} + +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) { + if (horizontalAlignment === "left") { + return index === 1 ? "var(--spacing100)" : "var(--spacing000)"; + } + return index === 1 ? "var(--spacing000)" : "var(--spacing100)"; + } + + 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 +164,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 +233,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 +305,8 @@ export { MenuItemDivider, SubMenuItemIcon, MenuButtonOverrideWrapper, + StyledMenuItemInnerText, + StyledMenuItemOuterContainer, StyledMenuItem, StyledMenuItemWrapper, };