Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion lib/src/HalstackContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,22 @@ const parseTheme = (theme: DeepPartial<OpinionatedTheme>): AdvancedTheme => {
chipTokens.hoverIconColor = subLightness(10, theme?.chip?.iconColor) ?? chipTokens.hoverIconColor;
chipTokens.activeIconColor = subLightness(30, theme?.chip?.iconColor) ?? chipTokens.activeIconColor;

const contextualMenu = componentTokensCopy.contextualMenu;
contextualMenu.selectedMenuItemBackgroundColor =
theme?.contextualMenu?.accentColor ?? contextualMenu.selectedMenuItemBackgroundColor;
contextualMenu.hoverSelectedMenuItemBackgroundColor =
subLightness(5, theme?.contextualMenu?.accentColor) ?? contextualMenu.hoverSelectedMenuItemBackgroundColor;
contextualMenu.activeSelectedMenuItemBackgroundColor =
subLightness(5, theme?.contextualMenu?.accentColor) ?? contextualMenu.activeSelectedMenuItemBackgroundColor;
contextualMenu.backgroundColor = theme?.contextualMenu?.baseColor ?? contextualMenu.backgroundColor;
contextualMenu.hoverMenuItemBackgroundColor =
subLightness(5, theme?.contextualMenu?.baseColor) ?? contextualMenu.hoverMenuItemBackgroundColor;
contextualMenu.activeMenuItemBackgroundColor =
subLightness(5, theme?.contextualMenu?.baseColor) ?? contextualMenu.activeMenuItemBackgroundColor;
contextualMenu.menuItemFontColor = theme?.contextualMenu?.fontColor ?? contextualMenu.menuItemFontColor;
contextualMenu.sectionTitleFontColor = theme?.contextualMenu?.fontColor ?? contextualMenu.sectionTitleFontColor;
contextualMenu.iconColor = theme?.contextualMenu?.iconColor ?? contextualMenu.iconColor;

const dateTokens = componentTokensCopy.dateInput;
dateTokens.pickerSelectedBackgroundColor = theme?.dateInput?.baseColor ?? dateTokens.pickerSelectedBackgroundColor;
dateTokens.pickerSelectedFontColor = theme?.dateInput?.selectedFontColor ?? dateTokens.pickerSelectedFontColor;
Expand Down Expand Up @@ -379,7 +395,7 @@ type HalstackProviderPropsType = {
const HalstackProvider = ({ theme, advancedTheme, labels, children }: HalstackProviderPropsType): JSX.Element => {
const parsedTheme = useMemo(
() => (theme ? parseTheme(theme) : advancedTheme ? parseAdvancedTheme(advancedTheme) : componentTokens),
[theme, advancedTheme]
[theme, advancedTheme],
);
const parsedLabels = useMemo(() => (labels ? parseLabels(labels) : defaultTranslatedComponentLabels), [labels]);

Expand Down
1 change: 0 additions & 1 deletion lib/src/common/coreTokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,6 @@ const CoreTokens = {
// Typography
type_sans: "Open Sans, sans-serif",

type_scale_root: "16px",
type_scale_08: "3.75rem",
type_scale_07: "2.5rem",
type_scale_06: "2rem",
Expand Down
33 changes: 31 additions & 2 deletions lib/src/common/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,29 @@ export const componentTokens = {
focusBorderThickness: CoreTokens.border_width_2,
focusBorderRadius: CoreTokens.border_radius_medium,
},
contextualMenu: {
fontFamily: CoreTokens.type_sans,
backgroundColor: CoreTokens.color_white,
borderColor: CoreTokens.color_grey_200,
menuItemFontColor: CoreTokens.color_grey_900,
menuItemFontSize: CoreTokens.type_scale_02,
menuItemFontStyle: CoreTokens.type_normal,
menuItemFontWeight: CoreTokens.type_regular,
menuItemLineHeight: "24px",
hoverMenuItemBackgroundColor: CoreTokens.color_grey_100,
activeMenuItemBackgroundColor: CoreTokens.color_grey_100,
selectedMenuItemBackgroundColor: CoreTokens.color_purple_100,
hoverSelectedMenuItemBackgroundColor: CoreTokens.color_purple_200,
activeSelectedMenuItemBackgroundColor: CoreTokens.color_purple_200,
selectedMenuItemFontWeight: CoreTokens.type_semibold,
sectionTitleFontColor: CoreTokens.color_grey_900,
sectionTitleFontSize: CoreTokens.type_scale_03,
sectionTitleFontStyle: CoreTokens.type_normal,
sectionTitleFontWeight: CoreTokens.type_semibold,
sectionTitleLineHeight: "24px",
iconColor: CoreTokens.color_grey_900,
iconSize: "16px",
},
dateInput: {
pickerBackgroundColor: CoreTokens.color_white,
pickerFontColor: CoreTokens.color_black,
Expand Down Expand Up @@ -295,7 +318,7 @@ export const componentTokens = {
hoverOptionBackgroundColor: CoreTokens.color_grey_100,
activeOptionBackgroundColor: CoreTokens.color_grey_300,
optionFontFamily: CoreTokens.type_sans,
optionFontSize: CoreTokens.type_scale_root,
optionFontSize: CoreTokens.type_scale_03,
optionFontStyle: CoreTokens.type_normal,
optionFontWeight: CoreTokens.type_regular,
optionFontColor: CoreTokens.color_black,
Expand Down Expand Up @@ -824,7 +847,7 @@ export const componentTokens = {
disabledLabelFontColor: CoreTokens.color_grey_500,
disabledLabelFontStyle: CoreTokens.type_normal,
labelFontFamily: CoreTokens.type_sans,
labelFontSize: CoreTokens.type_scale_root,
labelFontSize: CoreTokens.type_scale_03,
labelFontStyle: CoreTokens.type_normal,
labelFontWeight: CoreTokens.type_regular,
labelFontColor: CoreTokens.color_black,
Expand Down Expand Up @@ -1176,6 +1199,12 @@ export type OpinionatedTheme = {
fontColor: string;
iconColor: string;
};
contextualMenu: {
accentColor: string;
baseColor: string;
fontColor: string;
iconColor: string;
},
dateInput: {
baseColor: string;
selectedFontColor: string;
Expand Down
84 changes: 46 additions & 38 deletions lib/src/contextual-menu/ContextualMenu.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import ExampleContainer from "../../.storybook/components/ExampleContainer";
import DxcBadge from "../badge/Badge";
import { disabledRules } from "../../test/accessibility/rules/specific/contextual-menu/disabledRules";
import preview from "../../.storybook/preview";
import { ThemeProvider } from "styled-components";
import useTheme from "../useTheme";

export default {
title: "Contextual Menu",
Expand Down Expand Up @@ -76,7 +78,7 @@ const itemsWithIcon = [
{
label: "Item 1",
icon: (
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24" fill="currentColor">
<path d="M200-120v-640q0-33 23.5-56.5T280-840h400q33 0 56.5 23.5T760-760v640L480-240 200-120Zm80-122 200-86 200 86v-518H280v518Zm0-518h400-400Z" />
</svg>
),
Expand Down Expand Up @@ -123,7 +125,7 @@ const itemsWithTruncatedText = [
label: "Item with a very long label that should be truncated",
badge: <DxcBadge color="green" label="New" />,
icon: (
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24" fill="currentColor">
<path d="M200-120v-640q0-33 23.5-56.5T280-840h400q33 0 56.5 23.5T760-760v640L480-240 200-120Zm80-122 200-86 200 86v-518H280v518Zm0-518h400-400Z" />
</svg>
),
Expand Down Expand Up @@ -185,39 +187,45 @@ export const Chromatic = () => (
</>
);

export const SingleItemStates = () => (
<DxcContainer width="300px">
<ContextualMenuContext.Provider value={{ selectedItemId: -1, setSelectedItemId: () => {} }}>
<Title title="Default" theme="light" level={3} />
<ExampleContainer>
<SingleItem {...items[0]} id={0} depthLevel={0} />
</ExampleContainer>
<Title title="Focus" theme="light" level={3} />
<ExampleContainer pseudoState="pseudo-focus">
<SingleItem {...items[0]} id={0} depthLevel={0} />
</ExampleContainer>
<Title title="Hover" theme="light" level={3} />
<ExampleContainer pseudoState="pseudo-hover">
<SingleItem {...items[0]} id={0} depthLevel={0} />
</ExampleContainer>
<Title title="Active" theme="light" level={3} />
<ExampleContainer pseudoState="pseudo-active">
<SingleItem {...items[0]} id={0} depthLevel={0} />
</ExampleContainer>
</ContextualMenuContext.Provider>
<ContextualMenuContext.Provider value={{ selectedItemId: 0, setSelectedItemId: () => {} }}>
<Title title="Selected" theme="light" level={3} />
<ExampleContainer>
<SingleItem {...items[0]} id={0} depthLevel={0} />
</ExampleContainer>
<Title title="Selected hover" theme="light" level={3} />
<ExampleContainer pseudoState="pseudo-hover">
<SingleItem {...items[0]} id={0} depthLevel={0} />
</ExampleContainer>
<Title title="Selected active" theme="light" level={3} />
<ExampleContainer pseudoState="pseudo-active">
<SingleItem {...items[0]} id={0} depthLevel={0} />
</ExampleContainer>
</ContextualMenuContext.Provider>
</DxcContainer>
);
export const SingleItemStates = () => {
const colorsTheme = useTheme();

return (
<ThemeProvider theme={colorsTheme.contextualMenu}>
<DxcContainer width="300px">
<ContextualMenuContext.Provider value={{ selectedItemId: -1, setSelectedItemId: () => {} }}>
<Title title="Default" theme="light" level={3} />
<ExampleContainer>
<SingleItem {...items[0]} id={0} depthLevel={0} />
</ExampleContainer>
<Title title="Focus" theme="light" level={3} />
<ExampleContainer pseudoState="pseudo-focus">
<SingleItem {...items[0]} id={0} depthLevel={0} />
</ExampleContainer>
<Title title="Hover" theme="light" level={3} />
<ExampleContainer pseudoState="pseudo-hover">
<SingleItem {...items[0]} id={0} depthLevel={0} />
</ExampleContainer>
<Title title="Active" theme="light" level={3} />
<ExampleContainer pseudoState="pseudo-active">
<SingleItem {...items[0]} id={0} depthLevel={0} />
</ExampleContainer>
</ContextualMenuContext.Provider>
<ContextualMenuContext.Provider value={{ selectedItemId: 0, setSelectedItemId: () => {} }}>
<Title title="Selected" theme="light" level={3} />
<ExampleContainer>
<SingleItem {...items[0]} id={0} depthLevel={0} />
</ExampleContainer>
<Title title="Selected hover" theme="light" level={3} />
<ExampleContainer pseudoState="pseudo-hover">
<SingleItem {...items[0]} id={0} depthLevel={0} />
</ExampleContainer>
<Title title="Selected active" theme="light" level={3} />
<ExampleContainer pseudoState="pseudo-active">
<SingleItem {...items[0]} id={0} depthLevel={0} />
</ExampleContainer>
</ContextualMenuContext.Provider>
</DxcContainer>
</ThemeProvider>
);
};
47 changes: 26 additions & 21 deletions lib/src/contextual-menu/ContextualMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { Fragment, createContext, useLayoutEffect, useMemo, useRef, useState } from "react";
import styled from "styled-components";
import styled, { ThemeProvider } from "styled-components";
import CoreTokens from "../common/coreTokens";
import ContextualMenuPropsType, {
ContextualMenuContextProps,
Expand All @@ -13,6 +13,7 @@ import ContextualMenuPropsType, {
import DxcDivider from "../divider/Divider";
import DxcInset from "../inset/Inset";
import MenuItem from "./MenuItem";
import useTheme from "../useTheme";

export const ContextualMenuContext = createContext<ContextualMenuContextProps | null>(null);

Expand All @@ -26,8 +27,8 @@ const addIdToItems = (items: ContextualMenuPropsType["items"]): (ItemWithId | Gr
isSection(item)
? { ...item, items: innerAddIdToItems(item.items) }
: isGroupItem(item)
? { ...item, items: innerAddIdToItems(item.items) }
: { ...item, id: accId++ }
? { ...item, items: innerAddIdToItems(item.items) }
: { ...item, id: accId++ },
);
};
return innerAddIdToItems(items);
Expand All @@ -37,6 +38,7 @@ const DxcContextualMenu = ({ items }: ContextualMenuPropsType) => {
const [selectedItemId, setSelectedItemId] = useState(-1);
const contextualMenuRef = useRef(null);
const itemsWithId = useMemo(() => addIdToItems(items), [items]);
const colorsTheme = useTheme();

const renderSection = (section: SectionWithId, currentSectionIndex: number, length: number) => (
<Fragment key={`section-${currentSectionIndex}`}>
Expand Down Expand Up @@ -67,31 +69,33 @@ const DxcContextualMenu = ({ items }: ContextualMenuPropsType) => {
}, [firstUpdate, selectedItemId]);

return (
<ContextualMenu role="menu" ref={contextualMenuRef}>
<ContextualMenuContext.Provider value={{ selectedItemId, setSelectedItemId }}>
{itemsWithId.map((item: GroupItemWithId | ItemWithId | SectionWithId, index: number) =>
"items" in item && !("label" in item) ? (
renderSection(item, index, itemsWithId.length)
) : (
<MenuItem item={item} key={`${item.label}-${index}`} />
)
)}
</ContextualMenuContext.Provider>
</ContextualMenu>
<ThemeProvider theme={colorsTheme.contextualMenu}>
<ContextualMenu role="menu" ref={contextualMenuRef}>
<ContextualMenuContext.Provider value={{ selectedItemId, setSelectedItemId }}>
{itemsWithId.map((item: GroupItemWithId | ItemWithId | SectionWithId, index: number) =>
"items" in item && !("label" in item) ? (
renderSection(item, index, itemsWithId.length)
) : (
<MenuItem item={item} key={`${item.label}-${index}`} />
),
)}
</ContextualMenuContext.Provider>
</ContextualMenu>
</ThemeProvider>
);
};

const ContextualMenu = styled.ul`
box-sizing: border-box;
margin: 0;
border: 1px solid ${CoreTokens.color_grey_200};
border: 1px solid ${({ theme }) => theme.borderColor};
border-radius: 0.25rem;
padding: ${CoreTokens.spacing_16} ${CoreTokens.spacing_8};
display: grid;
gap: ${CoreTokens.spacing_4};
min-width: 248px;
max-height: 100%;
background-color: ${CoreTokens.color_white};
background-color: ${({ theme }) => theme.backgroundColor};
overflow-y: auto;
&::-webkit-scrollbar {
width: 8px;
Expand All @@ -118,11 +122,12 @@ const SectionList = styled.ul`
const Title = styled.h2`
margin: 0 0 ${CoreTokens.spacing_4} 0;
padding: ${CoreTokens.spacing_4};
color: ${CoreTokens.color_grey_900};
font-family: ${CoreTokens.type_sans};
font-size: ${CoreTokens.type_scale_03};
font-weight: ${CoreTokens.type_semibold};
line-height: 24px;
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 DxcContextualMenu;
44 changes: 24 additions & 20 deletions lib/src/contextual-menu/ItemAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ const ItemAction = ({ badge, collapseIcon, icon, label, depthLevel, ...props }:
return (
<Action depthLevel={depthLevel} {...props}>
<Label>
{collapseIcon}
{collapseIcon && <Icon>{collapseIcon}</Icon>}
{icon && depthLevel === 0 && <Icon>{typeof icon === "string" ? <DxcIcon icon={icon} /> : icon}</Icon>}
<Text
selected={props.selected}
onMouseEnter={(event: React.MouseEvent<HTMLSpanElement>) => {
const text = event.currentTarget;
if (text.title === "" && text.scrollWidth > text.clientWidth) text.title = label;
Expand All @@ -39,24 +40,24 @@ const Action = styled.button<{ depthLevel: ItemActionProps["depthLevel"]; select
align-items: center;
justify-content: space-between;
gap: ${CoreTokens.spacing_16};
${(props) =>
props.selected
? `background-color: ${CoreTokens.color_purple_100}; font-weight: ${CoreTokens.type_semibold};`
${({ selected, theme }) =>
selected
? `background-color: ${theme.selectedMenuItemBackgroundColor};`
: `background-color: ${CoreTokens.color_transparent}`};
cursor: pointer;
overflow: hidden;

&:hover {
${(props) =>
props.selected
? `background-color: ${CoreTokens.color_purple_200};`
: `background-color: ${CoreTokens.color_grey_100};`};
${({ selected, theme }) =>
selected
? `background-color: ${theme.hoverSelectedMenuItemBackgroundColor};`
: `background-color: ${theme.hoverMenuItemBackgroundColor};`};
}
&:active {
${(props) =>
props.selected
? `background-color: ${CoreTokens.color_purple_200};`
: `background-color: ${CoreTokens.color_grey_100};`};
${({ selected, theme }) =>
selected
? `background-color: ${theme.activeSelectedMenuItemBackgroundColor};`
: `background-color: ${theme.activeMenuItemBackgroundColor};`};
}
&:focus {
outline: 2px solid ${CoreTokens.color_blue_600};
Expand All @@ -66,11 +67,12 @@ const Action = styled.button<{ depthLevel: ItemActionProps["depthLevel"]; select

const Icon = styled.span`
display: flex;
font-size: 16px;
font-size: ${({ theme }) => theme.iconSize};
color: ${({ theme }) => theme.iconColor};

svg {
height: 16px;
width: 16px;
height: ${({ theme }) => theme.iconSize};
width: ${({ theme }) => theme.iconSize};
}
`;

Expand All @@ -81,11 +83,13 @@ const Label = styled.span`
overflow: hidden;
`;

const Text = styled.span`
color: ${CoreTokens.color_grey_900};
font-family: ${CoreTokens.type_sans};
font-size: ${CoreTokens.type_scale_02};
line-height: 24px;
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};
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
Expand Down
Loading