@box/blueprint-web 9.4.1 → 9.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. package/lib-esm/index.css +54 -10
  2. package/lib-esm/primitives/chips/filter-chip/filter-combo-chip.d.ts +2 -0
  3. package/lib-esm/primitives/chips/filter-chip/index.d.ts +2 -0
  4. package/lib-esm/primitives/dropdown-menu/FullScreenMenuContext.d.ts +16 -0
  5. package/lib-esm/primitives/dropdown-menu/FullScreenMenuContext.js +36 -0
  6. package/lib-esm/primitives/dropdown-menu/SubMenuContext.d.ts +13 -0
  7. package/lib-esm/primitives/dropdown-menu/SubMenuContext.js +27 -0
  8. package/lib-esm/primitives/dropdown-menu/dropdown-menu-content.d.ts +5 -0
  9. package/lib-esm/primitives/dropdown-menu/dropdown-menu-content.js +43 -3
  10. package/lib-esm/primitives/dropdown-menu/dropdown-menu-header.d.ts +15 -0
  11. package/lib-esm/primitives/dropdown-menu/dropdown-menu-header.js +93 -0
  12. package/lib-esm/primitives/dropdown-menu/dropdown-menu-item.js +4 -0
  13. package/lib-esm/primitives/dropdown-menu/dropdown-menu-root.d.ts +3 -1
  14. package/lib-esm/primitives/dropdown-menu/dropdown-menu-root.js +21 -3
  15. package/lib-esm/primitives/dropdown-menu/dropdown-menu-sub-menu-content.js +30 -2
  16. package/lib-esm/primitives/dropdown-menu/dropdown-menu-sub-menu-root.d.ts +6 -0
  17. package/lib-esm/primitives/dropdown-menu/dropdown-menu-sub-menu-root.js +32 -4
  18. package/lib-esm/primitives/dropdown-menu/dropdown-menu-sub-menu-trigger.js +22 -2
  19. package/lib-esm/primitives/dropdown-menu/dropdown-menu.module.js +1 -1
  20. package/lib-esm/primitives/dropdown-menu/index.d.ts +8 -0
  21. package/lib-esm/primitives/dropdown-menu/index.js +3 -1
  22. package/package.json +2 -2
package/lib-esm/index.css CHANGED
@@ -4036,7 +4036,7 @@ table.bp_inline_table_module_inlineTable--b023b tr:not(:last-child) td{
4036
4036
  }
4037
4037
  }
4038
4038
 
