@pushwoosh/dumb-components 2.6.0 → 2.7.1
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.
- package/DropdownMenu/__tests__/DropdownMenuItem.test.d.ts +1 -0
- package/DropdownMenu/__tests__/DropdownMenuItem.test.js +109 -0
- package/DropdownMenu/components/DropdownMenuItem/DropdownMenuItem.d.ts +13 -4
- package/DropdownMenu/components/DropdownMenuItem/DropdownMenuItem.js +32 -9
- package/DropdownMenu/components/DropdownMenuItem/styles.d.ts +1 -1
- package/DropdownMenu/components/DropdownMenuItem/styles.js +1 -1
- package/DropdownMenu/components/DropdownMenuItem/types.d.ts +3 -1
- package/DropdownMenu/shared/styles.d.ts +1 -0
- package/DropdownMenu/shared/styles.js +6 -2
- package/RichMessageEditor/slate/SlateEditor.js +3 -1
- package/RichMessageEditor/slate/SlateEditor.styled.d.ts +0 -1
- package/RichMessageEditor/slate/SlateEditor.styled.js +2 -4
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
3
|
+
import { createRef } from 'react';
|
|
4
|
+
import { DropdownMenuItem } from '../components/DropdownMenuItem';
|
|
5
|
+
import { DropdownContext } from '../context';
|
|
6
|
+
const renderInMenu = (ui, close = jest.fn()) => render(_jsx(DropdownContext.Provider, {
|
|
7
|
+
value: close,
|
|
8
|
+
children: ui
|
|
9
|
+
}));
|
|
10
|
+
describe('DropdownMenuItem', () => {
|
|
11
|
+
it('calls onClick and closes the menu', () => {
|
|
12
|
+
const onClick = jest.fn();
|
|
13
|
+
const close = jest.fn();
|
|
14
|
+
renderInMenu(_jsx(DropdownMenuItem, {
|
|
15
|
+
onClick: onClick,
|
|
16
|
+
children: "Preview"
|
|
17
|
+
}), close);
|
|
18
|
+
const item = screen.getByRole('button', {
|
|
19
|
+
name: 'Preview'
|
|
20
|
+
});
|
|
21
|
+
expect(item).toHaveAttribute('type', 'button');
|
|
22
|
+
fireEvent.click(item);
|
|
23
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
24
|
+
expect(close).toHaveBeenCalledWith(false);
|
|
25
|
+
});
|
|
26
|
+
it('keeps a disabled item without hint natively disabled', () => {
|
|
27
|
+
renderInMenu(_jsx(DropdownMenuItem, {
|
|
28
|
+
onClick: jest.fn(),
|
|
29
|
+
disabled: true,
|
|
30
|
+
children: "Preview"
|
|
31
|
+
}));
|
|
32
|
+
expect(screen.getByRole('button', {
|
|
33
|
+
name: 'Preview'
|
|
34
|
+
})).toBeDisabled();
|
|
35
|
+
});
|
|
36
|
+
it('makes a disabled item with hint focusable, described and inert', () => {
|
|
37
|
+
const onClick = jest.fn();
|
|
38
|
+
const close = jest.fn();
|
|
39
|
+
renderInMenu(_jsx(DropdownMenuItem, {
|
|
40
|
+
onClick: onClick,
|
|
41
|
+
disabled: true,
|
|
42
|
+
hint: "Save the template first",
|
|
43
|
+
children: "Test email"
|
|
44
|
+
}), close);
|
|
45
|
+
const item = screen.getByRole('button', {
|
|
46
|
+
name: 'Test email'
|
|
47
|
+
});
|
|
48
|
+
expect(item).not.toBeDisabled();
|
|
49
|
+
expect(item).toHaveAttribute('aria-disabled', 'true');
|
|
50
|
+
expect(item).toHaveAccessibleDescription('Save the template first');
|
|
51
|
+
fireEvent.click(item);
|
|
52
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
53
|
+
expect(close).not.toHaveBeenCalled();
|
|
54
|
+
});
|
|
55
|
+
it('shows the hint tooltip on hover and focus', () => {
|
|
56
|
+
renderInMenu(_jsx(DropdownMenuItem, {
|
|
57
|
+
onClick: jest.fn(),
|
|
58
|
+
disabled: true,
|
|
59
|
+
hint: "Save first",
|
|
60
|
+
children: "Test email"
|
|
61
|
+
}));
|
|
62
|
+
const item = screen.getByRole('button', {
|
|
63
|
+
name: 'Test email'
|
|
64
|
+
});
|
|
65
|
+
fireEvent.mouseEnter(item);
|
|
66
|
+
expect(screen.getAllByText('Save first')).toHaveLength(2);
|
|
67
|
+
fireEvent.mouseLeave(item);
|
|
68
|
+
expect(screen.getAllByText('Save first')).toHaveLength(1);
|
|
69
|
+
fireEvent.focusIn(item);
|
|
70
|
+
expect(screen.getAllByText('Save first')).toHaveLength(2);
|
|
71
|
+
});
|
|
72
|
+
it('ignores hint while enabled', () => {
|
|
73
|
+
renderInMenu(_jsx(DropdownMenuItem, {
|
|
74
|
+
onClick: jest.fn(),
|
|
75
|
+
hint: "Save first",
|
|
76
|
+
children: "Test email"
|
|
77
|
+
}));
|
|
78
|
+
expect(screen.queryByText('Save first')).toBeNull();
|
|
79
|
+
expect(screen.getByRole('button', {
|
|
80
|
+
name: 'Test email'
|
|
81
|
+
})).not.toHaveAttribute('aria-describedby');
|
|
82
|
+
});
|
|
83
|
+
it('forwards ref to the item button, with and without hint', () => {
|
|
84
|
+
const ref = createRef();
|
|
85
|
+
const {
|
|
86
|
+
rerender
|
|
87
|
+
} = renderInMenu(_jsx(DropdownMenuItem, {
|
|
88
|
+
ref: ref,
|
|
89
|
+
onClick: jest.fn(),
|
|
90
|
+
children: "Preview"
|
|
91
|
+
}));
|
|
92
|
+
expect(ref.current).toBe(screen.getByRole('button', {
|
|
93
|
+
name: 'Preview'
|
|
94
|
+
}));
|
|
95
|
+
rerender(_jsx(DropdownContext.Provider, {
|
|
96
|
+
value: jest.fn(),
|
|
97
|
+
children: _jsx(DropdownMenuItem, {
|
|
98
|
+
ref: ref,
|
|
99
|
+
onClick: jest.fn(),
|
|
100
|
+
disabled: true,
|
|
101
|
+
hint: "Add content",
|
|
102
|
+
children: "Preview"
|
|
103
|
+
})
|
|
104
|
+
}));
|
|
105
|
+
expect(ref.current).toBe(screen.getByRole('button', {
|
|
106
|
+
name: 'Preview'
|
|
107
|
+
}));
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import { type FC } from 'react';
|
|
2
|
-
import { type DropdownMenuItemProps } from './types';
|
|
3
1
|
/**
|
|
4
2
|
* Clickable row inside a {@link DropdownMenu}, with active (checked), danger and
|
|
5
|
-
* disabled states.
|
|
3
|
+
* disabled states. A disabled item with `hint` stays focusable and explains itself in a tooltip.
|
|
4
|
+
* Must be rendered inside a {@link DropdownMenu}.
|
|
6
5
|
*
|
|
7
6
|
* @example
|
|
8
7
|
* <DropdownMenuItem onClick={select} isActive={selected}>Option</DropdownMenuItem>
|
|
8
|
+
* <DropdownMenuItem onClick={sendTest} disabled={!isSaved} hint="Save the template first">Test email</DropdownMenuItem>
|
|
9
9
|
*/
|
|
10
|
-
export declare const DropdownMenuItem:
|
|
10
|
+
export declare const DropdownMenuItem: import("react").ForwardRefExoticComponent<{
|
|
11
|
+
onClick: () => void;
|
|
12
|
+
isActive?: boolean;
|
|
13
|
+
isDanger?: boolean;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
hint?: import("react").ReactNode;
|
|
16
|
+
closeOnClick?: boolean;
|
|
17
|
+
} & {
|
|
18
|
+
children?: import("react").ReactNode | undefined;
|
|
19
|
+
} & import("react").RefAttributes<HTMLButtonElement>>;
|
|
@@ -1,39 +1,50 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useContext } from 'react';
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useContext, useId } from 'react';
|
|
3
3
|
import { CheckIcon } from '@pushwoosh/kit-icons';
|
|
4
|
-
import { DropdownMenuItemStyled, DropdownMenuIconStyled } from './styles';
|
|
4
|
+
import { DropdownMenuItemStyled, DropdownMenuIconStyled, DropdownMenuItemHintStyled } from './styles';
|
|
5
|
+
import { Tooltip } from '../../../Tooltip';
|
|
5
6
|
import { DropdownContext } from '../../context';
|
|
6
7
|
/**
|
|
7
8
|
* Clickable row inside a {@link DropdownMenu}, with active (checked), danger and
|
|
8
|
-
* disabled states.
|
|
9
|
+
* disabled states. A disabled item with `hint` stays focusable and explains itself in a tooltip.
|
|
10
|
+
* Must be rendered inside a {@link DropdownMenu}.
|
|
9
11
|
*
|
|
10
12
|
* @example
|
|
11
13
|
* <DropdownMenuItem onClick={select} isActive={selected}>Option</DropdownMenuItem>
|
|
14
|
+
* <DropdownMenuItem onClick={sendTest} disabled={!isSaved} hint="Save the template first">Test email</DropdownMenuItem>
|
|
12
15
|
*/
|
|
13
|
-
export const DropdownMenuItem = ({
|
|
16
|
+
export const DropdownMenuItem = forwardRef(({
|
|
14
17
|
children,
|
|
15
18
|
onClick,
|
|
16
19
|
isActive,
|
|
17
20
|
isDanger,
|
|
18
21
|
disabled,
|
|
22
|
+
hint,
|
|
19
23
|
closeOnClick = true
|
|
20
|
-
}) => {
|
|
24
|
+
}, ref) => {
|
|
21
25
|
const onClickContext = useContext(DropdownContext);
|
|
26
|
+
const hintId = useId();
|
|
22
27
|
if (process.env.NODE_ENV === 'development') {
|
|
23
28
|
if (!onClickContext) {
|
|
24
29
|
console.error('DropdownMenuItem must be inside DropdownMenu');
|
|
25
30
|
}
|
|
26
31
|
}
|
|
27
|
-
|
|
32
|
+
const hasHint = Boolean(disabled && hint);
|
|
33
|
+
const item = _jsxs(DropdownMenuItemStyled, {
|
|
34
|
+
ref: ref,
|
|
35
|
+
type: "button",
|
|
28
36
|
"$active": isActive,
|
|
29
37
|
"$danger": isDanger,
|
|
30
38
|
onClick: () => {
|
|
39
|
+
if (disabled) return;
|
|
31
40
|
if (closeOnClick) {
|
|
32
41
|
onClickContext?.(false);
|
|
33
42
|
}
|
|
34
43
|
onClick();
|
|
35
44
|
},
|
|
36
|
-
disabled: disabled,
|
|
45
|
+
disabled: disabled && !hasHint,
|
|
46
|
+
"aria-disabled": hasHint || undefined,
|
|
47
|
+
"aria-describedby": hasHint ? hintId : undefined,
|
|
37
48
|
"data-active": isActive || undefined,
|
|
38
49
|
children: [children, isActive && _jsx(DropdownMenuIconStyled, {
|
|
39
50
|
children: _jsx(CheckIcon, {
|
|
@@ -41,4 +52,16 @@ export const DropdownMenuItem = ({
|
|
|
41
52
|
})
|
|
42
53
|
})]
|
|
43
54
|
});
|
|
44
|
-
|
|
55
|
+
if (!hasHint) return item;
|
|
56
|
+
return _jsxs(_Fragment, {
|
|
57
|
+
children: [_jsx(Tooltip, {
|
|
58
|
+
content: hint,
|
|
59
|
+
position: "left-middle",
|
|
60
|
+
children: item
|
|
61
|
+
}), _jsx(DropdownMenuItemHintStyled, {
|
|
62
|
+
id: hintId,
|
|
63
|
+
children: hint
|
|
64
|
+
})]
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
DropdownMenuItem.displayName = 'DropdownMenuItem';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { DropdownMenuItemStyled, DropdownMenuIconStyled } from '../../shared/styles';
|
|
1
|
+
export { DropdownMenuItemStyled, DropdownMenuIconStyled, DropdownMenuItemHintStyled } from '../../shared/styles';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { DropdownMenuItemStyled, DropdownMenuIconStyled } from '../../shared/styles';
|
|
1
|
+
export { DropdownMenuItemStyled, DropdownMenuIconStyled, DropdownMenuItemHintStyled } from '../../shared/styles';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PropsWithChildren } from 'react';
|
|
1
|
+
import type { PropsWithChildren, ReactNode } from 'react';
|
|
2
2
|
/** Props of {@link DropdownMenuItem}. */
|
|
3
3
|
export type DropdownMenuItemProps = PropsWithChildren<{
|
|
4
4
|
/** Fires when the item is clicked. */
|
|
@@ -9,6 +9,8 @@ export type DropdownMenuItemProps = PropsWithChildren<{
|
|
|
9
9
|
isDanger?: boolean;
|
|
10
10
|
/** Disable the item. */
|
|
11
11
|
disabled?: boolean;
|
|
12
|
+
/** Why the item is unavailable; shown as a tooltip on hover/focus, only while `disabled`. */
|
|
13
|
+
hint?: ReactNode;
|
|
12
14
|
/** Close the menu after clicking (default `true`). */
|
|
13
15
|
closeOnClick?: boolean;
|
|
14
16
|
}>;
|
|
@@ -21,4 +21,5 @@ export declare const DropdownMenuItemStyled: import("styled-components").StyledC
|
|
|
21
21
|
$active?: boolean;
|
|
22
22
|
$danger?: boolean;
|
|
23
23
|
}, never>;
|
|
24
|
+
export declare const DropdownMenuItemHintStyled: import("styled-components").StyledComponent<"span", any, {}, never>;
|
|
24
25
|
export declare const DropdownMenuIconStyled: import("styled-components").StyledComponent<"span", any, {}, never>;
|
|
@@ -24,14 +24,18 @@ export const Place = styled(Vertical).withConfig({
|
|
|
24
24
|
export const DropdownMenuItemStyled = styled.button.withConfig({
|
|
25
25
|
displayName: "DropdownMenuItemStyled",
|
|
26
26
|
componentId: "sc-hlq2pz-1"
|
|
27
|
-
})(["appearance:none;display:flex;align-items:center;justify-content:space-between;gap:8px;white-space:nowrap;padding:0 12px;min-height:", ";border:none;overflow:hidden;color:", ";background:none;cursor:pointer;&:hover{background:", ";color:", ";}&:disabled{color:", ";cursor:not-allowed;}"], UnitSize.FIELD_HEIGHT, ({
|
|
27
|
+
})(["appearance:none;display:flex;align-items:center;justify-content:space-between;gap:8px;white-space:nowrap;width:100%;padding:0 12px;min-height:", ";border:none;overflow:hidden;color:", ";background:none;cursor:pointer;&:hover:not(:disabled,[aria-disabled='true']){background:", ";color:", ";}&:disabled,&[aria-disabled='true']{color:", ";}&:disabled{cursor:not-allowed;}&[aria-disabled='true']{cursor:default;}"], UnitSize.FIELD_HEIGHT, ({
|
|
28
28
|
$active,
|
|
29
29
|
$danger
|
|
30
30
|
}) => {
|
|
31
31
|
if ($danger) return Color.DANGER;
|
|
32
32
|
return $active ? Color.PRIMARY : Color.MAIN;
|
|
33
33
|
}, Color.SOFT_LIGHT, Color.MAIN, Color.LOCKED);
|
|
34
|
+
export const DropdownMenuItemHintStyled = styled.span.withConfig({
|
|
35
|
+
displayName: "DropdownMenuItemHintStyled",
|
|
36
|
+
componentId: "sc-hlq2pz-2"
|
|
37
|
+
})(["position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;"]);
|
|
34
38
|
export const DropdownMenuIconStyled = styled.span.withConfig({
|
|
35
39
|
displayName: "DropdownMenuIconStyled",
|
|
36
|
-
componentId: "sc-hlq2pz-
|
|
40
|
+
componentId: "sc-hlq2pz-3"
|
|
37
41
|
})(["margin:-2px 0 0;line-height:0;"]);
|
|
@@ -35,7 +35,9 @@ export const SlateEditable = ({
|
|
|
35
35
|
return renderElement(props, onDelete);
|
|
36
36
|
});
|
|
37
37
|
return _jsx(EditableStyled, {
|
|
38
|
-
|
|
38
|
+
style: minHeight ? {
|
|
39
|
+
minHeight
|
|
40
|
+
} : undefined,
|
|
39
41
|
"$isErrored": isErrored,
|
|
40
42
|
placeholder: placeholder,
|
|
41
43
|
renderElement: renderElementPersistent,
|
|
@@ -14,6 +14,5 @@ export declare const EditableStyled: import("styled-components").StyledComponent
|
|
|
14
14
|
as?: React.ElementType<any> | undefined;
|
|
15
15
|
disableDefaultStyles?: boolean | undefined;
|
|
16
16
|
} & import("react").TextareaHTMLAttributes<HTMLDivElement> & import("react").RefAttributes<HTMLDivElement>>, any, {
|
|
17
|
-
$minHeight?: string;
|
|
18
17
|
$isErrored?: boolean;
|
|
19
18
|
}, never>;
|
|
@@ -4,8 +4,6 @@ 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
|
|
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
8
|
$isErrored
|
|
9
|
-
}) => $isErrored ? Color.DANGER : Color.FORM, ShapeRadius.CONTROL,
|
|
10
|
-
$minHeight
|
|
11
|
-
}) => $minHeight && `min-height: ${$minHeight};`, Color.PHANTOM, Color.BRIGHT);
|
|
9
|
+
}) => $isErrored ? Color.DANGER : Color.FORM, ShapeRadius.CONTROL, Color.PHANTOM, Color.BRIGHT);
|