@pushwoosh/dumb-components 1.1.230 → 1.1.231

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.
@@ -4,8 +4,9 @@ import { useState, useEffect, useCallback } from 'react';
4
4
  import { createPortal, flushSync } from 'react-dom';
5
5
  import { Place } from './styles';
6
6
  import { useLayer } from '../../../hooks';
7
- import { DropdownContext, DropdownZIndexContext } from '../../context';
7
+ import { DropdownColorContext, DropdownContext, DropdownZIndexContext, SubmenuRegistryContext } from '../../context';
8
8
  import { renderChildren } from '../../shared/renderChildren';
9
+ import { useSubmenuRegistry } from '../../shared/useSubmenuRegistry';
9
10
  const defaultPlacements = ['top-start', 'top-end', 'bottom-start', 'bottom-end'];
10
11
  /**
11
12
  * Floating dropdown menu anchored to a custom trigger (`renderHandler`), with
@@ -32,6 +33,7 @@ export const DropdownMenu = ({
32
33
  children
33
34
  }) => {
34
35
  const [isOpen, setIsOpen] = useState(openOnMount);
36
+ const submenus = useSubmenuRegistry();
35
37
  const [width, setWidth] = useState(windowWidth);
36
38
  const handleClose = useCallback(() => {
37
39
  setIsOpen(false);
@@ -111,7 +113,13 @@ export const DropdownMenu = ({
111
113
  "$fitContent": windowWidth === null && !fullWidth,
112
114
  "$scrollable": isScrollable,
113
115
  "$zIndex": resolvedZIndex,
114
- children: renderChildren(children)
116
+ children: _jsx(DropdownColorContext.Provider, {
117
+ value: color,
118
+ children: _jsx(SubmenuRegistryContext.Provider, {
119
+ value: submenus,
120
+ children: renderChildren(children)
121
+ })
122
+ })
115
123
  })
116
124
  }), document.body)]
117
125
  });
@@ -1,12 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { flip, shift, useFloating } from '@floating-ui/react-dom';
3
- import { useCallback, useContext, useEffect, useRef, useState } from 'react';
2
+ import { autoUpdate, flip, shift, size, useFloating } from '@floating-ui/react-dom';
3
+ import { useCallback, useContext, useEffect, useId, useRef, useState } from 'react';
4
4
  import { createPortal } from 'react-dom';
5
5
  import { ChevronIcon } from '@pushwoosh/kit-icons';
6
6
  import { SubmenuTriggerInner } from './styles';
7
- import { DropdownContext, DropdownZIndexContext } from '../../context';
7
+ import { DropdownColorContext, DropdownContext, DropdownZIndexContext, SubmenuRegistryContext } from '../../context';
8
8
  import { renderChildren } from '../../shared/renderChildren';
9
- import { DropdownMenuItemStyled, Place } from '../../shared/styles';
9
+ import { DropdownMenuItemStyled, Place, SCROLLABLE_MAX_HEIGHT } from '../../shared/styles';
10
+ import { useSubmenuRegistry } from '../../shared/useSubmenuRegistry';
10
11
  const OPEN_DELAY = 100;
11
12
  const CLOSE_DELAY = 200;
12
13
  /**
@@ -22,25 +23,35 @@ export const DropdownMenuSubmenu = ({
22
23
  label,
23
24
  isDanger,
24
25
  disabled,
26
+ isScrollable,
25
27
  children
26
28
  }) => {
27
29
  const parentClose = useContext(DropdownContext);
28
30
  const parentZIndex = useContext(DropdownZIndexContext);
31
+ const parentSubmenus = useContext(SubmenuRegistryContext);
32
+ const color = useContext(DropdownColorContext);
33
+ const submenus = useSubmenuRegistry();
34
+ const id = useId();
29
35
  const submenuZIndex = (parentZIndex ?? 10001) + 1;
30
36
  const [isOpen, setIsOpen] = useState(false);
31
37
  const openedByClick = useRef(false);
38
+ const [availableHeight, setAvailableHeight] = useState();
32
39
  const {
33
40
  refs,
34
41
  floatingStyles
35
42
  } = useFloating({
36
43
  strategy: 'fixed',
37
44
  placement: 'right-start',
45
+ whileElementsMounted: autoUpdate,
38
46
  middleware: [flip({
39
- fallbackPlacements: ['left-start'],
40
- boundary: document.body
47
+ fallbackPlacements: ['left-start']
41
48
  }), shift({
49
+ padding: 4
50
+ }), size({
42
51
  padding: 4,
43
- boundary: document.body
52
+ apply: ({
53
+ availableHeight: height
54
+ }) => setAvailableHeight(height)
44
55
  })]
45
56
  });
46
57
  const openTimer = useRef(null);
@@ -52,6 +63,18 @@ export const DropdownMenuSubmenu = ({
52
63
  closeTimer.current = null;
53
64
  }, []);
54
65
  useEffect(() => () => clearTimers(), [clearTimers]);
66
+ const maxHeight = isScrollable ? Math.min(availableHeight ?? SCROLLABLE_MAX_HEIGHT, SCROLLABLE_MAX_HEIGHT) : availableHeight;
67
+ const close = useCallback(() => {
68
+ clearTimers();
69
+ openedByClick.current = false;
70
+ setIsOpen(false);
71
+ }, [clearTimers]);
72
+ useEffect(() => parentSubmenus?.register(id, close), [parentSubmenus, id, close]);
73
+ useEffect(() => {
74
+ if (isOpen) {
75
+ parentSubmenus?.closeOthers(id);
76
+ }
77
+ }, [isOpen, parentSubmenus, id]);
55
78
  const scheduleOpen = useCallback(() => {
56
79
  if (disabled) return;
57
80
  openedByClick.current = false;
@@ -94,15 +117,24 @@ export const DropdownMenuSubmenu = ({
94
117
  size: "small"
95
118
  })]
96
119
  })
97
- }), isOpen && createPortal(_jsx(Place, {
98
- ref: refs.setFloating,
99
- style: floatingStyles,
100
- "$fitContent": true,
101
- "$zIndex": submenuZIndex,
102
- onMouseEnter: clearTimers,
103
- onMouseLeave: scheduleClose,
104
- onClick: e => e.stopPropagation(),
105
- children: renderChildren(children)
120
+ }), isOpen && createPortal(_jsx(SubmenuRegistryContext.Provider, {
121
+ value: submenus,
122
+ children: _jsx(Place, {
123
+ ref: refs.setFloating,
124
+ style: {
125
+ ...floatingStyles,
126
+ maxHeight,
127
+ overflowY: maxHeight ? 'auto' : undefined
128
+ },
129
+ "$color": color,
130
+ "$scrollable": isScrollable,
131
+ "$fitContent": true,
132
+ "$zIndex": submenuZIndex,
133
+ onMouseEnter: clearTimers,
134
+ onMouseLeave: scheduleClose,
135
+ onClick: e => e.stopPropagation(),
136
+ children: renderChildren(children)
137
+ })
106
138
  }), document.body)]
107
139
  })
108
140
  });
@@ -7,6 +7,8 @@ export type DropdownMenuSubmenuProps = {
7
7
  isDanger?: boolean;
8
8
  /** Disable the submenu. */
