@transferwise/components 46.157.1 → 46.159.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 (68) hide show
  1. package/build/FlowNavigation/FlowNavigation.js +5 -14
  2. package/build/FlowNavigation/FlowNavigation.js.map +1 -1
  3. package/build/FlowNavigation/FlowNavigation.mjs +5 -14
  4. package/build/FlowNavigation/FlowNavigation.mjs.map +1 -1
  5. package/build/Markup/Markup.js +15 -14
  6. package/build/Markup/Markup.js.map +1 -1
  7. package/build/Markup/Markup.mjs +15 -14
  8. package/build/Markup/Markup.mjs.map +1 -1
  9. package/build/Popover/Popover.js +4 -0
  10. package/build/Popover/Popover.js.map +1 -1
  11. package/build/Popover/Popover.mjs +4 -0
  12. package/build/Popover/Popover.mjs.map +1 -1
  13. package/build/_deprecated/OverlayHeader/OverlayHeader.js +6 -41
  14. package/build/_deprecated/OverlayHeader/OverlayHeader.js.map +1 -1
  15. package/build/_deprecated/OverlayHeader/OverlayHeader.mjs +6 -41
  16. package/build/_deprecated/OverlayHeader/OverlayHeader.mjs.map +1 -1
  17. package/build/common/Panel/Panel.js +13 -4
  18. package/build/common/Panel/Panel.js.map +1 -1
  19. package/build/common/Panel/Panel.mjs +14 -5
  20. package/build/common/Panel/Panel.mjs.map +1 -1
  21. package/build/common/PropsValues/position.js +29 -0
  22. package/build/common/PropsValues/position.js.map +1 -1
  23. package/build/common/PropsValues/position.mjs +28 -1
  24. package/build/common/PropsValues/position.mjs.map +1 -1
  25. package/build/main.css +8 -34
  26. package/build/styles/FlowNavigation/FlowNavigation.css +7 -13
  27. package/build/styles/main.css +8 -34
  28. package/build/types/FlowNavigation/FlowNavigation.d.ts +2 -9
  29. package/build/types/FlowNavigation/FlowNavigation.d.ts.map +1 -1
  30. package/build/types/Markup/Markup.d.ts.map +1 -1
  31. package/build/types/Markup/Markup.types.d.ts +16 -5
  32. package/build/types/Markup/Markup.types.d.ts.map +1 -1
  33. package/build/types/Markup/index.d.ts +1 -1
  34. package/build/types/Markup/index.d.ts.map +1 -1
  35. package/build/types/Popover/Popover.d.ts +1 -1
  36. package/build/types/Popover/Popover.d.ts.map +1 -1
  37. package/build/types/_deprecated/OverlayHeader/OverlayHeader.d.ts.map +1 -1
  38. package/build/types/common/Panel/Panel.d.ts +6 -6
  39. package/build/types/common/Panel/Panel.d.ts.map +1 -1
  40. package/build/types/common/PropsValues/position.d.ts +18 -1
  41. package/build/types/common/PropsValues/position.d.ts.map +1 -1
  42. package/build/types/common/ResponsivePanel/ResponsivePanel.d.ts +2 -2
  43. package/build/types/index.d.ts +1 -1
  44. package/build/types/index.d.ts.map +1 -1
  45. package/package.json +3 -3
  46. package/src/FlowNavigation/FlowNavigation.css +7 -13
  47. package/src/FlowNavigation/FlowNavigation.less +6 -15
  48. package/src/FlowNavigation/FlowNavigation.tsx +6 -27
  49. package/src/FlowNavigation/_stories/FlowNavigation.story.tsx +0 -86
  50. package/src/Markup/Markup.test.tsx +8 -7
  51. package/src/Markup/Markup.tsx +18 -15
  52. package/src/Markup/Markup.types.ts +16 -6
  53. package/src/Markup/_stories/Markup.docs.mdx +60 -0
  54. package/src/Markup/_stories/Markup.story.tsx +7 -8
  55. package/src/Markup/index.ts +6 -1
  56. package/src/Popover/Popover.tsx +9 -2
  57. package/src/_deprecated/OverlayHeader/OverlayHeader.tsx +2 -21
  58. package/src/common/Panel/Panel.test.tsx +17 -0
  59. package/src/common/Panel/Panel.tsx +17 -8
  60. package/src/common/Panel/_stories/Panel.story.tsx +182 -0
  61. package/src/common/PropsValues/position.ts +54 -0
  62. package/src/i18n/en.json +1 -0
  63. package/src/index.ts +1 -1
  64. package/src/main.css +8 -34
  65. package/src/main.less +0 -1
  66. package/build/styles/_deprecated/OverlayHeader/OverlayHeader.css +0 -15
  67. package/src/_deprecated/OverlayHeader/OverlayHeader.css +0 -15
  68. package/src/_deprecated/OverlayHeader/OverlayHeader.less +0 -24
