diff --git a/apps/website/pages/components/contextual-menu/code.tsx b/apps/website/pages/components/contextual-menu/code.tsx new file mode 100644 index 0000000000..265615d2ad --- /dev/null +++ b/apps/website/pages/components/contextual-menu/code.tsx @@ -0,0 +1,17 @@ +import Head from "next/head"; +import type { ReactElement } from "react"; +import ContextualMenuPageLayout from "screens/components/contextual-menu/ContextualMenuPageLayout"; +import ContextualMenuCodePage from "screens/components/contextual-menu/code/ContextualMenuCodePage"; + +const Code = () => ( + <> + + Contextual Menu Code — Halstack Design System + + + +); + +Code.getLayout = (page: ReactElement) => {page}; + +export default Code; diff --git a/apps/website/pages/components/contextual-menu/index.tsx b/apps/website/pages/components/contextual-menu/index.tsx index 616e6a7067..9cfdf09f6a 100644 --- a/apps/website/pages/components/contextual-menu/index.tsx +++ b/apps/website/pages/components/contextual-menu/index.tsx @@ -1,21 +1,17 @@ import Head from "next/head"; import type { ReactElement } from "react"; -import ContextualMenuCodePage from "screens/components/contextual-menu/code/ContextualMenuCodePage"; +import ContextualMenuOverviewPage from "screens/components/contextual-menu/overview/ContextualMenuOverviewPage"; import ContextualMenuPageLayout from "screens/components/contextual-menu/ContextualMenuPageLayout"; -const Usage = () => { - return ( - <> - - Contextual Menu — Halstack Design System - - - - ); -}; +const Usage = () => ( + <> + + Contextual Menu — Halstack Design System + + + +); -Usage.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; +Usage.getLayout = (page: ReactElement) => {page}; export default Usage; diff --git a/apps/website/pages/components/contextual-menu/specifications.tsx b/apps/website/pages/components/contextual-menu/specifications.tsx deleted file mode 100644 index 0c92317af0..0000000000 --- a/apps/website/pages/components/contextual-menu/specifications.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import Head from "next/head"; -import type { ReactElement } from "react"; -import ContextualMenuSpecsPage from "screens/components/contextual-menu/specs/ContextualMenuSpecsPage"; -import ContextualMenuPageLayout from "screens/components/contextual-menu/ContextualMenuPageLayout"; - -const Specifications = () => { - return ( - <> - - Contextual Menu Specs — Halstack Design System - - - - ); -}; - -Specifications.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; - -export default Specifications; diff --git a/apps/website/pages/components/contextual-menu/usage.tsx b/apps/website/pages/components/contextual-menu/usage.tsx deleted file mode 100644 index d2d652977e..0000000000 --- a/apps/website/pages/components/contextual-menu/usage.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import Head from "next/head"; -import type { ReactElement } from "react"; -import ContextualMenuPageLayout from "screens/components/contextual-menu/ContextualMenuPageLayout"; -import ContextualMenuUsagePage from "screens/components/contextual-menu/usage/ContextualMenuUsagePage"; - -const Usage = () => { - return ( - <> - - Contextual Menu Usage — Halstack Design System - - - - ); -}; - -Usage.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; - -export default Usage; diff --git a/apps/website/screens/components/contextual-menu/ContextualMenuPageLayout.tsx b/apps/website/screens/components/contextual-menu/ContextualMenuPageLayout.tsx index 000323545b..1d50e8da02 100644 --- a/apps/website/screens/components/contextual-menu/ContextualMenuPageLayout.tsx +++ b/apps/website/screens/components/contextual-menu/ContextualMenuPageLayout.tsx @@ -6,9 +6,8 @@ import { ReactNode } from "react"; const ContextualMenuPageHeading = ({ children }: { children: ReactNode }) => { const tabs = [ - { label: "Code", path: "/components/contextual-menu" }, - { label: "Usage", path: "/components/contextual-menu/usage" }, - { label: "Specifications", path: "/components/contextual-menu/specifications" }, + { label: "Overview", path: "/components/contextual-menu" }, + { label: "Code", path: "/components/contextual-menu/code" }, ]; return ( @@ -17,11 +16,10 @@ const ContextualMenuPageHeading = ({ children }: { children: ReactNode }) => { - The Contextual menu is a powerful component that improves user experience by allowing users to navigate - through page-level content or choose from a list of actions while complementing the general disposition of - the main content within the interface. + The Contextual menu provides quick access to navigation or actions related to the current context, enhancing + usability and content organization. - + {children} diff --git a/apps/website/screens/components/contextual-menu/code/ContextualMenuCodePage.tsx b/apps/website/screens/components/contextual-menu/code/ContextualMenuCodePage.tsx index 01259eab52..c7e80cc18e 100644 --- a/apps/website/screens/components/contextual-menu/code/ContextualMenuCodePage.tsx +++ b/apps/website/screens/components/contextual-menu/code/ContextualMenuCodePage.tsx @@ -86,15 +86,13 @@ const sections = [ }, ]; -const ContextualMenuCodePage = () => { - return ( - - - - - - - ); -}; +const ContextualMenuCodePage = () => ( + + + + + + +); export default ContextualMenuCodePage; diff --git a/apps/website/screens/components/contextual-menu/overview/ContextualMenuOverviewPage.tsx b/apps/website/screens/components/contextual-menu/overview/ContextualMenuOverviewPage.tsx new file mode 100644 index 0000000000..b2a4d6dc58 --- /dev/null +++ b/apps/website/screens/components/contextual-menu/overview/ContextualMenuOverviewPage.tsx @@ -0,0 +1,141 @@ +import { DxcParagraph, DxcFlex, DxcBulletedList } from "@dxc-technology/halstack-react"; +import DocFooter from "@/common/DocFooter"; +import QuickNavContainer from "@/common/QuickNavContainer"; +import QuickNavContainerLayout from "@/common/QuickNavContainerLayout"; +import Image from "@/common/Image"; +import anatomy from "./images/contextual-menu_anatomy.png"; + +const sections = [ + { + title: "Introduction", + content: ( + + This powerful component improves user experience by allowing users to{" "} + navigate through page-level content or choose from a list of actions while complementing the + general disposition of the main content within the interface. It also allows a wide range of possibilities when + it comes to placing content cohesively and comprehensively. To achieve this, it's important to understand how + the items in our contextual menu behave and interact with each other. + + ), + }, + { + title: "Anatomy", + content: ( + <> + Contextual menu's anatomy + + + Section title: a label that groups related menu items within a contextual menu, enhancing + organization and readability. + + + Container: the structural wrapper that holds all menu elements, ensuring proper alignment, + spacing, and visual consistency. + + + Group item: they are nests of other group items or individual items that are related to + each other and show indentation as they are unfolded. + + + Badge: a small visual indicator, often used to display counts, status updates or categories + within a menu item. + + + Single item: an actionable element within the menu that triggers navigation or an operation + when selected. + + + Expand/collapse Icon: a visual indicator for nested menus, allowing users to reveal or hide + subitems. + + + Icon: a graphical representation within a menu item that aids recognition and reinforces + meaning. + + + Section: they are a collection of group and single items within the menu that share a + certain relationship and have a title that describes them. + + + Divider: a subtle separator that groups related menu items and improves menu structure. + + + Scrollbar: appears when the menu content exceeds the container's height, enabling vertical + navigation. + + + + ), + }, + { + title: "Contextual menu and Sidenav", + content: ( + <> + + Although visually similar, the Sidenav component and the contextual menu differ significantly in + functionality. Our Sidenav is designed to provide a consistent and persistent navigation structure throughout + the application, allowing users to easily switch between different sections or pages within the app. + + + On the other hand, the contextual menu is more{" "} + context-sensitive, and appears in response to specific user actions, offering a set of + relevant options or actions that can be performed on the current page. This means that it{" "} + operates on a page level, so the component may appear or not depending on the specific needs + and requirements for each screen or interaction. + + + ), + }, + { + title: "Best practices", + content: ( + <> + + + Use meaningful icons: Select icons that accurately represent menu items, ensuring clarity + and intuitive navigation. + + + Align properly: position the contextual menu to the left or right, avoiding placement in + the center to prevent obstruction of main content. + + + Enhance navigation with hierarchy: structure menu items using different levels to maintain + logical organization. + + + Use badges for status indication: incorporate a Badge component to display status updates, + counts or categories for navigable sections. + + + Default selection: when pre-selecting an option, try to limit it to the first menu item to + maintain intuitive user interactions. + + + Avoid deep hierarchies: limit navigation depth to a maximum of three levels to prevent + excessive indentation and complexity. + + + Restrict icon usage: use icons only at the first navigation level to maintain readability + and avoid visual clutter. + + + Don't overload with icons: too many icons can create confusion rather than improve + usability. Keep them purposeful and minimal. + + + + ), + }, +]; + +const ContextualMenuOverviewPage = () => ( + + + + + + +); + +export default ContextualMenuOverviewPage; diff --git a/apps/website/screens/components/contextual-menu/overview/images/contextual-menu_anatomy.png b/apps/website/screens/components/contextual-menu/overview/images/contextual-menu_anatomy.png new file mode 100644 index 0000000000..46a3d0396e Binary files /dev/null and b/apps/website/screens/components/contextual-menu/overview/images/contextual-menu_anatomy.png differ diff --git a/apps/website/screens/components/contextual-menu/specs/ContextualMenuSpecsPage.tsx b/apps/website/screens/components/contextual-menu/specs/ContextualMenuSpecsPage.tsx deleted file mode 100644 index ae6c4bcf68..0000000000 --- a/apps/website/screens/components/contextual-menu/specs/ContextualMenuSpecsPage.tsx +++ /dev/null @@ -1,335 +0,0 @@ -import { DxcFlex, DxcBulletedList, DxcParagraph, DxcTable } from "@dxc-technology/halstack-react"; -import QuickNavContainer from "@/common/QuickNavContainer"; -import QuickNavContainerLayout from "@/common/QuickNavContainerLayout"; -import DocFooter from "@/common/DocFooter"; -import Figure from "@/common/Figure"; -import Image from "@/common/Image"; -import anatomy from "./images/contextual_menu_anatomy.png"; -import contextualMenuSpecs from "./images/contextual_menu_specs.png"; -import contextualMenuItemSpecs from "./images/contextual_menu_item_specs.png"; -import itemStates from "./images/contextual_menu_item_states.png"; -import Code from "@/common/Code"; - -const sections = [ - { - title: "Specifications", - subSections: [ - { - title: "Contextual menu", - content: ( -
- Contextual Menu design specifications -
- ), - }, - { - title: "Contextual menu item", - content: ( -
- Contextual menu item design specifications -
- ), - }, - ], - }, - { - title: "States", - content: ( - <> -
- Contextual menu item states -
- - ), - }, - { - title: "Anatomy", - content: ( - <> - Contextual menu anatomy - - Section title - Container - Badge - Menu item - Expand/collapse icon - Icon - Divider - - - ), - }, - { - title: "Design tokens", - subSections: [ - { - title: "Color", - content: ( - - - - Component token - Element - Core token - Value - - - - - - backgroundColor - - Container - - color-white - - #ffffff - - - - borderColor - - Container - - color-grey-200 - - #e6e6e6 - - - - menuItemFontColor - - Menu item - - color-grey-900 - - #333333 - - - - hoverMenuItemBackgroundColor - - Menu item:hover - - color-grey-100 - - #f2f2f2 - - - - activeMenuItemBackgroundColor - - Menu item:active - - color-grey-100 - - #f2f2f2 - - - - selectedMenuItemBackgroundColor - - Menu item selected - - color-purple-100 - - #f2eafa - - - - hoverSelectedMenuItemBackgroundColor - - Menu item:hover selected - - color-purple-200 - - #e5d5f6 - - - - activeSelectedMenuItemBackgroundColor - - Menu item:active selected - - color-purple-200 - - #e5d5f6 - - - - sectionTitleFontColor - - Section title - - color-grey-900 - - #333333 - - - - iconColor - - Icon - - color-grey-900 - - #333333 - - - - ), - }, - { - title: "Iconography", - content: ( - - - - Component token - Element - Core token - Value - - - - - - iconSize - - Icon - - - 16px - - - - ), - }, - { - title: "Typography", - content: ( - - - - Component token - Element - Core token - Value - - - - - - fontFamily - - Contextual menu - - font-family-sans - - 'Open Sans', sans-serif - - - - menuItemFontSize - - Menu item - - font-scale-02 - - 0.875rem / 14px - - - - menuItemFontStyle - - Menu item - - font-style-normal - - normal - - - - menuItemFontWeight - - Menu item - - font-weight-regular - - 400 - - - - menuItemLineHeight - - Menu item - - - 24px - - - - selectedMenuItemFontWeight - - Menu item selected - - font-weight-semibold - - 600 - - - - sectionTitleFontSize - - Section title - - font-scale-03 - - 1rem / 16px - - - - sectionTitleFontStyle - - Section title - - font-style-normal - - normal - - - - sectionTitleFontWeight - - Section title - - font-weight-semibold - - 600 - - - - sectionTitleLineHeight - - Section title - - - 24px - - - - ), - }, - ], - }, -]; - -const ContextualMenuSpecsPage = () => { - return ( - - - - - - - ); -}; - -export default ContextualMenuSpecsPage; diff --git a/apps/website/screens/components/contextual-menu/specs/images/contextual_menu_anatomy.png b/apps/website/screens/components/contextual-menu/specs/images/contextual_menu_anatomy.png deleted file mode 100644 index 82e7d1d5a8..0000000000 Binary files a/apps/website/screens/components/contextual-menu/specs/images/contextual_menu_anatomy.png and /dev/null differ diff --git a/apps/website/screens/components/contextual-menu/specs/images/contextual_menu_item_specs.png b/apps/website/screens/components/contextual-menu/specs/images/contextual_menu_item_specs.png deleted file mode 100644 index 13344874e7..0000000000 Binary files a/apps/website/screens/components/contextual-menu/specs/images/contextual_menu_item_specs.png and /dev/null differ diff --git a/apps/website/screens/components/contextual-menu/specs/images/contextual_menu_item_states.png b/apps/website/screens/components/contextual-menu/specs/images/contextual_menu_item_states.png deleted file mode 100644 index c02d0814f3..0000000000 Binary files a/apps/website/screens/components/contextual-menu/specs/images/contextual_menu_item_states.png and /dev/null differ diff --git a/apps/website/screens/components/contextual-menu/specs/images/contextual_menu_specs.png b/apps/website/screens/components/contextual-menu/specs/images/contextual_menu_specs.png deleted file mode 100644 index 265559bab4..0000000000 Binary files a/apps/website/screens/components/contextual-menu/specs/images/contextual_menu_specs.png and /dev/null differ diff --git a/apps/website/screens/components/contextual-menu/usage/ContextualMenuUsagePage.tsx b/apps/website/screens/components/contextual-menu/usage/ContextualMenuUsagePage.tsx deleted file mode 100644 index 013b2eba16..0000000000 --- a/apps/website/screens/components/contextual-menu/usage/ContextualMenuUsagePage.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { DxcParagraph, DxcFlex, DxcBulletedList } from "@dxc-technology/halstack-react"; -import DocFooter from "@/common/DocFooter"; -import QuickNavContainer from "@/common/QuickNavContainer"; -import QuickNavContainerLayout from "@/common/QuickNavContainerLayout"; -import Figure from "@/common/Figure"; -import Image from "@/common/Image"; -import contextualMenuElements from "./images/contextual_menu_elements.png"; - -const sections = [ - { - title: "Usage", - content: ( - - This component allows a wide range of possibilities when it comes to placing content cohesively and - comprehensively. To achieve this, it's important to understand how the items in our Contextual menu behave and - interact with each other. - - ), - subSections: [ - { - title: "Do's", - content: ( - - - Choose icons based on their relevance to the items they represent, ensuring accurate and clear - descriptions. - - - Place the Contextual menu aligned to the left or right, but never in the center of the interface. This - component is a complement to navigate within the page, but it should never obstruct the main content. - - - Use different navigation levels to structure the elements logically. - - - Use our Badge component as a complement to the menu items to show the status of any navigable section. - - - We strongly recommend only selecting by default the first option of the menu. Any other option may become - unintuitive for the user. - - - ), - }, - { - title: "Don'ts", - content: ( - - - Use icons on items that don't belong to the first level of navigation. Keep in mind that icons are limited - to the first level of navigation, as having icons in the subsequent elements can interfere with the user's - reading of the content. - - - Use an excessive amount of icons. While they can enhance the visual appeal and usability of a menu, - overusing them can lead to confusion and clutter. - - - Use more than three levels of navigation, as excessive indentation can be confusing and distracting for - the user. - - - ), - }, - ], - }, - { - title: "Elements of the Contextual menu", - content: ( - <> - - The Contextual menu is composed of different elements that allow the user to navigate through the interface. - Each of these elements has a different criteria and behaviour, and they are as follows: - - - - Sections: they have a title and are a collection of group and single items within the menu - that share a certain relationship. - - - Group items: they are nests of other group items or individual items that are related to - each other and show indentation as they are unfolded. - - - Single items: they are items that carry on a specific change to the interface and don't - contain any type of nesting. - - - Divider: its purpose is to separate sections within the Contextual menu. They only appear - at the end of one section and right before the following one. - - - Scrollbar: only present when the scrollable function is available. - - -
- Every possible element of the Contextual menu -
- - ), - }, - { - title: "Contextual menu and Sidenav", - content: ( - <> - - Although visually similar, the Sidenav component and the Contextual menu differ significantly in - functionality. Our Sidenav is designed to provide a consistent and persistent navigation structure throughout - the application, allowing users to easily switch between different sections or pages within the app. - - - On the other hand, the Contextual menu is more context-sensitive, and appears in response to specific user - actions, offering a set of relevant options or actions that can be performed on the current page. This means - that it operates on a page level, so the component may appear or not depending on the specific needs and - requirements for each screen or interaction. - - - ), - }, -]; - -const ContextualMenuUsagePage = () => { - return ( - - - - - - - ); -}; - -export default ContextualMenuUsagePage; diff --git a/apps/website/screens/components/contextual-menu/usage/images/contextual_menu_elements.png b/apps/website/screens/components/contextual-menu/usage/images/contextual_menu_elements.png deleted file mode 100644 index 5c576ca7f0..0000000000 Binary files a/apps/website/screens/components/contextual-menu/usage/images/contextual_menu_elements.png and /dev/null differ diff --git a/packages/lib/src/contextual-menu/ContextualMenu.tsx b/packages/lib/src/contextual-menu/ContextualMenu.tsx index 98b3c6a629..2285398aa6 100644 --- a/packages/lib/src/contextual-menu/ContextualMenu.tsx +++ b/packages/lib/src/contextual-menu/ContextualMenu.tsx @@ -1,87 +1,40 @@ -import { useContext, useLayoutEffect, useMemo, useRef, useState } from "react"; -import styled, { ThemeProvider } from "styled-components"; -import CoreTokens from "../common/coreTokens"; +import { useLayoutEffect, useMemo, useRef, useState } from "react"; +import styled from "styled-components"; import MenuItem from "./MenuItem"; import ContextualMenuPropsType, { - GroupItem, GroupItemWithId, - Item, ItemWithId, - SubMenuProps, - Section as SectionType, SectionWithId, } from "./types"; import Section from "./Section"; import ContextualMenuContext from "./ContextualMenuContext"; -import HalstackContext from "../HalstackContext"; +import { scrollbarStyles } from "../styles/scroll"; +import { addIdToItems, isSection } from "./utils"; +import SubMenu from "./SubMenu"; const ContextualMenu = styled.div` box-sizing: border-box; margin: 0; - border: 1px solid ${({ theme }) => theme.borderColor}; - border-radius: 0.25rem; - padding: ${CoreTokens.spacing_16} ${CoreTokens.spacing_8}; + border: var(--border-width-s) var(--border-style-default) var(--border-color-neutral-lighter); + border-radius: var(--border-radius-s); + padding: var(--spacing-padding-m) var(--spacing-padding-xs); display: grid; - gap: ${CoreTokens.spacing_4}; + gap: var(--spacing-gap-xs); min-width: 248px; max-height: 100%; - background-color: ${({ theme }) => theme.backgroundColor}; + background-color: var(--color-bg-neutral-lightest); overflow-y: auto; overflow-x: hidden; - &::-webkit-scrollbar { - width: 8px; - height: 8px; - } - &::-webkit-scrollbar-thumb { - background-color: ${CoreTokens.color_grey_700}; - border-radius: 0.25rem; - } - &::-webkit-scrollbar-track { - background-color: ${CoreTokens.color_grey_300}; - border-radius: 0.25rem; - } + ${scrollbarStyles} `; -const StyledSubMenu = styled.ul` - margin: 0; - padding: 0; - display: grid; - gap: ${CoreTokens.spacing_4}; - list-style: none; -`; - -const isGroupItem = (item: Item | GroupItem): item is GroupItem => "items" in item; -const isSection = (item: SectionType | Item | GroupItem): item is SectionType => "items" in item && !("label" in item); -const addIdToItems = (items: ContextualMenuPropsType["items"]): (ItemWithId | GroupItemWithId | SectionWithId)[] => { - let accId = 0; - const innerAddIdToItems = ( - items: ContextualMenuPropsType["items"] - ): (ItemWithId | GroupItemWithId | SectionWithId)[] => { - return items.map((item: Item | GroupItem | SectionType) => - isSection(item) - ? ({ ...item, items: innerAddIdToItems(item.items) } as SectionWithId) - : isGroupItem(item) - ? ({ ...item, items: innerAddIdToItems(item.items) } as GroupItemWithId) - : { ...item, id: accId++ } - ); - }; - return innerAddIdToItems(items); -}; - -export const SubMenu = ({ children, id }: SubMenuProps) => ( - - {children} - -); - export default function DxcContextualMenu({ items }: ContextualMenuPropsType) { + const [firstUpdate, setFirstUpdate] = useState(true); const [selectedItemId, setSelectedItemId] = useState(-1); const contextualMenuRef = useRef(null); const itemsWithId = useMemo(() => addIdToItems(items), [items]); const contextValue = useMemo(() => ({ selectedItemId, setSelectedItemId }), [selectedItemId, setSelectedItemId]); - const colorsTheme = useContext(HalstackContext); - const [firstUpdate, setFirstUpdate] = useState(true); useLayoutEffect(() => { if (selectedItemId !== -1 && firstUpdate) { const contextualMenuEl = contextualMenuRef.current; @@ -94,22 +47,20 @@ export default function DxcContextualMenu({ items }: ContextualMenuPropsType) { }, [firstUpdate, selectedItemId]); return ( - - - - {itemsWithId[0] && isSection(itemsWithId[0]) ? ( - (itemsWithId as SectionWithId[]).map((item, index) => ( -
- )) - ) : ( - - {(itemsWithId as (GroupItemWithId | ItemWithId)[]).map((item, index) => ( - - ))} - - )} - - - + + + {itemsWithId[0] && isSection(itemsWithId[0]) ? ( + (itemsWithId as SectionWithId[]).map((item, index) => ( +
+ )) + ) : ( + + {(itemsWithId as (GroupItemWithId | ItemWithId)[]).map((item, index) => ( + + ))} + + )} + + ); } diff --git a/packages/lib/src/contextual-menu/GroupItem.tsx b/packages/lib/src/contextual-menu/GroupItem.tsx index 95cad49611..ebf2c79f9b 100644 --- a/packages/lib/src/contextual-menu/GroupItem.tsx +++ b/packages/lib/src/contextual-menu/GroupItem.tsx @@ -1,19 +1,13 @@ import { useContext, useMemo, useState, memo, useId } from "react"; import DxcIcon from "../icon/Icon"; -import { SubMenu } from "./ContextualMenu"; +import SubMenu from "./SubMenu"; import ItemAction from "./ItemAction"; import MenuItem from "./MenuItem"; -import { GroupItemProps, ItemWithId } from "./types"; +import { GroupItemProps } from "./types"; import ContextualMenuContext from "./ContextualMenuContext"; +import { isGroupSelected } from "./utils"; -const isGroupSelected = (items: GroupItemProps["items"], selectedItemId?: number): boolean => - items.some((item) => { - if ("items" in item) return isGroupSelected(item.items, selectedItemId); - else if (selectedItemId !== -1) return item.id === selectedItemId; - else return (item as ItemWithId).selectedByDefault; - }); - -const GroupItem = ({ items, ...props }: GroupItemProps) => { +export default function GroupItem({ items, ...props }: GroupItemProps) { const groupMenuId = `group-menu-${useId()}`; const { selectedItemId } = useContext(ContextualMenuContext) ?? {}; const groupSelected = useMemo(() => isGroupSelected(items, selectedItemId), [items, selectedItemId]); @@ -42,5 +36,3 @@ const GroupItem = ({ items, ...props }: GroupItemProps) => { ); }; - -export default memo(GroupItem); diff --git a/packages/lib/src/contextual-menu/ItemAction.tsx b/packages/lib/src/contextual-menu/ItemAction.tsx index 3435b2bb60..083d8bb35f 100644 --- a/packages/lib/src/contextual-menu/ItemAction.tsx +++ b/packages/lib/src/contextual-menu/ItemAction.tsx @@ -1,104 +1,90 @@ -import { cloneElement, MouseEvent, useState } from "react"; +import { cloneElement, memo, MouseEvent, useState } from "react"; import styled from "styled-components"; -import CoreTokens from "../common/coreTokens"; import { ItemActionProps } from "./types"; import DxcIcon from "../icon/Icon"; import { TooltipWrapper } from "../tooltip/Tooltip"; -const ItemAction = ({ badge, collapseIcon, icon, label, depthLevel, ...props }: ItemActionProps) => { - const [hasTooltip, setHasTooltip] = useState(false); - const modifiedBadge = badge && cloneElement(badge, { size: "small" }); - - return ( - - - - {modifiedBadge} - - - ); -}; - const Action = styled.button<{ depthLevel: ItemActionProps["depthLevel"]; selected: ItemActionProps["selected"]; }>` + box-sizing: content-box; border: none; - border-radius: 4px; - padding: ${(props) => - `${CoreTokens.spacing_4} ${CoreTokens.spacing_8} ${CoreTokens.spacing_4} ${` - calc(${CoreTokens.spacing_8} + (${CoreTokens.spacing_24} * ${props.depthLevel})) - `};`}; - box-shadow: inset 0 0 0 2px transparent; + border-radius: var(--border-radius-s); + padding: var(--spacing-padding-xxs) var(--spacing-padding-xxs) var(--spacing-padding-xxs) + ${({ depthLevel }) => `calc(var(--spacing-padding-xs) + ${depthLevel} * var(--spacing-padding-l))`}; display: flex; align-items: center; + gap: var(--spacing-gap-m); justify-content: space-between; - gap: ${CoreTokens.spacing_16}; - ${({ selected, theme }) => - selected - ? `background-color: ${theme.selectedMenuItemBackgroundColor};` - : `background-color: ${CoreTokens.color_transparent}`}; + background-color: ${({ selected }) => (selected ? "var(--color-bg-primary-lighter)" : "transparent")}; + height: var(--height-s); cursor: pointer; overflow: hidden; &:hover { - ${({ selected, theme }) => - selected - ? `background-color: ${theme.hoverSelectedMenuItemBackgroundColor};` - : `background-color: ${theme.hoverMenuItemBackgroundColor};`}; + background-color: ${({ selected }) => + selected ? "var(--color-bg-primary-medium)" : "var(--color-bg-neutral-light)"}; } &:active { - ${({ selected, theme }) => - selected - ? `background-color: ${theme.activeSelectedMenuItemBackgroundColor};` - : `background-color: ${theme.activeMenuItemBackgroundColor};`}; + background-color: ${({ selected }) => + selected ? "var(--color-bg-primary-medium)" : "var(--color-bg-neutral-light)"}; } &:focus { - outline: 2px solid ${CoreTokens.color_blue_600}; - outline-offset: -1px; - } -`; - -const Icon = styled.span` - display: flex; - font-size: ${({ theme }) => theme.iconSize}; - color: ${({ theme }) => theme.iconColor}; - - svg { - height: ${({ theme }) => theme.iconSize}; - width: ${({ theme }) => theme.iconSize}; + outline: var(--border-width-m) var(--border-style-default) var(--border-color-secondary-medium); + outline-offset: -2px; } `; const Label = styled.span` display: flex; align-items: center; - gap: ${CoreTokens.spacing_8}; + gap: var(--spacing-gap-s); overflow: hidden; `; +const Icon = styled.span` + display: flex; + color: var(--color-fg-neutral-dark); + font-size: var(--height-xxs); + svg { + height: var(--height-xxs); + width: 16px; + } +`; + const Text = styled.span<{ selected: ItemActionProps["selected"] }>` - color: ${({ theme }) => theme.menuItemFontColor}; - font-family: ${({ theme }) => theme.fontFamily}; - font-size: ${({ theme }) => theme.menuItemFontSize}; - font-style: ${({ theme }) => theme.menuItemFontStyle}; - font-weight: ${({ selected, theme }) => (selected ? theme.selectedMenuItemFontWeight : theme.menuItemFontWeight)}; - line-height: ${({ theme }) => theme.menuItemLineHeight}; + color: var(--color-fg-neutral-dark); + font-family: var(--typography-font-family); + font-size: var(--typography-label-m); + font-weight: ${({ selected }) => (selected ? "var(--typography-label-semibold)" : "var(--typography-label-regular)")}; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; `; -export default ItemAction; +export default memo(function ItemAction({ badge, collapseIcon, depthLevel, icon, label, ...props }: ItemActionProps) { + const [hasTooltip, setHasTooltip] = useState(false); + const modifiedBadge = badge && cloneElement(badge, { size: "small" }); + + return ( + + + + {modifiedBadge} + + + ); +}); diff --git a/packages/lib/src/contextual-menu/MenuItem.tsx b/packages/lib/src/contextual-menu/MenuItem.tsx index 0c4eeab9ff..4b3fcfb1c7 100644 --- a/packages/lib/src/contextual-menu/MenuItem.tsx +++ b/packages/lib/src/contextual-menu/MenuItem.tsx @@ -1,22 +1,21 @@ import styled from "styled-components"; -import CoreTokens from "../common/coreTokens"; import GroupItem from "./GroupItem"; import SingleItem from "./SingleItem"; import { MenuItemProps } from "./types"; -const MenuItem = ({ item, depthLevel = 0 }: MenuItemProps) => ( - - {"items" in item ? ( - - ) : ( - - )} - -); - -const StyledMenuItem = styled.li` +const MenuItemContainer = styled.li` display: grid; - gap: ${CoreTokens.spacing_4}; + gap: var(--spacing-gap-xs); `; -export default MenuItem; +export default function MenuItem({ item, depthLevel = 0 }: MenuItemProps) { + return ( + + {"items" in item ? ( + + ) : ( + + )} + + ); +} diff --git a/packages/lib/src/contextual-menu/Section.tsx b/packages/lib/src/contextual-menu/Section.tsx index 07d8029f50..a8a09e47d3 100644 --- a/packages/lib/src/contextual-menu/Section.tsx +++ b/packages/lib/src/contextual-menu/Section.tsx @@ -1,17 +1,30 @@ import styled from "styled-components"; import { DxcInset } from ".."; -import CoreTokens from "../common/coreTokens"; import DxcDivider from "../divider/Divider"; -import { SubMenu } from "./ContextualMenu"; +import SubMenu from "./SubMenu"; import MenuItem from "./MenuItem"; import { SectionProps } from "./types"; import { useId } from "react"; -const Section = ({ section, index, length }: SectionProps) => { +const SectionContainer = styled.section` + display: grid; + gap: var(--spacing-gap-xs); +`; + +const Title = styled.h2` + all: unset; + color: var(--color-fg-neutral-dark); + font-family: var(--typography-font-family); + font-size: var(--typography-label-l); + font-weight: var(--typography-label-semibold); + padding: var(--spacing-padding-xxs); +`; + +export default function Section({ index, length, section }: SectionProps) { const id = `section-${useId()}`; return ( -
+ {section.title && {section.title}} {section.items.map((item, index) => ( @@ -23,19 +36,6 @@ const Section = ({ section, index, length }: SectionProps) => { )} -
+ ); -}; - -const Title = styled.h2` - margin: 0 0 ${CoreTokens.spacing_4} 0; - padding: ${CoreTokens.spacing_4}; - color: ${({ theme }) => theme.sectionTitleFontColor}; - font-family: ${({ theme }) => theme.fontFamily}; - font-size: ${({ theme }) => theme.sectionTitleFontSize}; - font-style: ${({ theme }) => theme.sectionTitleFontStyle}; - font-weight: ${({ theme }) => theme.sectionTitleFontWeight}; - line-height: ${({ theme }) => theme.sectionTitleLineHeight}; -`; - -export default Section; +} diff --git a/packages/lib/src/contextual-menu/SingleItem.tsx b/packages/lib/src/contextual-menu/SingleItem.tsx index 447a2c6a41..df86ea61da 100644 --- a/packages/lib/src/contextual-menu/SingleItem.tsx +++ b/packages/lib/src/contextual-menu/SingleItem.tsx @@ -3,7 +3,7 @@ import ItemAction from "./ItemAction"; import { SingleItemProps } from "./types"; import ContextualMenuContext from "./ContextualMenuContext"; -const SingleItem = ({ id, onSelect, selectedByDefault = false, ...props }: SingleItemProps) => { +export default function SingleItem({ id, onSelect, selectedByDefault = false, ...props }: SingleItemProps) { const { selectedItemId, setSelectedItemId } = useContext(ContextualMenuContext) ?? {}; const handleClick = () => { @@ -25,6 +25,4 @@ const SingleItem = ({ id, onSelect, selectedByDefault = false, ...props }: Singl {...props} /> ); -}; - -export default SingleItem; +} diff --git a/packages/lib/src/contextual-menu/SubMenu.tsx b/packages/lib/src/contextual-menu/SubMenu.tsx new file mode 100644 index 0000000000..f7d9269b70 --- /dev/null +++ b/packages/lib/src/contextual-menu/SubMenu.tsx @@ -0,0 +1,18 @@ +import styled from "styled-components"; +import { SubMenuProps } from "./types"; + +const SubMenuContainer = styled.ul` + margin: 0; + padding: 0; + display: grid; + gap: var(--spacing-gap-xs); + list-style: none; +`; + +export default function SubMenu({ children, id }: SubMenuProps) { + return ( + + {children} + + ); +} diff --git a/packages/lib/src/contextual-menu/utils.ts b/packages/lib/src/contextual-menu/utils.ts new file mode 100644 index 0000000000..a77c213b0b --- /dev/null +++ b/packages/lib/src/contextual-menu/utils.ts @@ -0,0 +1,36 @@ +import ContextualMenuPropsType, { + GroupItem, + GroupItemProps, + GroupItemWithId, + Item, + ItemWithId, + Section as SectionType, + SectionWithId, +} from "./types"; + +export const isGroupItem = (item: Item | GroupItem): item is GroupItem => "items" in item; + +export const isSection = (item: SectionType | Item | GroupItem): item is SectionType => "items" in item && !("label" in item); + +export const addIdToItems = (items: ContextualMenuPropsType["items"]): (ItemWithId | GroupItemWithId | SectionWithId)[] => { + let accId = 0; + const innerAddIdToItems = ( + items: ContextualMenuPropsType["items"] + ): (ItemWithId | GroupItemWithId | SectionWithId)[] => { + return items.map((item: Item | GroupItem | SectionType) => + isSection(item) + ? ({ ...item, items: innerAddIdToItems(item.items) } as SectionWithId) + : isGroupItem(item) + ? ({ ...item, items: innerAddIdToItems(item.items) } as GroupItemWithId) + : { ...item, id: accId++ } + ); + }; + return innerAddIdToItems(items); +}; + +export const isGroupSelected = (items: GroupItemProps["items"], selectedItemId?: number): boolean => + items.some((item) => { + if ("items" in item) return isGroupSelected(item.items, selectedItemId); + else if (selectedItemId !== -1) return item.id === selectedItemId; + else return (item as ItemWithId).selectedByDefault; + }); \ No newline at end of file