@pushwoosh/dumb-components 1.1.229 → 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
+ }
@@ -25,7 +25,9 @@ export const RichMessageEditor = ({
25
25
  title,
26
26
  initialValue,
27
27
  minHeight,
28
+ placeholder,
28
29
  error,
30
+ isErrored,
29
31
  canUsePersonalization,
30
32
  canUseEmoji,
31
33
  onChange,
@@ -123,6 +125,8 @@ export const RichMessageEditor = ({
123
125
  }),
124
126
  children: [_jsx(SlateEditable, {
125
127
  minHeight: minHeight,
128
+ placeholder: placeholder,
129
+ isErrored: isErrored || !!error,
126
130
  onFocus: () => {
127
131
  setIsFocused(true);
128
132
  const {
@@ -24,8 +24,12 @@ export type RichMessageEditorProps = {
24
24
  onChange: (value: string) => void;
25
25
  /** Minimum editor height (CSS value). */
26
26
  minHeight?: string;
27
+ /** Hint shown while the editor is empty. */
28
+ placeholder?: string;
27
29
  /** Error message; switches the field to the error state. */
28
30
  error?: ReactNode;
31
+ /** Draw the error border without an error message. */
32
+ isErrored?: boolean;
29
33
  /** Enable inserting personalization (dynamic-content) tokens. */
30
34
  canUsePersonalization?: boolean;
31
35
  /** Enable the emoji picker. */
@@ -12,6 +12,8 @@ type SlateEditor = {
12
12
  export declare const SlateEditor: FC<SlateEditor>;
13
13
  type SlateEditableProps = {
14
14
  minHeight?: string;
15
+ placeholder?: string;
16
+ isErrored?: boolean;
15
17
  onFocus?: () => void;
16
18
  onDelete: () => void;
17
19
  };
@@ -26,6 +26,8 @@ export const SlateEditor = ({
26
26
  };
27
27
  export const SlateEditable = ({
28
28
  minHeight,
29
+ placeholder,
30
+ isErrored,
29
31
  onFocus,
30
32
  onDelete
31
33
  }) => {
@@ -34,6 +36,8 @@ export const SlateEditable = ({
34
36
  });
35
37
  return _jsx(EditableStyled, {
36
38
  "$minHeight": minHeight,
39
+ "$isErrored": isErrored,
40
+ placeholder: placeholder,
37
41
  renderElement: renderElementPersistent,
38
42
  onFocus: onFocus
39
43
  });
@@ -15,4 +15,5 @@ export declare const EditableStyled: import("styled-components").StyledComponent
15
15
  disableDefaultStyles?: boolean | undefined;
16
16
  } & import("react").TextareaHTMLAttributes<HTMLDivElement> & import("react").RefAttributes<HTMLDivElement>>, any, {
17
17
  $minHeight?: string;
18
+ $isErrored?: boolean;
18
19
  }, never>;
@@ -4,6 +4,8 @@ import { Color, FontSize, FontWeight, ShapeRadius } from '@pushwoosh/kit-constan
4
4
  export const EditableStyled = styled(Editable).withConfig({
5
5
  displayName: "EditableStyled",
6
6
  componentId: "sc-1frhl1f-0"
7
- })(["font-weight:", ";font-size:", ";line-height:140%;padding:8px;color:", ";background:", ";caret-color:", ";border:1px solid ", ";border-radius:", ";overflow-x:auto;", " &:focus{border-color:", ";outline:none;}& > p{margin:4px 0;&:first-child{margin-top:0;}&:last-child{margin-bottom:0;}}"], FontWeight.REGULAR, FontSize.REGULAR, Color.MAIN, Color.CLEAR, Color.BRIGHT, Color.FORM, ShapeRadius.CONTROL, ({
7
+ })(["font-weight:", ";font-size:", ";line-height:140%;padding:8px;color:", ";background:", ";caret-color:", ";border:1px solid ", ";border-radius:", ";overflow-x:auto;", " & [data-slate-placeholder='true']{color:", ";opacity:1 !important;}&:focus{border-color:", ";outline:none;}& > p{position:relative;margin:4px 0;&:first-child{margin-top:0;}&:last-child{margin-bottom:0;}}"], FontWeight.REGULAR, FontSize.REGULAR, Color.MAIN, Color.CLEAR, Color.BRIGHT, ({
8
+ $isErrored
9
+ }) => $isErrored ? Color.DANGER : Color.FORM, ShapeRadius.CONTROL, ({
8
10
  $minHeight
9
- }) => $minHeight && `min-height: ${$minHeight};`, Color.BRIGHT);
11
+ }) => $minHeight && `min-height: ${$minHeight};`, Color.PHANTOM, Color.BRIGHT);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushwoosh/dumb-components",
3
- "version": "1.1.229",
3
+ "version": "1.1.231",
4
4
  "description": "React components to build Pushwoosh products",
5
5
  "main": "index.js",
6
6
  "module": "index.js",