@@ -4,7 +4,7 @@ import Link from '../Link';
4
4
 
5
5
  import { parseMarkup } from './utils/parseMarkup';
6
6
  import { type AllowLinksMode, isHrefAllowedByMode, sanitiseHref } from './utils/sanitise';
7
- import type { MarkupActions, MarkupNode, MarkupProps } from './Markup.types';
7
+ import type { MarkupActionHandler, MarkupNode, MarkupProps } from './Markup.types';
8
8
 
9
9
  const PLACEHOLDER_REGEX = /\{\{(\w+)\}\}/g;
10
10
 
@@ -16,28 +16,31 @@ function substituteData(text: string, data: MarkupData): string {
16
16
  );
17
17
  }
18
18
 
19
+ type OnAction = MarkupActionHandler | undefined;
20
+
19
21
  function renderNodes(
20
22
  nodes: MarkupNode[],
21
- actions: MarkupActions | undefined,
23
+ onAction: OnAction,
22
24
  data: MarkupData | undefined,
23
25
  allowLinks: AllowLinksMode,
24
26
  ): React.ReactNode[] {
25
- return nodes.map((node, index) => renderNode(node, actions, data, allowLinks, index));
27
+ return nodes.map((node, index) => renderNode(node, onAction, data, allowLinks, index));
26
28
  }
27
29
 
