@pushwoosh/dumb-components 2.6.0 → 2.7.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.
@@ -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. Must be rendered inside a {@link DropdownMenu}.
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: FC<DropdownMenuItemProps>;
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. Must be rendered inside a {@link DropdownMenu}.
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
- return _jsxs(DropdownMenuItemStyled, {
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-2"
40
+ componentId: "sc-hlq2pz-3"
37
41
  })(["margin:-2px 0 0;line-height:0;"]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushwoosh/dumb-components",
3
- "version": "2.6.0",
3
+ "version": "2.7.0",
4
4
  "description": "React components to build Pushwoosh products",
5
5
  "main": "index.js",
6
6
  "module": "index.js",