4039
- .bp_dropdown_menu_module_content--291e5{
4039
+ .bp_dropdown_menu_module_content--00a5d{
4040
4040
  background-color:var(--surface-menu-surface);
4041
4041
  border:var(--border-1) solid var(--border-card-border);
4042
4042
  border-radius:var(--radius-3);
@@ -4057,14 +4057,31 @@ table.bp_inline_table_module_inlineTable--b023b tr:not(:last-child) td{
4057
4057
  text-transform:none;
4058
4058
  z-index:2147483647;
4059
4059
  }
4060
+ .bp_dropdown_menu_module_content--00a5d[data-menu-fullscreen=true]{
4061
+ border:unset;
4062
+ border-radius:unset;
4063
+ display:flex;
4064
+ flex-direction:column;
4065
+ height:100vh;
4066
+ max-height:none;
4067
+ max-width:none;
4068
+ overflow:hidden;
4069
+ padding:unset;
4070
+ position:relative;
4071
+ width:100vw;
4072
+ }
4073
+ .bp_dropdown_menu_module_content--00a5d[data-menu-fullscreen=true] .bp_dropdown_menu_module_fullScreenContent--00a5d{
4074
+ overflow-y:auto;
4075
+ padding:var(--space-3);
4076
+ }
4060
4077
 
4061
- .bp_dropdown_menu_module_ellipsis--291e5{
4078
+ .bp_dropdown_menu_module_ellipsis--00a5d{
4062
4079
  overflow:hidden;
4063
4080
  text-overflow:ellipsis;
4064
4081
  white-space:nowrap;
4065
4082
  }
4066
4083
 
4067
- .bp_dropdown_menu_module_checkmark--291e5{
4084
+ .bp_dropdown_menu_module_checkmark--00a5d{
4068
4085
  align-items:center;
4069
4086
  display:inline-flex;
4070
4087
  justify-content:center;
@@ -4072,36 +4089,63 @@ table.bp_inline_table_module_inlineTable--b023b tr:not(:last-child) td{
4072
4089
  position:absolute;
4073
4090
  }
4074
4091
 
4075
- .bp_dropdown_menu_module_item--291e5,.bp_dropdown_menu_module_subMenuTrigger--291e5{
4092
+ .bp_dropdown_menu_module_item--00a5d,.bp_dropdown_menu_module_subMenuTrigger--00a5d{
4076
4093
  align-items:center;
4077
4094
  border:var(--border-2) solid #0000;
4078
4095
  border-radius:var(--radius-3);
4079
4096
  cursor:pointer;
4080
4097
  display:flex;
4081
4098
  outline:none;
4082
- padding:var(--border-8);
4099
+ padding:calc(var(--border-8) - var(--border-2));
4083
4100
  position:relative;
4084
4101
  -webkit-user-select:none;
4085
4102
  user-select:none;
4086
4103
  }
4087
- .bp_dropdown_menu_module_item--291e5[data-disabled],.bp_dropdown_menu_module_subMenuTrigger--291e5[data-disabled]{
4104
+ .bp_dropdown_menu_module_item--00a5d[data-disabled],.bp_dropdown_menu_module_subMenuTrigger--00a5d[data-disabled]{
4088
4105
  opacity:60%;
4089
4106
  pointer-events:none;
4090
4107
  }
4091
- .bp_dropdown_menu_module_item--291e5[data-highlighted],.bp_dropdown_menu_module_subMenuTrigger--291e5[data-highlighted]{
4108
+ .bp_dropdown_menu_module_item--00a5d[data-highlighted],.bp_dropdown_menu_module_subMenuTrigger--00a5d[data-highlighted]{
4092
4109
  background-color:var(--surface-menu-surface-hover);
4093
4110
  border:var(--border-2) solid var(--outline-focus-on-light);
4094
4111
  }
4095
- .bp_dropdown_menu_module_item--291e5:hover,.bp_dropdown_menu_module_item--291e5[data-state=open],.bp_dropdown_menu_module_subMenuTrigger--291e5:hover,.bp_dropdown_menu_module_subMenuTrigger--291e5[data-state=open]{
4112
+ .bp_dropdown_menu_module_item--00a5d:hover,.bp_dropdown_menu_module_item--00a5d[data-state=open],.bp_dropdown_menu_module_subMenuTrigger--00a5d:hover,.bp_dropdown_menu_module_subMenuTrigger--00a5d[data-state=open]{
4096
4113
  background-color:var(--surface-menu-surface-hover);
4097
4114
  }
4098
- .bp_dropdown_menu_module_item--291e5.bp_dropdown_menu_module_checkboxItem--291e5,.bp_dropdown_menu_module_item--291e5.bp_dropdown_menu_module_radioItem--291e5,.bp_dropdown_menu_module_subMenuTrigger--291e5.bp_dropdown_menu_module_checkboxItem--291e5,.bp_dropdown_menu_module_subMenuTrigger--291e5.bp_dropdown_menu_module_radioItem--291e5{
4115
+ .bp_dropdown_menu_module_item--00a5d.bp_dropdown_menu_module_checkboxItem--00a5d,.bp_dropdown_menu_module_item--00a5d.bp_dropdown_menu_module_radioItem--00a5d,.bp_dropdown_menu_module_subMenuTrigger--00a5d.bp_dropdown_menu_module_checkboxItem--00a5d,.bp_dropdown_menu_module_subMenuTrigger--00a5d.bp_dropdown_menu_module_radioItem--00a5d{
4099
4116
  padding:.4375rem .5rem .4375rem calc(var(--space-2) + var(--space-4) + var(--space-4));
4100
4117
  }
4101
4118
 
4102
- .bp_dropdown_menu_module_dropdownMenuItemSeparator--291e5{
4119
+ .bp_dropdown_menu_module_dropdownMenuItemSeparator--00a5d{
4103
4120
  margin-block:var(--space-2);
4104
4121
  }
4122
+ div[data-radix-popper-content-wrapper]:has([role=menu]):has([data-state=open]):has([data-menu-fullscreen=true]){
4123
+ height:100%;
4124
+ transform:none !important;
4125
+ width:100%;
4126
+ }
4127
+
4128
+ .bp_dropdown_menu_module_menuHeader--00a5d{
4129
+ align-items:center;
4130
+ box-shadow:var(--dropshadow-1);
4131
+ display:grid;
4132
+ gap:var(--space-2);
4133
+ grid-template-areas:"submenu-close content close";
4134
+ grid-template-columns:auto minmax(0, 1fr) auto;
4135
+ padding:var(--space-3);
4136
+ }
4137
+
4138
+ .bp_dropdown_menu_module_headerTextContent--00a5d{
4139
+ display:grid;
4140
+ }
4141
+
4142
+ .bp_dropdown_menu_module_submenuCloseButton--00a5d{
4143
+ grid-area:submenu-close;
4144
+ }
4145
+
4146
+ .bp_dropdown_menu_module_menuCloseButton--00a5d{
4147
+ grid-area:close;
4148
+ }
4105
4149
 
4106
4150
  .bp_menu_item_sections_module_menuItemMainContent--c557e{
4107
4151
  display:flex;
@@ -20,6 +20,8 @@ export type FilterComboChipProps = DropdownMenuRootProps & {
20
20
  children: React.ReactNode;
21
21
  };
22
22
  export declare const FilterComboChip: React.ForwardRefExoticComponent<import("@radix-ui/react-dropdown-menu").DropdownMenuProps & {
23
+ isFullScreenEnabled?: boolean;
24
+ } & {
23
25
  /** FilterChip.Chip component used as Dropdown menu trigger. Trigger chip value is ignored from selection */
24
26
  chip: React.ReactElement;
25
27
  /** List of FilterChip.ComboOption elements displayed in the dropdown menu */
@@ -7,6 +7,8 @@ export declare const FilterChip: {
7
7
  Status: import("react").ForwardRefExoticComponent<Omit<import("../types").ChipStatusProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
8
8
  DropdownIndicator: import("react").ForwardRefExoticComponent<import("react").RefAttributes<SVGSVGElement>>;
9
9
  ComboChip: import("react").ForwardRefExoticComponent<import("@radix-ui/react-dropdown-menu").DropdownMenuProps & {
10
+ isFullScreenEnabled?: boolean;
11
+ } & {
10
12
  chip: React.ReactElement;
11
13
  children: React.ReactNode;
12
14
  } & import("react").RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,16 @@
1
+ import React, { type ReactNode } from 'react';
2
+ interface FullScreenMenuContextProps {
3
+ isMenuFullScreenEnabled: boolean;
4
+ setIsMenuFullScreenEnabled: React.Dispatch<React.SetStateAction<boolean>>;
5
+ isMenuOpen: boolean;
6
+ setIsMenuOpen: React.Dispatch<React.SetStateAction<boolean | undefined>>;
7
+ }
8
+ interface FullScreenProviderProps {
9
+ children: ReactNode;
10
+ isFullScreenEnabled?: boolean;
11
+ isMenuOpen: boolean;
12
+ setIsMenuOpen: React.Dispatch<React.SetStateAction<boolean | undefined>>;
13
+ }
14
+ export declare const FullScreenMenuProvider: ({ children, isFullScreenEnabled, isMenuOpen, setIsMenuOpen, }: FullScreenProviderProps) => import("react/jsx-runtime").JSX.Element;
15
+ export declare const useFullScreenMenu: () => FullScreenMenuContextProps;
16
+ export {};
@@ -0,0 +1,36 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { useContext, createContext, useState, useEffect, useMemo } from 'react';
3
+
4
+ const FullScreenMenuContext = /*#__PURE__*/createContext(null);
5
+ const FullScreenMenuProvider = ({
6
+ children,
7
+ isFullScreenEnabled,
8
+ isMenuOpen,
9
+ setIsMenuOpen
10
+ }) => {
11
+ const [isMenuFullScreenEnabled, setIsMenuFullScreenEnabled] = useState(isFullScreenEnabled || false);
12
+ useEffect(() => {
13
+ if (isFullScreenEnabled !== undefined) {
14
+ setIsMenuFullScreenEnabled(isFullScreenEnabled);
15
+ }
16
+ }, [isFullScreenEnabled]);
17
+ const value = useMemo(() => ({
18
+ isMenuFullScreenEnabled,
19
+ setIsMenuFullScreenEnabled,
20
+ isMenuOpen,
21
+ setIsMenuOpen
22
+ }), [isMenuFullScreenEnabled, setIsMenuFullScreenEnabled, isMenuOpen, setIsMenuOpen]);
23
+ return jsx(FullScreenMenuContext.Provider, {
24
+ value: value,
25
+ children: children
26
+ });
27
+ };
28
+ const useFullScreenMenu = () => {
29
+ const context = useContext(FullScreenMenuContext);
30
+ if (context === null) {
31
+ throw new Error('useFullScreenMenu must be used within a DropdownMenu.Root');
32
+ }
33
+ return context;
34
+ };
35
+
36
+ export { FullScreenMenuProvider, useFullScreenMenu };
@@ -0,0 +1,13 @@
1
+ import React, { type ReactNode } from 'react';
2
+ interface SubMenuContextProps {
3
+ isSubMenuOpen: boolean;
4
+ setIsSubMenuOpen: React.Dispatch<React.SetStateAction<boolean | undefined>>;
5
+ }
6
+ interface SubMenuProviderProps {
7
+ children: ReactNode;
8
+ isSubMenuOpen: boolean;
9
+ setIsSubMenuOpen: React.Dispatch<React.SetStateAction<boolean | undefined>>;
10
+ }
11
+ export declare const SubMenuMenuProvider: ({ children, isSubMenuOpen, setIsSubMenuOpen }: SubMenuProviderProps) => import("react/jsx-runtime").JSX.Element;
12
+ export declare const useSubMenu: () => SubMenuContextProps;
13
+ export {};
@@ -0,0 +1,27 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { useContext, createContext, useMemo } from 'react';
3
+
4
+ const SubMenuContext = /*#__PURE__*/createContext(null);
5
+ const SubMenuMenuProvider = ({
6
+ children,
7
+ isSubMenuOpen,
8
+ setIsSubMenuOpen
9
+ }) => {
10
+ const value = useMemo(() => ({
11
+ isSubMenuOpen,
12
+ setIsSubMenuOpen
13
+ }), [isSubMenuOpen, setIsSubMenuOpen]);
14
+ return jsx(SubMenuContext.Provider, {
15
+ value: value,
16
+ children: children
17
+ });
18
+ };
19
+ const useSubMenu = () => {
20
+ const context = useContext(SubMenuContext);
21
+ if (context === null) {
22
+ throw new Error('useSubMenu must be used within a DropdownMenu.Root');
23
+ }
24
+ return context;
25
+ };
26
+
27
+ export { SubMenuMenuProvider, useSubMenu };
@@ -1,4 +1,9 @@
1
1
  import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
2
+ import { type ReactElement, type ReactNode } from 'react';
3
+ export declare const sortMenuChildren: (children: ReactNode) => {
4
+ Header: ReactElement | null;
5
+ OtherChildren: Array<ReactElement>;
6
+ };
2
7
  export interface DropdownMenuContentProps extends DropdownMenuPrimitive.DropdownMenuContentProps {
3
8
  /** Specify a container element to portal the content into. */
4
9
  /** @default document.body */
@@ -1,9 +1,27 @@
1
- import { jsx } from 'react/jsx-runtime';
1
+ import { jsx, jsxs } from 'react/jsx-runtime';
2
2
  import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
3
3
  import clsx from 'clsx';
4
- import { forwardRef } from 'react';
4
+ import { forwardRef, Children, isValidElement } from 'react';
5
+ import { DropdownMenuHeader } from './dropdown-menu-header.js';
5
6
  import styles from './dropdown-menu.module.js';
7
+ import { useFullScreenMenu } from './FullScreenMenuContext.js';
6
8
 
9
+ const sortMenuChildren = children => {
10
+ return Children.toArray(children).reduce((acc, child) => {
11
+ if (! /*#__PURE__*/isValidElement(child)) {
12
+ return acc;
13
+ }
14
+ if (child.type === DropdownMenuHeader) {
15
+ acc.Header = child;
16
+ } else {
17
+ acc.OtherChildren.push(child);
18
+ }
19
+ return acc;
20
+ }, {
21
+ Header: null,
22
+ OtherChildren: []
23
+ });
24
+ };
7
25
  /**
8
26
  * Based on Radix-UI Content
9
27
  * @see https://www.radix-ui.com/docs/primitives/components/dropdown-menu#content
@@ -15,6 +33,28 @@ const DropdownMenuContent = /*#__PURE__*/forwardRef((props, forwardedRef) => {
15
33
  className,
16
34
  ...rest
17
35
  } = props;
36
+ const {
37
+ isMenuFullScreenEnabled
38
+ } = useFullScreenMenu();
39
+ if (isMenuFullScreenEnabled) {
40
+ const {
41
+ Header,
42
+ OtherChildren
43
+ } = sortMenuChildren(children);
44
+ return jsx(DropdownMenuPrimitive.Portal, {
45
+ container: container,
46
+ children: jsxs(DropdownMenuPrimitive.Content, {
47
+ ...rest,
48
+ ref: forwardedRef,
49
+ className: clsx(styles.content, className),
50
+ "data-menu-fullscreen": true,
51
+ children: [Header, jsx("div", {
52
+ className: styles.fullScreenContent,
53
+ children: OtherChildren
54
+ })]
55
+ })
56
+ });
57
+ }
18
58
  return (
19
59
  // TODO(DSYS-440): Unify all Portals (Tooltip, Select, DropdownMenu, etc...), Expose a Provider which would tell where to mount (body? other elem?)
20
60
  jsx(DropdownMenuPrimitive.Portal, {
@@ -32,4 +72,4 @@ const DropdownMenuContent = /*#__PURE__*/forwardRef((props, forwardedRef) => {
32
72
  });
33
73
  DropdownMenuContent.displayName = 'DropdownMenuContent';
34
74
 
35
- export { DropdownMenuContent };
75
+ export { DropdownMenuContent, sortMenuChildren };
@@ -0,0 +1,15 @@
1
+ import { type HTMLAttributes } from 'react';
2
+ import { type IconButtonProps } from '../icon-button';
3
+ export type MenuCloseButtonProps = Omit<IconButtonProps, 'icon' | 'variant' | 'size'>;
4
+ export type TextContentProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
5
+ title: string;
6
+ subtitle?: string;
7
+ };
8
+ export declare const DropdownMenuHeader: (({ children, className, ...rest }: HTMLAttributes<HTMLDivElement>) => import("react/jsx-runtime").JSX.Element) & {
9
+ MenuCloseButton: import("react").ForwardRefExoticComponent<Omit<MenuCloseButtonProps, "ref"> & import("react").RefAttributes<HTMLButtonElement>>;
10
+ SubMenuBackButton: import("react").ForwardRefExoticComponent<Omit<MenuCloseButtonProps, "ref"> & import("react").RefAttributes<HTMLButtonElement>>;
11
+ TextContent: import("react").ForwardRefExoticComponent<Omit<HTMLAttributes<HTMLDivElement>, "children"> & {
12
+ title: string;
13
+ subtitle?: string;
14
+ } & import("react").RefAttributes<HTMLDivElement>>;
15
+ };
@@ -0,0 +1,93 @@
1
+ import { jsx, jsxs } from 'react/jsx-runtime';
2
+ import { XMark, PointerChevronLeft } from '@box/blueprint-web-assets/icons/Fill';
3
+ import clsx from 'clsx';
4
+ import { forwardRef, useCallback } from 'react';
5
+ import { Text } from '../../text/text.js';
6
+ import { composeEventHandlers } from '../../utils/composeEventHandlers.js';
7
+ import { IconButton } from '../icon-button/icon-button.js';
8
+ import styles from './dropdown-menu.module.js';
9
+ import { useFullScreenMenu } from './FullScreenMenuContext.js';
10
+ import { useSubMenu } from './SubMenuContext.js';
11
+
12
+ const MenuCloseButton = /*#__PURE__*/forwardRef(({
13
+ onClick,
14
+ className,
15
+ ...rest
16
+ }, ref) => {
17
+ const {
18
+ setIsMenuOpen
19
+ } = useFullScreenMenu();
20
+ const handleCloseClick = useCallback(() => {
21
+ setIsMenuOpen(false);
22
+ }, [setIsMenuOpen]);
23
+ return jsx(IconButton, {
24
+ ref: ref,
25
+ ...rest,
26
+ className: clsx(styles.menuCloseButton, className),
27
+ icon: XMark,
28
+ onClick: composeEventHandlers(handleCloseClick, onClick),
29
+ variant: "small-utility"
30
+ });
31
+ });
32
+ const SubMenuBackButton = /*#__PURE__*/forwardRef(({
33
+ onClick,
34
+ className,
35
+ ...rest
36
+ }, ref) => {
37
+ const {
38
+ setIsSubMenuOpen
39
+ } = useSubMenu();
40
+ const handleCloseClick = useCallback(() => {
41
+ setIsSubMenuOpen(false);
42
+ }, [setIsSubMenuOpen]);
43
+ return jsx(IconButton, {
44
+ ref: ref,
45
+ ...rest,
46
+ className: clsx(styles.submenuCloseButton, className),
47
+ icon: PointerChevronLeft,
48
+ onClick: composeEventHandlers(handleCloseClick, onClick)
49
+ });
50
+ });
51
+ const MenuHeaderPrimitive = ({
52
+ children,
53
+ className,
54
+ ...rest
55
+ }) => {
56
+ return jsx("div", {
57
+ ...rest,
58
+ className: clsx(styles.menuHeader, className),
59
+ role: "presentation",
60
+ children: children
61
+ });
62
+ };
63
+ const TextContent = /*#__PURE__*/forwardRef(({
64
+ title,
65
+ subtitle,
66
+ className,
67
+ ...rest
68
+ }, ref) => {
69
+ return jsxs("div", {
70
+ ref: ref,
71
+ ...rest,
72
+ className: clsx(className, styles.headerTextContent),
73
+ children: [jsx(Text, {
74
+ as: "span",
75
+ className: styles.ellipsis,
76
+ variant: "bodyLargeBold",
77
+ children: title
78
+ }), subtitle && jsx(Text, {
79
+ as: "span",
80
+ className: styles.ellipsis,
81
+ color: "textOnLightSecondary",
82
+ variant: "caption",
83
+ children: subtitle
84
+ })]
85
+ });
86
+ });
87
+ const DropdownMenuHeader = Object.assign(MenuHeaderPrimitive, {
88
+ MenuCloseButton,
89
+ SubMenuBackButton,
90
+ TextContent
91
+ });
92
+
93
+ export { DropdownMenuHeader };
@@ -18,6 +18,10 @@ const DropdownMenuItem = /*#__PURE__*/forwardRef((props, forwardedRef) => {
18
18
  className: styles.item,
19
19
  onPointerLeave: preventDefault,
20
20
  onPointerMove: preventDefault,
21
+ // If click starts at trigger button, and ends on the item, it should not trigger the item.
22
+ // Note: this also has a side effect of not allowing to start click and end on a different time.
23
+ // TODO: see if possible to make https://github.com/radix-ui/primitives/blob/b32a93318cdfce383c2eec095710d35ffbd33a1c/packages/react/menu/src/Menu.tsx#L646
24
+ onPointerUp: preventDefault,
21
25
  children: jsx("span", {
22
26
  className: styles.ellipsis,
23
27
  children: children
@@ -1,5 +1,7 @@
1
1
  import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
2
- export type DropdownMenuRootProps = DropdownMenuPrimitive.DropdownMenuProps;
2
+ export type DropdownMenuRootProps = DropdownMenuPrimitive.DropdownMenuProps & {
3
+ isFullScreenEnabled?: boolean;
4
+ };
3
5
  /**
4
6
  * The Dropdown-menu component displays a menu to the user, such as a set of actions or functions—triggered by a button.
5
7
  */
@@ -1,5 +1,7 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
2
  import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
3
+ import { useControllableState } from '../../utils/useControllableState.js';
4
+ import { FullScreenMenuProvider } from './FullScreenMenuContext.js';
3
5
 
4
6
  /**
5
7
  * The Dropdown-menu component displays a menu to the user, such as a set of actions or functions—triggered by a button.
@@ -7,11 +9,27 @@ import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
7
9
  const DropdownMenuRoot = props => {
8
10
  const {
9
11
  children,
12
+ isFullScreenEnabled,
13
+ open,
14
+ onOpenChange,
15
+ defaultOpen,
10
16
  ...rest
11
17
  } = props;
12
- return jsx(DropdownMenuPrimitive.Root, {
13
- ...rest,
14
- children: children
18
+ const [isMenuOpen = false, setIsMenuOpen] = useControllableState({
19
+ prop: open,
20
+ onChange: onOpenChange,
21
+ defaultProp: defaultOpen
22
+ });
23
+ return jsx(FullScreenMenuProvider, {
24
+ isFullScreenEnabled: isFullScreenEnabled,
25
+ isMenuOpen: isMenuOpen,
26
+ setIsMenuOpen: setIsMenuOpen,
27
+ children: jsx(DropdownMenuPrimitive.Root, {
28
+ ...rest,
29
+ onOpenChange: setIsMenuOpen,
30
+ open: isMenuOpen,
31
+ children: children
32
+ })
15
33
  });
16
34
  };
17
35
  DropdownMenuRoot.displayName = 'DropdownMenuRoot';
@@ -1,8 +1,10 @@
1
- import { jsx } from 'react/jsx-runtime';
1
+ import { jsx, jsxs } from 'react/jsx-runtime';
2
2
  import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
3
3
  import clsx from 'clsx';
4
- import { forwardRef } from 'react';
4
+ import { forwardRef, useCallback } from 'react';
5
+ import { sortMenuChildren } from './dropdown-menu-content.js';
5
6
  import styles from './dropdown-menu.module.js';
7
+ import { useFullScreenMenu } from './FullScreenMenuContext.js';
6
8
 
7
9
  /**
8
10
  * Based on Radix-UI Sub Content
@@ -15,6 +17,32 @@ const DropdownMenuSubMenuContent = /*#__PURE__*/forwardRef((props, forwardedRef)
15
17
  className,
16
18
  ...rest
17
19
  } = props;
20
+ const {
21
+ isMenuFullScreenEnabled
22
+ } = useFullScreenMenu();
23
+ const preventDefault = useCallback(event => event.preventDefault(), []);
24
+ if (isMenuFullScreenEnabled) {
25
+ const {
26
+ Header,
27
+ OtherChildren
28
+ } = sortMenuChildren(children);
29
+ return jsx(DropdownMenuPrimitive.Portal, {
30
+ container: container,
31
+ children: jsxs(DropdownMenuPrimitive.SubContent, {
32
+ ...rest,
33
+ ref: forwardedRef,
34
+ className: clsx(styles.content, className),
35
+ "data-menu-fullscreen": true,
36
+ // Prevents submenu from closing, when detects focus outside submenu in fullscreen mode,
37
+ // while mouse is moving over submenu.
38
+ onFocusOutside: preventDefault,
39
+ children: [Header, jsx("div", {
40
+ className: styles.fullScreenContent,
41
+ children: OtherChildren
42
+ })]
43
+ })
44
+ });
45
+ }
18
46
  return (
19
47
  // TODO(DSYS-440): Unify all Portals (Tooltip, Select, DropdownMenu, etc...), Expose a Provider which would tell where to mount (body? other elem?)
20
48
  jsx(DropdownMenuPrimitive.Portal, {
@@ -1,5 +1,10 @@
1
1
  import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
2
2
  export type DropdownMenuSubMenuRootProps = DropdownMenuPrimitive.DropdownMenuSubProps;
3
+ interface ResponsiveSubmenuContextValue {
4
+ open: boolean;
5
+ setOpen: (open: boolean) => void;
6
+ }
7
+ export declare const useResponsiveSubmenuContext: () => ResponsiveSubmenuContextValue;
3
8
  /**
4
9
  * Based on Radix-UI Sub
5
10
  * @see https://www.radix-ui.com/docs/primitives/components/dropdown-menu#sub
@@ -8,3 +13,4 @@ export declare const DropdownMenuSubMenuRoot: {
8
13
  (props: DropdownMenuSubMenuRootProps): import("react/jsx-runtime").JSX.Element;
9
14
  displayName: string;
10
15
  };
16
+ export {};
@@ -1,6 +1,17 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
2
  import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
3
+ import noop from 'lodash/noop';
4
+ import { useContext, useCallback, createContext } from 'react';
5
+ import { useControllableState } from '../../utils/useControllableState.js';
6
+ import { SubMenuMenuProvider } from './SubMenuContext.js';
3
7
 
8
+ const ResponsiveSubmenuContext = /*#__PURE__*/createContext({
9
+ open: false,
10
+ setOpen: noop
11
+ });
12
+ const useResponsiveSubmenuContext = () => {
13
+ return useContext(ResponsiveSubmenuContext);
14
+ };
4
15
  /**
5
16
  * Based on Radix-UI Sub
6
17
  * @see https://www.radix-ui.com/docs/primitives/components/dropdown-menu#sub
@@ -8,13 +19,30 @@ import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
8
19
  const DropdownMenuSubMenuRoot = props => {
9
20
  const {
10
21
  children,
22
+ defaultOpen,
23
+ open,
24
+ onOpenChange,
11
25
  ...rest
12
26
  } = props;
13
- return jsx(DropdownMenuPrimitive.Sub, {
14
- ...rest,
15
- children: children
27
+ const [isSubMenuOpen = false, setIsSubMenuOpen] = useControllableState({
28
+ prop: open,
29
+ onChange: onOpenChange,
30
+ defaultProp: defaultOpen
31
+ });
32
+ const handleOpenChange = useCallback(value => {
33
+ setIsSubMenuOpen(value);
34
+ }, [setIsSubMenuOpen]);
35
+ return jsx(SubMenuMenuProvider, {
36
+ isSubMenuOpen: isSubMenuOpen,
37
+ setIsSubMenuOpen: setIsSubMenuOpen,
38
+ children: jsx(DropdownMenuPrimitive.Sub, {
39
+ ...rest,
40
+ onOpenChange: handleOpenChange,
41
+ open: isSubMenuOpen,
42
+ children: children
43
+ })
16
44
  });
17
45
  };
18
46
  DropdownMenuSubMenuRoot.displayName = 'DropdownMenuSubMenuRoot';
19
47
 
20
- export { DropdownMenuSubMenuRoot };
48
+ export { DropdownMenuSubMenuRoot, useResponsiveSubmenuContext };
@@ -1,10 +1,12 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
2
  import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
3
- import { forwardRef } from 'react';
3
+ import { forwardRef, useEffect, useCallback } from 'react';
4
4
  import { MenuItemSideContent, MenuItemMainContent } from '../../util-components/menu-item-sections/menu-item-sections.js';
5
5
  import { useForkRef } from '../../utils/useForkRef.js';
6
6
  import { useSubMenuFocus } from '../../utils/useSubMenuFocus.hook.js';
7
+ import { useResponsiveSubmenuContext } from './dropdown-menu-sub-menu-root.js';
7
8
  import styles from './dropdown-menu.module.js';
9
+ import { useFullScreenMenu } from './FullScreenMenuContext.js';
8
10
 
9
11
  const DropdownMenuSubMenuTriggerRoot = /*#__PURE__*/forwardRef((props, forwardedRef) => {
10
12
  const {
@@ -12,11 +14,29 @@ const DropdownMenuSubMenuTriggerRoot = /*#__PURE__*/forwardRef((props, forwarded
12
14
  ...rest
13
15
  } = props;
14
16
  const [localRef, setRefFocusNoop] = useSubMenuFocus();
17
+ const {
18
+ open
19
+ } = useResponsiveSubmenuContext();
20
+ const {
21
+ isMenuFullScreenEnabled
22
+ } = useFullScreenMenu();
23
+ useEffect(() => {
24
+ if (!open) {
25
+ localRef.current?.focus();
26
+ }
27
+ }, [localRef, open]);
28
+ const handleOnPointerMove = useCallback(e => {
29
+ setRefFocusNoop();
30
+ // we should call prevent default so hover doesn't trigger submenu, which is confusing on full screen.
31
+ if (isMenuFullScreenEnabled) {
32
+ e.preventDefault();
33
+ }
34
+ }, [isMenuFullScreenEnabled, setRefFocusNoop]);
15
35
  return jsx(DropdownMenuPrimitive.SubTrigger, {
16
36
  ...rest,
17
37
  ref: useForkRef(localRef, forwardedRef),
18
38
  className: styles.item,
19
- onPointerMove: setRefFocusNoop,
39
+ onPointerMove: handleOnPointerMove,
20
40
  children: children
21
41
  });
22
42
  });
@@ -1,4 +1,4 @@
1
1
  import '../../index.css';
2
- var styles = {"content":"bp_dropdown_menu_module_content--291e5","ellipsis":"bp_dropdown_menu_module_ellipsis--291e5","checkmark":"bp_dropdown_menu_module_checkmark--291e5","item":"bp_dropdown_menu_module_item--291e5","subMenuTrigger":"bp_dropdown_menu_module_subMenuTrigger--291e5","radioItem":"bp_dropdown_menu_module_radioItem--291e5","checkboxItem":"bp_dropdown_menu_module_checkboxItem--291e5","dropdownMenuItemSeparator":"bp_dropdown_menu_module_dropdownMenuItemSeparator--291e5"};
2
+ var styles = {"content":"bp_dropdown_menu_module_content--00a5d","fullScreenContent":"bp_dropdown_menu_module_fullScreenContent--00a5d","ellipsis":"bp_dropdown_menu_module_ellipsis--00a5d","checkmark":"bp_dropdown_menu_module_checkmark--00a5d","item":"bp_dropdown_menu_module_item--00a5d","subMenuTrigger":"bp_dropdown_menu_module_subMenuTrigger--00a5d","radioItem":"bp_dropdown_menu_module_radioItem--00a5d","checkboxItem":"bp_dropdown_menu_module_checkboxItem--00a5d","dropdownMenuItemSeparator":"bp_dropdown_menu_module_dropdownMenuItemSeparator--00a5d","menuHeader":"bp_dropdown_menu_module_menuHeader--00a5d","headerTextContent":"bp_dropdown_menu_module_headerTextContent--00a5d","submenuCloseButton":"bp_dropdown_menu_module_submenuCloseButton--00a5d","menuCloseButton":"bp_dropdown_menu_module_menuCloseButton--00a5d"};
3
3
 
4
4
  export { styles as default };
@@ -20,6 +20,14 @@ export declare const DropdownMenu: {
20
20
  MainContent: ({ className, label, description, ...rest }: import("../../util-components/menu-item-sections/menu-item-sections").MenuItemMainContentProps) => import("react/jsx-runtime").JSX.Element;
21
21
  };
22
22
  SubMenuContent: import("react").ForwardRefExoticComponent<import("./dropdown-menu-sub-menu-content").DropdownMenuSubMenuContentProps & import("react").RefAttributes<HTMLDivElement>>;
23
+ Header: (({ children, className, ...rest }: import("react").HTMLAttributes<HTMLDivElement>) => import("react/jsx-runtime").JSX.Element) & {
24
+ MenuCloseButton: import("react").ForwardRefExoticComponent<Omit<import("./dropdown-menu-header").MenuCloseButtonProps, "ref"> & import("react").RefAttributes<HTMLButtonElement>>;
25
+ SubMenuBackButton: import("react").ForwardRefExoticComponent<Omit<import("./dropdown-menu-header").MenuCloseButtonProps, "ref"> & import("react").RefAttributes<HTMLButtonElement>>;
26
+ TextContent: import("react").ForwardRefExoticComponent<Omit<import("react").HTMLAttributes<HTMLDivElement>, "children"> & {
27
+ title: string;
28
+ subtitle?: string;
29
+ } & import("react").RefAttributes<HTMLDivElement>>;
30
+ };
23
31
  };
24
32
  export { type DropdownMenuContentProps } from './dropdown-menu-content';
25
33
  export { type DropdownMenuItemProps } from './dropdown-menu-item';
@@ -1,6 +1,7 @@
1
1
  import { DropdownMenuCheckboxItem } from './dropdown-menu-checkbox-item.js';
2
2
  import { DropdownMenuContent } from './dropdown-menu-content.js';
3
3
  import { DropdownMenuGroup } from './dropdown-menu-group.js';
4
+ import { DropdownMenuHeader } from './dropdown-menu-header.js';
4
5
  import { DropdownMenuItem } from './dropdown-menu-item.js';
5
6
  import { DropdownMenuRadioGroup } from './dropdown-menu-radio-group.js';
6
7
  import { DropdownMenuRadioSelectItem } from './dropdown-menu-radio-select-item.js';
@@ -23,7 +24,8 @@ const DropdownMenu = {
23
24
  Separator: DropdownMenuSeparator,
24
25
  SubMenuRoot: DropdownMenuSubMenuRoot,
25
26
  SubMenuTrigger: DropdownMenuSubMenuTrigger,
26
- SubMenuContent: DropdownMenuSubMenuContent
27
+ SubMenuContent: DropdownMenuSubMenuContent,
28
+ Header: DropdownMenuHeader
27
29
  };
28
30
 
29
31
  export { DropdownMenu };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@box/blueprint-web",
3
- "version": "9.4.1",
3
+ "version": "9.5.0",
4
4
  "type": "module",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "publishConfig": {
@@ -63,7 +63,7 @@
63
63
  "react-stately": "^3.31.1",
64
64
  "tsx": "^4.16.5"
65
65
  },
66
- "gitHead": "afdd3dfde3958f020bf010cf62d8997183ee49f0",
66
+ "gitHead": "b2faf0831bb50c94a2fad4178d7e8b580c5a6c6e",
67
67
  "module": "lib-esm/index.js",
68
68
  "main": "lib-esm/index.js",
69
69
  "exports": {