Skip to content

Commit cdc1cb0

Browse files
authored
Merge pull request #7244 from Sage/FE-7071/submenu-horizontal-overflow
feat(menu-item): add submenuMinWidth prop
2 parents a66eb9d + 28c5376 commit cdc1cb0

File tree

7 files changed

+87
-84
lines changed

7 files changed

+87
-84
lines changed

src/components/menu/__internal__/submenu/submenu.component.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ export interface SubmenuProps {
7373
ariaLabel?: string;
7474
/** Sets the max-width of the submenu container element */
7575
submenuMaxWidth?: string;
76+
/** Sets the min-width of the submenu container element */
77+
submenuMinWidth?: string;
7678
}
7779

7880
const Submenu = React.forwardRef<HTMLAnchorElement, SubmenuProps>(
@@ -94,6 +96,7 @@ const Submenu = React.forwardRef<HTMLAnchorElement, SubmenuProps>(
9496
onSubmenuClose,
9597
onClick,
9698
submenuMaxWidth,
99+
submenuMinWidth,
97100
...rest
98101
}: SubmenuProps,
99102
ref,
@@ -528,6 +531,7 @@ const Submenu = React.forwardRef<HTMLAnchorElement, SubmenuProps>(
528531
applyFocusRadiusStyling={applyFocusRadius}
529532
applyFocusRadiusStylingToLastItem={applyFocusRadiusToLastItem}
530533
submenuMaxWidth={submenuMaxWidth}
534+
submenuMinWidth={submenuMinWidth}
531535
onBlur={handleSubmenuBlur}
532536
>
533537
<SubmenuContext.Provider

src/components/menu/__internal__/submenu/submenu.style.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ interface StyledSubmenuWrapperProps extends SharedStyleProps {
2323

2424
interface StyledSubmenuProps
2525
extends SharedStyleProps,
26-
Pick<SubmenuProps, "variant" | "submenuMaxWidth"> {
26+
Pick<SubmenuProps, "variant" | "submenuMaxWidth" | "submenuMinWidth"> {
2727
submenuDirection?: string;
2828
maxHeight?: string;
2929
applyFocusRadiusStyling: boolean;
@@ -73,6 +73,7 @@ const StyledSubmenu = styled.ul<StyledSubmenuProps>`
7373
applyFocusRadiusStyling,
7474
applyFocusRadiusStylingToLastItem,
7575
submenuMaxWidth,
76+
submenuMinWidth,
7677
}) => css`
7778
${!inFullscreenView &&
7879
menuType &&
@@ -84,7 +85,7 @@ const StyledSubmenu = styled.ul<StyledSubmenuProps>`
8485
? menuConfigVariants[menuType].submenuItemBackground
8586
: menuConfigVariants[menuType].background};
8687
87-
min-width: 100%;
88+
min-width: ${submenuMinWidth ?? "100%"};
8889
8990
${submenuMaxWidth &&
9091
css`

src/components/menu/__internal__/submenu/submenu.test.tsx

+17
Original file line numberDiff line numberDiff line change
@@ -513,3 +513,20 @@ test("should override submenu children's `maxWidth` if `submenuMaxWidth` is set"
513513
expect(submenuChildren[0]).toHaveStyle({ maxWidth: "300px" });
514514
expect(submenuChildren[1]).toHaveStyle({ maxWidth: "300px" });
515515
});
516+
517+
test("sets minimum width for submenu when `submenuMinWidth` is set", async () => {
518+
const user = userEvent.setup();
519+
render(
520+
<MenuContext.Provider value={menuContextValues}>
521+
<Submenu title="Fruits" submenuMinWidth="300px">
522+
<MenuItem href="#">Apple</MenuItem>
523+
<MenuItem href="#">Banana</MenuItem>
524+
</Submenu>
525+
</MenuContext.Provider>,
526+
);
527+
const menuItem = screen.getByRole("button", { name: "Fruits" });
528+
await user.hover(menuItem);
529+
const submenu = screen.getByRole("list");
530+
531+
expect(submenu).toHaveStyle({ minWidth: "300px" });
532+
});

src/components/menu/menu-item/menu-item.component.tsx

+14-1
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,19 @@ interface MenuItemBaseProps
8282
* Renders MenuItem as a div element
8383
* */
8484
as?: "div";
85-
/** Sets the max-width of the submenu container element, accepts any valid CSS string */
85+
/**
86+
* Sets the maximum width for the item's submenu when it is opened.
87+
* This prop is only applicable if the item has a submenu.
88+
* Overrides the maximum width of any items within the submenu.
89+
* Accepts any valid CSS width value (e.g. "200px", "50%").
90+
* */
8691
submenuMaxWidth?: string;
92+
/**
93+
* Sets a minimum width for the item's submenu when it is opened.
94+
* Accepts any valid CSS width value (e.g. "200px", "50%").
95+
* This prop is only applicable if the item has a submenu.
96+
* */
97+
submenuMinWidth?: string;
8798
}
8899

89100
export interface MenuWithChildren extends MenuItemBaseProps {
@@ -101,6 +112,7 @@ export interface MenuWithIcon extends MenuItemBaseProps {
101112
export const MenuItem = ({
102113
submenu,
103114
submenuMaxWidth,
115+
submenuMinWidth,
104116
children,
105117
href,
106118
onClick,
@@ -279,6 +291,7 @@ export const MenuItem = ({
279291
onSubmenuOpen={onSubmenuOpen}
280292
onSubmenuClose={onSubmenuClose}
281293
submenuMaxWidth={submenuMaxWidth}
294+
submenuMinWidth={submenuMinWidth}
282295
{...elementProps}
283296
variant={variant}
284297
{...rest}

src/components/menu/menu-test.stories.tsx

-16
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,6 @@ const defaultOpenState = isChromatic();
2626

2727
const meta: Meta<typeof Menu> = {
2828
title: "Menu/Test",
29-
includeStories: [
30-
"MenuFullScreenStory",
31-
"LongLabelsStory",
32-
"InGlobalHeaderStory",
33-
"InNavigationBarStory",
34-
"MenuFullScreenKeysTest",
35-
"MenuWithTwoSegments",
36-
"MenuFullScreenWithLargeMenuItems",
37-
"MenuComponentFullScreenWithLongSubmenuText",
38-
"AsLinkWithAlternateVariant",
39-
"MenuWithSubmenuCustomPadding",
40-
"WhenMenuItemsWrap",
41-
"MenuFullScreenWithMaxWidth",
42-
"IconAlignment",
43-
"TabbingOrder",
44-
],
4529
parameters: {
4630
info: { disable: true },
4731
chromatic: {

src/components/menu/menu.mdx

+6-4
Original file line numberDiff line numberDiff line change
@@ -133,12 +133,14 @@ A title attribute is added to the item when using this prop, containing the full
133133

134134
<Canvas of={MenuStories.TruncatedTitlesStory} />
135135

136-
### Submenu maxWidth
136+
### Controlling the submenu width
137137

138-
By default the submenu will have the same width as the widest `MenuItem`. This behaviour can be overridden
139-
by setting the `submenuMaxWidth` prop on the submenu's parent `MenuItem` component.
138+
By default, the submenu will have the same width as the widest `MenuItem`. This can be changed
139+
by setting the `submenuMaxWidth` or `submenuMinWidth` prop on the submenu's parent `MenuItem` component.
140140

141-
<Canvas of={MenuStories.TruncationAndSubmenuWidth} />
141+
Setting `submenuMaxWidth` will override the `maxWidth` prop of any `MenuItem` in the submenu. Overflowing submenu items will wrap instead of truncating.
142+
143+
<Canvas of={MenuStories.ControllingTheSubmenuWidth} />
142144

143145
### Responsive composition
144146

src/components/menu/menu.stories.tsx

+43-61
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,11 @@ const meta: Meta<typeof Menu> = {
5959
};
6060

6161
export default meta;
62-
type Story = StoryObj<typeof Menu>;
62+
type MenuStory = StoryObj<typeof Menu>;
6363

6464
const menuTypes: MenuType[] = ["white", "light", "dark", "black"];
6565

66-
export const DefaultStory: Story = () => {
66+
export const DefaultStory: MenuStory = () => {
6767
return (
6868
<Box mb={150}>
6969
{menuTypes.map((menuType) => (
@@ -102,7 +102,7 @@ export const DefaultStory: Story = () => {
102102
DefaultStory.storyName = "Default";
103103
DefaultStory.parameters = { chromatic: { disableSnapshot: true } };
104104

105-
export const SelectedStory: Story = () => {
105+
export const SelectedStory: MenuStory = () => {
106106
return (
107107
<Box mb={150}>
108108
{menuTypes.map((menuType) => (
@@ -127,7 +127,7 @@ export const SelectedStory: Story = () => {
127127
};
128128
SelectedStory.storyName = "Selected";
129129

130-
export const DividerStory: Story = () => {
130+
export const DividerStory: MenuStory = () => {
131131
return (
132132
<Box mb={150}>
133133
{menuTypes.map((menuType) => (
@@ -153,7 +153,7 @@ export const DividerStory: Story = () => {
153153
DividerStory.storyName = "Divider";
154154
DividerStory.parameters = { chromatic: { disableSnapshot: true } };
155155

156-
export const LargeDividerStory: Story = () => {
156+
export const LargeDividerStory: MenuStory = () => {
157157
return (
158158
<Box mb={150}>
159159
{menuTypes.map((menuType) => (
@@ -179,7 +179,7 @@ export const LargeDividerStory: Story = () => {
179179
LargeDividerStory.storyName = "Large Divider";
180180
LargeDividerStory.parameters = { chromatic: { disableSnapshot: true } };
181181

182-
export const SegmentTitleStory: Story = () => {
182+
export const SegmentTitleStory: MenuStory = () => {
183183
return (
184184
<Box mb={150}>
185185
{menuTypes.map((menuType) => (
@@ -206,7 +206,7 @@ export const SegmentTitleStory: Story = () => {
206206
SegmentTitleStory.storyName = "Segment Title";
207207
SegmentTitleStory.parameters = { chromatic: { disableSnapshot: true } };
208208

209-
export const AlternateColourStory: Story = () => {
209+
export const AlternateColourStory: MenuStory = () => {
210210
return (
211211
<Box mb={150}>
212212
{menuTypes.map((menuType) => (
@@ -238,7 +238,7 @@ export const AlternateColourStory: Story = () => {
238238
AlternateColourStory.storyName = "Alternate Colour";
239239
AlternateColourStory.parameters = { chromatic: { disableSnapshot: true } };
240240

241-
export const SubmenuOptionsStory: Story = () => {
241+
export const SubmenuOptionsStory: MenuStory = () => {
242242
return (
243243
<Box mb={150}>
244244
{menuTypes.map((menuType) => (
@@ -275,7 +275,7 @@ export const SubmenuOptionsStory: Story = () => {
275275
SubmenuOptionsStory.storyName = "Submenu Options";
276276
SubmenuOptionsStory.parameters = { chromatic: { disableSnapshot: true } };
277277

278-
export const SubmenuDirectionLeftStory: Story = () => {
278+
export const SubmenuDirectionLeftStory: MenuStory = () => {
279279
return (
280280
<Box mb={150}>
281281
<Menu>
@@ -297,7 +297,7 @@ export const SubmenuDirectionLeftStory: Story = () => {
297297
SubmenuDirectionLeftStory.storyName = "Submenu Direction Left";
298298
SubmenuDirectionLeftStory.parameters = { chromatic: { disableSnapshot: true } };
299299

300-
export const WithIconStory: Story = () => {
300+
export const WithIconStory: MenuStory = () => {
301301
return (
302302
<Box mb={150}>
303303
{menuTypes.map((menuType) => (
@@ -330,7 +330,7 @@ export const WithIconStory: Story = () => {
330330
WithIconStory.storyName = "With Icon";
331331
WithIconStory.parameters = { chromatic: { disableSnapshot: true } };
332332

333-
export const NoDropdownArrowOnSubmenuStory: Story = () => {
333+
export const NoDropdownArrowOnSubmenuStory: MenuStory = () => {
334334
return (
335335
<Box minHeight="150px">
336336
<Menu>
@@ -349,7 +349,7 @@ NoDropdownArrowOnSubmenuStory.parameters = {
349349
chromatic: { disableSnapshot: true },
350350
};
351351

352-
export const SplitSubmenuIntoSeparateComponentStory: Story = () => {
352+
export const SplitSubmenuIntoSeparateComponentStory: MenuStory = () => {
353353
const MySubMenu = (
354354
<MenuItem submenu="Menu Item Three">
355355
<MenuItem href="#">Item Submenu One</MenuItem>
@@ -372,7 +372,7 @@ SplitSubmenuIntoSeparateComponentStory.parameters = {
372372
chromatic: { disableSnapshot: true },
373373
};
374374

375-
export const SubmenuIconAndTextAlignment: Story = () => {
375+
export const SubmenuIconAndTextAlignment: MenuStory = () => {
376376
return (
377377
<Box minHeight="250px">
378378
<Menu menuType="dark">
@@ -400,7 +400,7 @@ SubmenuIconAndTextAlignment.parameters = {
400400
chromatic: { disableSnapshot: true },
401401
};
402402

403-
export const ScrollableSubmenuStory: Story = () => {
403+
export const ScrollableSubmenuStory: MenuStory = () => {
404404
return (
405405
<Box mb={150}>
406406
{menuTypes.map((menuType) => (
@@ -452,7 +452,7 @@ export const ScrollableSubmenuStory: Story = () => {
452452
ScrollableSubmenuStory.storyName = "Scrollable Submenu";
453453
ScrollableSubmenuStory.parameters = { chromatic: { disableSnapshot: true } };
454454

455-
export const ScrollableSubmenuWithParent: Story = () => {
455+
export const ScrollableSubmenuWithParent: MenuStory = () => {
456456
const items = [
457457
"apple",
458458
"banana",
@@ -510,7 +510,7 @@ ScrollableSubmenuWithParent.parameters = {
510510
chromatic: { disableSnapshot: true },
511511
};
512512

513-
export const SubmenuWithSearch: Story = () => {
513+
export const SubmenuWithSearch: MenuStory = () => {
514514
return (
515515
<Box mb={150}>
516516
{menuTypes.map((menuType) => (
@@ -547,7 +547,7 @@ export const SubmenuWithSearch: Story = () => {
547547
SubmenuWithSearch.storyName = "Submenu with Search";
548548
SubmenuWithSearch.parameters = { chromatic: { disableSnapshot: true } };
549549

550-
export const TruncatedTitlesStory: Story = () => {
550+
export const TruncatedTitlesStory: MenuStory = () => {
551551
return (
552552
<Box minHeight="150px">
553553
<Menu>
@@ -568,7 +568,7 @@ export const TruncatedTitlesStory: Story = () => {
568568
};
569569
TruncatedTitlesStory.storyName = "Truncated Titles";
570570

571-
export const ResponsiveCompositionStory: Story = () => {
571+
export const ResponsiveCompositionStory: MenuStory = () => {
572572
const isBelowBreakpoint1 = useMediaQuery("(max-width: 1200px)");
573573
const isBelowBreakpoint2 = useMediaQuery("(max-width: 1000px)");
574574
const isBelowBreakpoint3 = useMediaQuery("(max-width: 800px)");
@@ -652,7 +652,7 @@ ResponsiveCompositionStory.parameters = {
652652
chromatic: { disableSnapshot: true },
653653
};
654654

655-
export const FullscreenViewStory: Story = () => {
655+
export const FullscreenViewStory: MenuStory = () => {
656656
const [menuOpen, setMenuOpen] = useState({
657657
light: false,
658658
dark: false,
@@ -737,50 +737,32 @@ export const FullscreenViewStory: Story = () => {
737737
FullscreenViewStory.storyName = "Fullscreen View";
738738
FullscreenViewStory.parameters = { chromatic: { disableSnapshot: true } };
739739

740-
export const TruncationAndSubmenuWidth: Story = () => {
741-
return (
742-
<Box mb={150}>
743-
{menuTypes.map((menuType) => (
744-
<Box key={menuType}>
745-
<Typography variant="h4" textTransform="capitalize" my={2}>
746-
{menuType}
747-
</Typography>
748-
<Menu menuType={menuType}>
749-
<MenuItem
750-
maxWidth="240px"
751-
submenuMaxWidth="300px"
752-
submenu="This is a very long menu item title"
753-
>
754-
<MenuItem href="#">Item One</MenuItem>
755-
<MenuItem p="2px 16px">
756-
<Box minWidth="268px">
757-
<Search
758-
placeholder="placeholder"
759-
variant={
760-
["white", "light"].includes(menuType) ? "default" : "dark"
761-
}
762-
defaultValue=""
763-
/>
764-
</Box>
765-
</MenuItem>
766-
<MenuSegmentTitle text="segment title that should wrap when it will overflow">
767-
<MenuItem href="#">Item Two</MenuItem>
768-
<MenuItem href="#">
769-
This is a longer text string that will wrap when it will
770-
overflow the width of the submenu container
771-
</MenuItem>
772-
</MenuSegmentTitle>
773-
</MenuItem>
774-
</Menu>
775-
</Box>
776-
))}
777-
</Box>
778-
);
740+
export const ControllingTheSubmenuWidth: MenuStory = {
741+
render: () => (
742+
<Menu menuType="black">
743+
<MenuItem submenuMaxWidth="300px" submenu="Open submenu with max width">
744+
<MenuItem href="#">Item One</MenuItem>
745+
<MenuItem href="#">
746+
This is a longer text string. I will wrap instead of truncating!
747+
</MenuItem>
748+
</MenuItem>
749+
<MenuItem submenuMinWidth="300px" submenu="Open submenu with min width">
750+
<MenuItem href="#">Item One</MenuItem>
751+
<MenuItem href="#">Item Two</MenuItem>
752+
<MenuItem href="#">Item Three</MenuItem>
753+
</MenuItem>
754+
</Menu>
755+
),
756+
decorators: [
757+
(Story) => (
758+
<div style={{ minHeight: "250px" }}>
759+
<Story />
760+
</div>
761+
),
762+
],
763+
parameters: { chromatic: { disableSnapshot: true } },
779764
};
780765

781-
TruncationAndSubmenuWidth.storyName = "Truncation and Submenu Width";
782-
TruncationAndSubmenuWidth.parameters = { chromatic: { disableSnapshot: true } };
783-
784766
export const MenuFullscreenWithSegmentStyling = () => {
785767
const [isOpen, setIsOpen] = useState(false);
786768
const [isSegmentedOpen, setIsSegmentedOpen] = useState(false);

0 commit comments

Comments
 (0)