28
30
  function renderLinkNode(
29
31
  node: Extract<MarkupNode, { type: 'link' }>,
30
- actions: MarkupActions | undefined,
32
+ onAction: OnAction,
31
33
  data: MarkupData | undefined,
32
34
  allowLinks: AllowLinksMode,
33
35
  key: number,
34
36
  ): React.ReactNode {
35
- const children = renderNodes(node.children, actions, data, allowLinks);
37
+ const children = renderNodes(node.children, onAction, data, allowLinks);
36
38
 
37
- if (node.action && actions?.[node.action]) {
39
+ if (node.action && onAction) {
40
+ const actionId = node.action;
38
41
  return (
39
42
  // eslint-disable-next-line jsx-a11y/click-events-have-key-events
40
- <Link key={key} aria-label={node.accessibilityLabel} onClick={actions[node.action]}>
43
+ <Link key={key} aria-label={node.accessibilityLabel} onClick={() => onAction(actionId)}>
41
44
  {children}
42
45
  </Link>
43
46
  );
@@ -62,7 +65,7 @@ function renderLinkNode(
62
65
 
63
66
  function renderNode(
64
67
  node: MarkupNode,
65
- actions: MarkupActions | undefined,
68
+ onAction: OnAction,
66
69
  data: MarkupData | undefined,
67
70
  allowLinks: AllowLinksMode,
68
71
  key: number,
@@ -83,29 +86,29 @@ function renderNode(
83
86
  className={`wds-markup-emphasis wds-markup-emphasis--${node.type}`}
84
87
  aria-label={node.accessibilityLabel}
85
88
  >
86
- {renderNodes(node.children, actions, data, allowLinks)}
89
+ {renderNodes(node.children, onAction, data, allowLinks)}
87
90
  </em>
88
91
  );
89
92
  case 'strong':
90
93
  return (
91
94
  <strong key={key} className="wds-markup-strong" aria-label={node.accessibilityLabel}>
92
- {renderNodes(node.children, actions, data, allowLinks)}
95
+ {renderNodes(node.children, onAction, data, allowLinks)}
93
96
  </strong>
94
97
  );
95
98
  case 'strikethrough':
96
99
  return (
97
100
  <del key={key} className="wds-markup-strikethrough" aria-label={node.accessibilityLabel}>
98
- {renderNodes(node.children, actions, data, allowLinks)}
101
+ {renderNodes(node.children, onAction, data, allowLinks)}
99
102
  </del>
100
103
  );
101
104
  case 'paragraph':
102
105
  return (
103
106
  <p key={key} className="wds-markup-paragraph">
104
- {renderNodes(node.children, actions, data, allowLinks)}
107
+ {renderNodes(node.children, onAction, data, allowLinks)}
105
108
  </p>
106
109
  );
107
110
  case 'link':
108
- return renderLinkNode(node, actions, data, allowLinks, key);
111
+ return renderLinkNode(node, onAction, data, allowLinks, key);
109
112
  }
110
113
  }
111
114
 
@@ -138,7 +141,7 @@ export const Markup: React.FC<MarkupProps> = ({
138
141
  children,
139
142
  data,
140
143
  allowLinks = 'internal',
141
- actions,
144
+ onAction,
142
145
  as,
143
146
  className,
144
147
  'data-testid': testId,
@@ -167,7 +170,7 @@ export const Markup: React.FC<MarkupProps> = ({
167
170
 
168
171
  return (
169
172
  <Element className={className} data-testid={testId}>
170
- {renderNodes(tree, actions, data, allowLinks)}
173
+ {renderNodes(tree, onAction, data, allowLinks)}
171
174
  </Element>
172
175
  );
173
176
  };
@@ -1,7 +1,5 @@
1
1
  import React from 'react';
2
2
 
3
- import type { LinkProps } from '../Link';
4
-
5
3
  export type MarkupNode =
6
4
  | { type: 'text'; content: string }
7
5
  | { type: 'newline' }
@@ -20,9 +18,20 @@ export type MarkupNode =
20
18
  children: MarkupNode[];
21
19
  };
22
20
 
23
- export type MarkupActionHandler = NonNullable<LinkProps['onClick']>;
21
+ /**
22
+ * Callback signature for the `onAction` prop. Receives the action name from
23
+ * `<link action="name">` when clicked.
24
+ */
25
+ export type MarkupActionHandler = (actionId: string) => void;
24
26
 
25
- export type MarkupActions = Record<string, MarkupActionHandler>;
27
+ /**
28
+ * Per-prop Markup configuration that host components can accept via a `markupOptions`
29
+ * prop keyed by their text prop names (e.g. `{ title?: MarkupTextOptions; description?: MarkupTextOptions }`).
30
+ */
31
+ export type MarkupTextOptions = {
32
+ data?: Record<string, string | number>;
33
+ allowLinks?: 'internal' | 'all' | 'none';
34
+ };
26
35
 
27
36
  export interface MarkupProps {
28
37
  /**
@@ -45,9 +54,10 @@ export interface MarkupProps {
45
54
  * */
46
55
  allowLinks?: 'internal' | 'all' | 'none';
47
56
  /**
48
- * Map of action names to click handlers for `<link action="name">` button-mode links.
57
+ * Callback invoked when a `<link action="name">` button-mode link is clicked.
58
+ * Receives the action name as argument.
49
59
  * */
50
- actions?: MarkupActions;
60
+ onAction?: MarkupActionHandler;
51
61
  /**
52
62
  * Wrapper element. Defaults to `div` when the parsed tree contains `<paragraph>` nodes,
53
63
  * `span` otherwise. Set explicitly to override auto-detection.
@@ -4,6 +4,66 @@ import { Meta, Source } from '@storybook/addon-docs/blocks';
4
4
 
5
5
  # Developer notes
6
6
 
7
+ ## Integrating Markup in host components
8
+
9
+ To integrate any component with Markup (e.g. `title`, `description`), follow this pattern:
10
+
11
+ 1. **Add `onMarkupAction`** — if the component is expected to accept actions, add a single callback prop that the
12
+ component forwards to every internal `<Markup onAction={onMarkupAction}>` instance. The name of this prop must
13
+ remain consistent across components.
14
+
15
+ 2. **Add `markupOptions`** — an optional prop keyed by the text prop names, carrying per-prop Markup configuration
16
+ (`data`, `allowLinks`). `MarkupTextOptions` type is exposed by the component.
17
+
18
+ <Source dark language="tsx" code={`
19
+ import Markup, { type MarkupActionHandler, type MarkupTextOptions } from '../Markup';
20
+
21
+ type MyComponentProps = {
22
+ title: ReactNode;
23
+ description?: ReactNode;
24
+ onMarkupAction?: MarkupActionHandler;
25
+ markupOptions?: {
26
+ title?: MarkupTextOptions;
27
+ description?: MarkupTextOptions;
28
+ };
29
+ };
30
+
31
+ function MyComponent({ title, description, onMarkupAction, markupOptions }: MyComponentProps) {
32
+ return (
33
+ <div>
34
+ <Markup onAction={onMarkupAction} {...markupOptions?.title}>
35
+ {title}
36
+ </Markup>
37
+
38
+ {description && (
39
+ <Markup onAction={onMarkupAction} {...markupOptions?.description}>
40
+ {description}
41
+ </Markup>
42
+ )}
43
+ </div>
44
+ );
45
+ }
46
+ `} />
47
+
48
+ Consumer usage:
49
+
50
+ <Source
51
+ dark
52
+ language="tsx"
53
+ code={`
54
+ <MyComponent
55
+ title='Something went wrong. <link action="retry">Try again</link>.'
56
+ description='Your balance is {{amount}}.'
57
+ onMarkupAction={(actionId) => {
58
+ if (actionId === 'retry') refetch();
59
+ }}
60
+ markupOptions={{
61
+ description: { data: { amount: '£100.00' }, allowLinks: 'all' }
62
+ }}
63
+ />
64
+ `}
65
+ />
66
+
7
67
  ## How parsing works
8
68
 
9
69
  The parser runs in two phases: **tokenise** then **parse**.
@@ -1,6 +1,5 @@
1
1
  import React, { useState } from 'react';
2
2
  import { Meta, StoryObj } from '@storybook/react-webpack5';
3
- import { action } from 'storybook/actions';
4
3
 
5
4
  import {
6
5
  createSandboxStory,
@@ -9,7 +8,6 @@ import {
9
8
  import { storySourceWithoutNoise } from '../../../.storybook/helpers';
10
9
 
11
10
  import { Markup } from '../Markup';
12
- import type { MarkupActionHandler } from '../Markup.types';
13
11
 
14
12
  export default {
15
13
  component: Markup,
@@ -58,8 +56,8 @@ export const WithLinks: Story = {
58
56
  };
59
57
 
60
58
  /**
61
- * Button-mode link using the `action` attribute. The handler is provided via the `actions` prop.
62
- * If the named action is missing from the map, the link degrades to plain text.
59
+ * Button-mode link using the `action` attribute. The handler is provided via the `onAction` prop.
60
+ * If `onAction` is not provided, the link degrades to plain text.
63
61
  * */
64
62
  export const WithLinksAsButtons = storySourceWithoutNoise<Story>({
65
63
  args: {
@@ -68,14 +66,15 @@ export const WithLinksAsButtons = storySourceWithoutNoise<Story>({
68
66
  render: function Render(args) {
69
67
  const [count, setCount] = useState(0);
70
68
 
71
- const increment: MarkupActionHandler = (event) => {
72
- action('Button event')(event);
73
- setCount((prev) => prev + 1);
69
+ const handleAction = (actionId: string) => {
70
+ if (actionId === 'increment') {
71
+ setCount((prev) => prev + 1);
72
+ }
74
73
  };
75
74
 
76
75
  return (
77
76
  <>
78
- <Markup actions={{ increment }}>{args.children}</Markup>
77
+ <Markup onAction={handleAction}>{args.children}</Markup>
79
78
  <output>{count}</output>
80
79
  </>
81
80
  );
@@ -1,2 +1,7 @@
1
1
  export { default } from './Markup';
2
- export type { MarkupActionHandler, MarkupActions, MarkupNode, MarkupProps } from './Markup.types';
2
+ export type {
3
+ MarkupActionHandler,
4
+ MarkupNode,
5
+ MarkupProps,
6
+ MarkupTextOptions,
7
+ } from './Markup.types';
@@ -1,4 +1,3 @@
1
- import { useTheme } from '@wise/components-theming';
2
1
  import { clsx } from 'clsx';
3
2
  import { useRef, useState, cloneElement, useEffect, isValidElement, useId } from 'react';
4
3
 
@@ -9,10 +8,14 @@ import { logActionRequired } from '../utilities';
9
8
 
10
9
  /** @deprecated Use `"top" | "bottom"` instead. */
11
10
  type PopoverPreferredPlacementDeprecated =
11
+ | `${Position.TOP_LEFT}`
12
+ | `${Position.TOP_RIGHT}`
12
13
  | `${Position.LEFT_TOP}`
13
14
  | `${Position.RIGHT_TOP}`
14
15
  | `${Position.BOTTOM_RIGHT}`
15
- | `${Position.BOTTOM_LEFT}`;
16
+ | `${Position.BOTTOM_LEFT}`
17
+ | `${Position.LEFT_BOTTOM}`
18
+ | `${Position.RIGHT_BOTTOM}`;
16
19
 
17
20
  export type PopoverPreferredPlacement =
18
21
  | `${Position.TOP}`
@@ -35,11 +38,15 @@ export interface PopoverProps {
35
38
 
36
39
  function resolvePlacement(preferredPlacement: PopoverPreferredPlacement) {
37
40
  switch (preferredPlacement) {
41
+ case 'top-left':
42
+ case 'top-right':
38
43
  case 'left-top':
39
44
  case 'right-top':
40
45
  return 'top';
41
46
  case 'bottom-left':
42
47
  case 'bottom-right':
48
+ case 'left-bottom':
49
+ case 'right-bottom':
43
50
  return 'bottom';
44
51
  default:
45
52
  return preferredPlacement;
@@ -1,9 +1,5 @@
1
- import { clsx } from 'clsx';
2
-
3
- import { Size } from '../../common';
4
- import { CloseButton } from '../../common/CloseButton';
5
- import FlowHeader from '../../common/FlowHeader';
6
1
  import Logo from '../../Logo';
2
+ import FlowNavigation from '../../FlowNavigation';
7
3
 
8
4
  /**
9
5
  * @deprecated Use `FlowNavigation` component instead, with `steps={[]}`
@@ -22,20 +18,5 @@ const defaultLogo = <Logo />;
22
18
  * @deprecated Use `FlowNavigation` component instead
23
19
  */
24
20
  export default function OverlayHeader({ avatar, onClose, logo = defaultLogo }: OverlayHeaderProps) {
25
- const closeButton = onClose && <CloseButton size={Size.LARGE} onClick={onClose} />;
26
- return (
27
- <div className="np-overlay-header d-flex justify-content-center">
28
- <FlowHeader
29
- className="np-overlay-header__content p-a-3"
30
- leftContent={logo}
31
- rightContent={
32
- <div className={clsx('d-flex', 'align-items-center', 'order-2')}>
33
- {avatar}
34
- {avatar && closeButton && <span className={clsx('m-x-1')} />}
35
- {closeButton}
36
- </div>
37
- }
38
- />
39
- </div>
40
- );
21
+ return <FlowNavigation {...{ avatar, onClose, logo }} />;
41
22
  }
@@ -76,4 +76,21 @@ describe('Panel', () => {
76
76
 
77
77
  const getPanel = () => document.querySelector('.np-panel--open');
78
78
  const getArrow = () => document.querySelector('.np-panel__arrow');
79
+
80
+ describe('aligned placements', () => {
81
+ it.each<[PanelProps['position'], string]>([
82
+ [Position.TOP_LEFT, 'top-start'],
83
+ [Position.TOP_RIGHT, 'top-end'],
84
+ [Position.RIGHT_TOP, 'right-start'],
85
+ [Position.RIGHT_BOTTOM, 'right-end'],
86
+ [Position.BOTTOM_RIGHT, 'bottom-end'],
87
+ [Position.BOTTOM_LEFT, 'bottom-start'],
88
+ [Position.LEFT_BOTTOM, 'left-end'],
89
+ [Position.LEFT_TOP, 'left-start'],
90
+ ])('converts %s to floating-ui placement %s', (position, expectedPlacement) => {
91
+ render(<Panel {...props} position={position} />);
92
+
93
+ expect(getPanel()).toHaveAttribute('data-popper-placement', expectedPlacement);
94
+ });
95
+ });
79
96
  });
@@ -12,15 +12,15 @@ import { clsx } from 'clsx';
12
12
  import {
13
13
  CSSProperties,
14
14
  HTMLAttributes,
15
- MutableRefObject,
16
15
  PropsWithChildren,
16
+ RefObject,
17
17
  SyntheticEvent,
18
18
  forwardRef,
19
19
  useContext,
20
20
  useRef,
21
21
  } from 'react';
22
22
 
23
- import { Position, PositionBottom, PositionLeft, PositionRight, PositionTop } from '..';
23
+ import { Position, type FloatingPlacement, positionToPlacement } from '..';
24
24
  import Dimmer from '../../Dimmer';
25
25
  import { OverlayIdContext } from '../../Provider/overlay/OverlayIdProvider';
26
26
 
@@ -29,11 +29,19 @@ const POPOVER_OFFSET = 16;
29
29
  // By default the flip positioning explores only the opposite alternative. So if left is passed and there's no enough space
30
30
  // the right one gets chosen. If there's no space on both sides popover goes back to the initially chosen one left.
31
31
  // This mapping forces popover to try the four available positions before going back to the initial chosen one.
32
- const fallbackPlacements: Record<string, Placement[]> = {
32
+ const fallbackPlacements: Record<Placement, Placement[]> = {
33
33
  [Position.TOP]: [Position.BOTTOM, Position.RIGHT, Position.LEFT],
34
34
  [Position.BOTTOM]: [Position.TOP, Position.RIGHT, Position.LEFT],
35
35
  [Position.LEFT]: [Position.RIGHT, Position.TOP, Position.BOTTOM],
36
36
  [Position.RIGHT]: [Position.LEFT, Position.TOP, Position.BOTTOM],
37
+ 'top-start': ['bottom-start', 'top-end', 'bottom-end'],
38
+ 'top-end': ['bottom-end', 'top-start', 'bottom-start'],
39
+ 'bottom-start': ['top-start', 'bottom-end', 'top-end'],
40
+ 'bottom-end': ['top-end', 'bottom-start', 'top-start'],
41
+ 'left-start': ['right-start', 'left-end', 'right-end'],
42
+ 'left-end': ['right-end', 'left-start', 'right-start'],
43
+ 'right-start': ['left-start', 'right-end', 'left-end'],
44
+ 'right-end': ['left-end', 'right-start', 'left-start'],
37
45
  };
38
46
 
39
47
  export type PanelProps = PropsWithChildren<{
@@ -42,8 +50,8 @@ export type PanelProps = PropsWithChildren<{
42
50
  altAxis?: boolean;
43
51
  open?: boolean;
44
52
  onClose?: (event: Event | SyntheticEvent) => void;
45
- position?: PositionBottom | PositionLeft | PositionRight | PositionTop;
46
- anchorRef: MutableRefObject<Element | null>;
53
+ position?: FloatingPlacement;
54
+ anchorRef: RefObject<Element | null>;
47
55
  anchorWidth?: boolean;
48
56
  considerHeight?: boolean;
49
57
  }> &
@@ -66,6 +74,7 @@ const Panel = forwardRef<HTMLDivElement, PanelProps>(function Panel(
66
74
  reference,
67
75
  ) {
68
76
  const arrowRef = useRef<HTMLDivElement | null>(null);
77
+ const floatingPlacement = positionToPlacement(position);
69
78
 
70
79
  const middleware = [];
71
80
 
@@ -73,10 +82,10 @@ const Panel = forwardRef<HTMLDivElement, PanelProps>(function Panel(
73
82
  middleware.push(offset(POPOVER_OFFSET));
74
83
  }
75
84
 
76
- if (flip && fallbackPlacements[position]) {
85
+ if (flip && fallbackPlacements[floatingPlacement]) {
77
86
  middleware.push(
78
87
  flipMiddleware({
79
- fallbackPlacements: fallbackPlacements[position],
88
+ fallbackPlacements: fallbackPlacements[floatingPlacement],
80
89
  }),
81
90
  );
82
91
  }
@@ -104,7 +113,7 @@ const Panel = forwardRef<HTMLDivElement, PanelProps>(function Panel(
104
113
  }
105
114
 
106
115
  const { refs, floatingStyles, placement, middlewareData } = useFloating({
107
- placement: position,
116
+ placement: floatingPlacement,
108
117
  middleware,
109
118
  elements: {
110
119
  reference: anchorRef.current,
@@ -0,0 +1,182 @@
1
+ import { Meta, StoryObj } from '@storybook/react-webpack5';
2
+ import { screen, userEvent, within } from 'storybook/test';
3
+ import { useRef, useState } from 'react';
4
+
5
+ import Button from '../../../Button';
6
+ import { Position } from '../../PropsValues/position';
7
+ import Panel from '../Panel';
8
+
9
+ export default {
10
+ component: Panel,
11
+ title: 'Dialogs/Panel',
12
+ parameters: {
13
+ docs: {
14
+ description: {
15
+ component:
16
+ 'Panel is a low-level floating component used by Popover and ResponsivePanel. It supports all floating-ui placements.',
17
+ },
18
+ },
19
+ chromatic: {
20
+ delay: 500,
21
+ },
22
+ },
23
+ } satisfies Meta<typeof Panel>;
24
+
25
+ type Story = StoryObj<typeof Panel>;
26
+
27
+ const PanelDemo = ({
28
+ position,
29
+ label,
30
+ }: {
31
+ position: Parameters<typeof Panel>[0]['position'];
32
+ label: string;
33
+ }) => {
34
+ const anchorRef = useRef<HTMLButtonElement>(null);
35
+ const [open, setOpen] = useState(false);
36
+
37
+ return (
38
+ <div style={{ display: 'inline-block' }}>
39
+ <Button ref={anchorRef} onClick={() => setOpen((prev) => !prev)}>
40
+ {label}
41
+ </Button>
42
+ <Panel
43
+ open={open}
44
+ position={position}
45
+ anchorRef={anchorRef}
46
+ onClose={() => setOpen(false)}
47
+ aria-label={`Panel positioned ${label}`}
48
+ >
49
+ <div style={{ padding: '16px', maxWidth: '200px' }}>
50
+ Panel content positioned at <strong>{label}</strong>
51
+ </div>
52
+ </Panel>
53
+ </div>
54
+ );
55
+ };
56
+
57
+ export const AllPositions: Story = {
58
+ parameters: {
59
+ chromatic: { disableSnapshot: true },
60
+ },
61
+ render: () => (
62
+ <div
63
+ style={{
64
+ display: 'flex',
65
+ flexWrap: 'wrap',
66
+ gap: '48px',
67
+ justifyContent: 'center',
68
+ alignItems: 'center',
69
+ padding: '120px 80px',
70
+ }}
71
+ >
72
+ <PanelDemo position={Position.TOP} label="top" />
73
+ <PanelDemo position={Position.TOP_LEFT} label="top-left" />
74
+ <PanelDemo position={Position.TOP_RIGHT} label="top-right" />
75
+ <PanelDemo position={Position.RIGHT} label="right" />
76
+ <PanelDemo position={Position.RIGHT_TOP} label="right-top" />
77
+ <PanelDemo position={Position.RIGHT_BOTTOM} label="right-bottom" />
78
+ <PanelDemo position={Position.BOTTOM} label="bottom" />
79
+ <PanelDemo position={Position.BOTTOM_LEFT} label="bottom-left" />
80
+ <PanelDemo position={Position.BOTTOM_RIGHT} label="bottom-right" />
81
+ <PanelDemo position={Position.LEFT} label="left" />
82
+ <PanelDemo position={Position.LEFT_TOP} label="left-top" />
83
+ <PanelDemo position={Position.LEFT_BOTTOM} label="left-bottom" />
84
+ </div>
85
+ ),
86
+ };
87
+
88
+ const SinglePanelDemo = ({
89
+ position,
90
+ label,
91
+ }: {
92
+ position: Parameters<typeof Panel>[0]['position'];
93
+ label: string;
94
+ }) => {
95
+ const anchorRef = useRef<HTMLButtonElement>(null);
96
+ const [open, setOpen] = useState(false);
97
+
98
+ return (
99
+ <div style={{ padding: '120px 250px' }}>
100
+ <Button ref={anchorRef} onClick={() => setOpen((prev) => !prev)}>
101
+ {label}
102
+ </Button>
103
+ <Panel
104
+ open={open}
105
+ position={position}
106
+ anchorRef={anchorRef}
107
+ onClose={() => setOpen(false)}
108
+ aria-label={`Panel positioned ${label}`}
109
+ >
110
+ <div style={{ padding: '16px', maxWidth: '200px' }}>
111
+ Panel content positioned at <strong>{label}</strong>
112
+ </div>
113
+ </Panel>
114
+ </div>
115
+ );
116
+ };
117
+
118
+ const openPanelPlay = async ({ canvasElement }: { canvasElement: HTMLElement }) => {
119
+ const canvas = within(canvasElement);
120
+ await userEvent.click(canvas.getByRole('button'));
121
+ await screen.findByText(/Panel content positioned at/i);
122
+ };
123
+
124
+ export const Top: Story = {
125
+ render: () => <SinglePanelDemo position={Position.TOP} label="top" />,
126
+ play: openPanelPlay,
127
+ };
128
+
129
+ export const Right: Story = {
130
+ render: () => <SinglePanelDemo position={Position.RIGHT} label="right" />,
131
+ play: openPanelPlay,
132
+ };
133
+
134
+ export const Bottom: Story = {
135
+ render: () => <SinglePanelDemo position={Position.BOTTOM} label="bottom" />,
136
+ play: openPanelPlay,
137
+ };
138
+
139
+ export const Left: Story = {
140
+ render: () => <SinglePanelDemo position={Position.LEFT} label="left" />,
141
+ play: openPanelPlay,
142
+ };
143
+
144
+ export const TopLeft: Story = {
145
+ render: () => <SinglePanelDemo position={Position.TOP_LEFT} label="top-left" />,
146
+ play: openPanelPlay,
147
+ };
148
+
149
+ export const TopRight: Story = {
150
+ render: () => <SinglePanelDemo position={Position.TOP_RIGHT} label="top-right" />,
151
+ play: openPanelPlay,
152
+ };
153
+
154
+ export const RightTop: Story = {
155
+ render: () => <SinglePanelDemo position={Position.RIGHT_TOP} label="right-top" />,
156
+ play: openPanelPlay,
157
+ };
158
+
159
+ export const RightBottom: Story = {
160
+ render: () => <SinglePanelDemo position={Position.RIGHT_BOTTOM} label="right-bottom" />,
161
+ play: openPanelPlay,
162
+ };
163
+
164
+ export const BottomRight: Story = {
165
+ render: () => <SinglePanelDemo position={Position.BOTTOM_RIGHT} label="bottom-right" />,
166
+ play: openPanelPlay,
167
+ };
168
+
169
+ export const BottomLeft: Story = {
170
+ render: () => <SinglePanelDemo position={Position.BOTTOM_LEFT} label="bottom-left" />,
171
+ play: openPanelPlay,
172
+ };
173
+
174
+ export const LeftBottom: Story = {
175
+ render: () => <SinglePanelDemo position={Position.LEFT_BOTTOM} label="left-bottom" />,
176
+ play: openPanelPlay,
177
+ };
178
+
179
+ export const LeftTop: Story = {
180
+ render: () => <SinglePanelDemo position={Position.LEFT_TOP} label="left-top" />,
181
+ play: openPanelPlay,
182
+ };