9
9
  disabled?: boolean;
10
+ /** Cap the flyout height and scroll its contents. */
11
+ isScrollable?: boolean;
10
12
  /** Submenu contents — nodes, or a function returning nodes. */
11
13
  children: (() => ReactNode) | ReactNode;
12
14
  };
@@ -1,2 +1,9 @@
1
+ import type { Color } from '@pushwoosh/kit-constants';
1
2
  export declare const DropdownContext: import("react").Context<((isOpen: false) => void) | null>;
2
3
  export declare const DropdownZIndexContext: import("react").Context<number | undefined>;
4
+ export type SubmenuRegistry = {
5
+ register: (id: string, close: () => void) => () => void;
6
+ closeOthers: (id: string) => void;
7
+ };
8
+ export declare const SubmenuRegistryContext: import("react").Context<SubmenuRegistry | null>;
9
+ export declare const DropdownColorContext: import("react").Context<Color.FROZEN | Color.CLEAR | undefined>;
@@ -1,3 +1,5 @@
1
1
  import { createContext } from 'react';
2
2
  export const DropdownContext = createContext(null);
3
- export const DropdownZIndexContext = createContext(undefined);
3
+ export const DropdownZIndexContext = createContext(undefined);
4
+ export const SubmenuRegistryContext = createContext(null);
5
+ export const DropdownColorContext = createContext(undefined);
@@ -1,3 +1,4 @@
1
+ export declare const SCROLLABLE_MAX_HEIGHT = 248;
1
2
  export declare const Place: import("styled-components").StyledComponent<"div", any, import("@pushwoosh/kit-helpers/styled-helpers").CommonBoxProps & {
2
3
  $alignContent?: import("@pushwoosh/kit-helpers/styled-helpers").AlignContentValue | undefined;
3
4
  $alignItems?: import("@pushwoosh/kit-helpers/styled-helpers").ItemAlignmentValue | undefined;
@@ -1,7 +1,8 @@
1
1
  import styled, { css } from 'styled-components';
2
2
  import { Color, Shadow, ShapeRadius, UnitSize } from '@pushwoosh/kit-constants';
3
3
  import { Vertical } from '@pushwoosh/kit-helpers';
4
- const scrollableCss = css(["max-height:248px;overflow-y:auto;"]);
4
+ export const SCROLLABLE_MAX_HEIGHT = 248;
5
+ const scrollableCss = css(["max-height:", "px;overflow-y:auto;"], SCROLLABLE_MAX_HEIGHT);
5
6
  const getWidth = ($width, $fitContent) => {
6
7
  if ($fitContent) return 'max-content';
7
8
  if ($width) return `${$width}px`;
@@ -0,0 +1,6 @@
1
+ import type { SubmenuRegistry } from '../context';
2
+ /**
3
+ * Registry of the submenus rendered on one menu level: opening a submenu closes
4
+ * its siblings through it, and never the branch it hangs from.
5
+ */
6
+ export declare function useSubmenuRegistry(): SubmenuRegistry;
@@ -0,0 +1,23 @@
1
+ import { useMemo, useRef } from 'react';
2
+ /**
3
+ * Registry of the submenus rendered on one menu level: opening a submenu closes
4
+ * its siblings through it, and never the branch it hangs from.
5
+ */
6
+ export function useSubmenuRegistry() {
7
+ const entries = useRef(new Map());
8
+ return useMemo(() => ({
9
+ register: (id, close) => {
10
+ entries.current.set(id, close);
11
+ return () => {
12
+ entries.current.delete(id);
13
+ };
14
+ },
15
+ closeOthers: id => {
16
+ entries.current.forEach((close, key) => {
17
+ if (key !== id) {
18
+ close();
19
+ }
20
+ });
21
+ }
22
+ }), []);
23
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushwoosh/dumb-components",
3
- "version": "1.1.230",
3
+ "version": "1.1.231",
4
4
  "description": "React components to build Pushwoosh products",
5
5
  "main": "index.js",
6
6
  "module": "index.js",