@transferwise/components 0.0.0-experimental-77e4e3a → 0.0.0-experimental-0657ea4

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 (50) hide show
  1. package/build/ExpressiveMoneyInput/Chevron/Chevron.js +10 -2
  2. package/build/ExpressiveMoneyInput/Chevron/Chevron.js.map +1 -1
  3. package/build/ExpressiveMoneyInput/Chevron/Chevron.mjs +11 -3
  4. package/build/ExpressiveMoneyInput/Chevron/Chevron.mjs.map +1 -1
  5. package/build/ListItem/ListItem.js +5 -5
  6. package/build/ListItem/ListItem.js.map +1 -1
  7. package/build/ListItem/ListItem.mjs +5 -5
  8. package/build/ListItem/ListItem.mjs.map +1 -1
  9. package/build/common/Panel/Panel.js +13 -25
  10. package/build/common/Panel/Panel.js.map +1 -1
  11. package/build/common/Panel/Panel.mjs +10 -22
  12. package/build/common/Panel/Panel.mjs.map +1 -1
  13. package/build/common/Panel/Panel.positionUtils.js +68 -0
  14. package/build/common/Panel/Panel.positionUtils.js.map +1 -0
  15. package/build/common/Panel/Panel.positionUtils.mjs +63 -0
  16. package/build/common/Panel/Panel.positionUtils.mjs.map +1 -0
  17. package/build/common/PropsValues/position.js +0 -25
  18. package/build/common/PropsValues/position.js.map +1 -1
  19. package/build/common/PropsValues/position.mjs +1 -24
  20. package/build/common/PropsValues/position.mjs.map +1 -1
  21. package/build/main.css +12 -4
  22. package/build/styles/common/Panel/Panel.css +12 -4
  23. package/build/styles/main.css +12 -4
  24. package/build/types/Body/Body.d.ts +2 -2
  25. package/build/types/ExpressiveMoneyInput/Chevron/Chevron.d.ts.map +1 -1
  26. package/build/types/IconButton/IconButton.d.ts +1 -1
  27. package/build/types/MoneyInput/MoneyInput.d.ts +1 -1
  28. package/build/types/MoneyInput/MoneyInput.d.ts.map +1 -1
  29. package/build/types/Title/Title.d.ts +2 -2
  30. package/build/types/Upload/Steps/UploadImageStep/UploadImageStep.d.ts +1 -1
  31. package/build/types/UploadInput/UploadItem/UploadItemLink.d.ts +1 -1
  32. package/build/types/common/Panel/Panel.d.ts +1 -1
  33. package/build/types/common/Panel/Panel.d.ts.map +1 -1
  34. package/build/types/common/Panel/Panel.positionUtils.d.ts +7 -0
  35. package/build/types/common/Panel/Panel.positionUtils.d.ts.map +1 -0
  36. package/build/types/common/PropsValues/position.d.ts +0 -13
  37. package/build/types/common/PropsValues/position.d.ts.map +1 -1
  38. package/build/types/common/ResponsivePanel/ResponsivePanel.d.ts +1 -1
  39. package/package.json +2 -2
  40. package/src/ExpressiveMoneyInput/Chevron/Chevron.tsx +8 -3
  41. package/src/ListItem/ListItem.tsx +5 -5
  42. package/src/ListItem/_stories/ListItem.layout.test.story.tsx +0 -23
  43. package/src/common/Panel/Panel.css +12 -4
  44. package/src/common/Panel/Panel.less +27 -9
  45. package/src/common/Panel/Panel.positionUtils.ts +79 -0
  46. package/src/common/Panel/Panel.test.tsx +40 -0
  47. package/src/common/Panel/Panel.tsx +13 -24
  48. package/src/common/Panel/_stories/Panel.rtl.test.story.tsx +110 -0
  49. package/src/common/PropsValues/position.ts +0 -50
  50. package/src/main.css +12 -4
@@ -1,4 +1,3 @@
1
- import type { Placement } from '@floating-ui/react';
2
1
  export declare enum Position {
3
2
  BOTTOM = "bottom",
4
3
  BOTTOM_LEFT = "bottom-left",
@@ -19,16 +18,4 @@ export type PositionTop = 'top';
19
18
  export type PositionRight = 'right';
20
19
  export type PositionBottom = 'bottom';
21
20
  export type PositionLeft = 'left';
22
- export type PositionTopLeft = 'top-left';
23
- export type PositionTopRight = 'top-right';
24
- export type PositionBottomLeft = 'bottom-left';
25
- export type PositionBottomRight = 'bottom-right';
26
- export type PositionLeftTop = 'left-top';
27
- export type PositionLeftBottom = 'left-bottom';
28
- export type PositionRightTop = 'right-top';
29
- export type PositionRightBottom = 'right-bottom';
30
- /** Neptune position values that can be converted to floating-ui Placement */
31
- export type FloatingPlacement = PositionTop | PositionRight | PositionBottom | PositionLeft | PositionTopLeft | PositionTopRight | PositionRightTop | PositionRightBottom | PositionBottomLeft | PositionBottomRight | PositionLeftTop | PositionLeftBottom;
32
- /** Maps Neptune position values to floating-ui Placement */
33
- export declare const positionToPlacement: (position: FloatingPlacement) => Placement;
34
21
  //# sourceMappingURL=position.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"position.d.ts","sourceRoot":"","sources":["../../../../src/common/PropsValues/position.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAEpD,oBAAY,QAAQ;IAClB,MAAM,WAAW;IACjB,WAAW,gBAAgB;IAC3B,YAAY,iBAAiB;IAC7B,MAAM,WAAW;IACjB,IAAI,SAAS;IACb,WAAW,gBAAgB;IAC3B,QAAQ,aAAa;IACrB,KAAK,UAAU;IACf,YAAY,iBAAiB;IAC7B,SAAS,cAAc;IACvB,GAAG,QAAQ;IACX,QAAQ,aAAa;IACrB,SAAS,cAAc;CACxB;AAED,MAAM,MAAM,cAAc,GAAG,QAAQ,CAAC;AACtC,MAAM,MAAM,WAAW,GAAG,KAAK,CAAC;AAChC,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC;AACpC,MAAM,MAAM,cAAc,GAAG,QAAQ,CAAC;AACtC,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC;AAElC,MAAM,MAAM,eAAe,GAAG,UAAU,CAAC;AACzC,MAAM,MAAM,gBAAgB,GAAG,WAAW,CAAC;AAC3C,MAAM,MAAM,kBAAkB,GAAG,aAAa,CAAC;AAC/C,MAAM,MAAM,mBAAmB,GAAG,cAAc,CAAC;AACjD,MAAM,MAAM,eAAe,GAAG,UAAU,CAAC;AACzC,MAAM,MAAM,kBAAkB,GAAG,aAAa,CAAC;AAC/C,MAAM,MAAM,gBAAgB,GAAG,WAAW,CAAC;AAC3C,MAAM,MAAM,mBAAmB,GAAG,cAAc,CAAC;AAEjD,6EAA6E;AAC7E,MAAM,MAAM,iBAAiB,GACzB,WAAW,GACX,aAAa,GACb,cAAc,GACd,YAAY,GACZ,eAAe,GACf,gBAAgB,GAChB,gBAAgB,GAChB,mBAAmB,GACnB,kBAAkB,GAClB,mBAAmB,GACnB,eAAe,GACf,kBAAkB,CAAC;AAEvB,4DAA4D;AAC5D,eAAO,MAAM,mBAAmB,GAAI,UAAU,iBAAiB,KAAG,SAqBjE,CAAC"}
1
+ {"version":3,"file":"position.d.ts","sourceRoot":"","sources":["../../../../src/common/PropsValues/position.ts"],"names":[],"mappings":"AAAA,oBAAY,QAAQ;IAClB,MAAM,WAAW;IACjB,WAAW,gBAAgB;IAC3B,YAAY,iBAAiB;IAC7B,MAAM,WAAW;IACjB,IAAI,SAAS;IACb,WAAW,gBAAgB;IAC3B,QAAQ,aAAa;IACrB,KAAK,UAAU;IACf,YAAY,iBAAiB;IAC7B,SAAS,cAAc;IACvB,GAAG,QAAQ;IACX,QAAQ,aAAa;IACrB,SAAS,cAAc;CACxB;AAED,MAAM,MAAM,cAAc,GAAG,QAAQ,CAAC;AACtC,MAAM,MAAM,WAAW,GAAG,KAAK,CAAC;AAChC,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC;AACpC,MAAM,MAAM,cAAc,GAAG,QAAQ,CAAC;AACtC,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC"}
@@ -4,7 +4,7 @@ declare const ResponsivePanel: import("react").ForwardRefExoticComponent<{
4
4
  altAxis?: boolean;
5
5
  open?: boolean;
6
6
  onClose?: (event: Event | import("react").SyntheticEvent) => void;
7
- position?: import("..").FloatingPlacement;
7
+ position?: import("../Panel/Panel.positionUtils").FloatingPlacement;
8
8
  anchorRef: import("react").RefObject<Element | null>;
9
9
  anchorWidth?: boolean;
10
10
  considerHeight?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@transferwise/components",
3
- "version": "0.0.0-experimental-77e4e3a",
3
+ "version": "0.0.0-experimental-0657ea4",
4
4
  "description": "Neptune React components",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -91,7 +91,7 @@
91
91
  "storybook": "^10.5.4",
92
92
  "storybook-addon-tag-badges": "^3.1.0",
93
93
  "storybook-addon-test-codegen": "^3.0.4",
94
- "@transferwise/less-config": "0.0.0-experimental-77e4e3a",
94
+ "@transferwise/less-config": "3.2.0",
95
95
  "@wise/components-theming": "1.10.3",
96
96
  "@wise/wds-configs": "0.0.0"
97
97
  },
@@ -1,12 +1,17 @@
1
- import { ChevronLeft } from '@transferwise/icons';
1
+ import { ChevronLeft, ChevronRight } from '@transferwise/icons';
2
2
  import { clsx } from 'clsx';
3
3
  import { motion } from 'motion/react';
4
4
 
5
+ import { useDirection } from '../../common/hooks';
6
+
5
7
  interface Props {
6
8
  shouldShow: boolean;
7
9
  }
8
10
 
9
11
  export const Chevron = ({ shouldShow = true }: Props) => {
12
+ const { isRTL } = useDirection();
13
+ const Icon = isRTL ? ChevronRight : ChevronLeft;
14
+
10
15
  return (
11
16
  <div
12
17
  className={clsx(
@@ -17,7 +22,7 @@ export const Chevron = ({ shouldShow = true }: Props) => {
17
22
  >
18
23
  <motion.div
19
24
  animate={{
20
- x: [12, 0, 0, -12],
25
+ x: isRTL ? [-12, 0, 0, 12] : [12, 0, 0, -12],
21
26
  opacity: [0, 1, 1, 0],
22
27
  }}
23
28
  transition={{
@@ -28,7 +33,7 @@ export const Chevron = ({ shouldShow = true }: Props) => {
28
33
  repeatType: 'loop',
29
34
  }}
30
35
  >
31
- <ChevronLeft />
36
+ <Icon />
32
37
  </motion.div>
33
38
  </div>
34
39
  );
@@ -182,24 +182,24 @@ export const ListItem = ({
182
182
  const getFeatureClassName = () => {
183
183
  const partials = [];
184
184
  const hasMedia = Boolean(media);
185
- const hasControl = Boolean(control) && !isNavigationIndicatorHidden;
185
+ const hasVisibleControl = Boolean(control) && !isNavigationIndicatorHidden;
186
186
  const hasInfo = Boolean(additionalInfo);
187
187
  const hasPrompt = Boolean(prompt) || (disabled && Boolean(disabledPromptMessage));
188
188
 
189
189
  /* eslint-disable functional/immutable-data */
190
- if (hasMedia && hasControl) {
190
+ if (hasMedia && hasVisibleControl) {
191
191
  partials.push('wds-list-item-hasMedia-hasControl');
192
192
  }
193
193
 
194
- if (hasMedia && !hasControl) {
194
+ if (hasMedia && !hasVisibleControl) {
195
195
  partials.push('wds-list-item-hasMedia-noControl');
196
196
  }
197
197
 
198
- if (!hasMedia && hasControl) {
198
+ if (!hasMedia && hasVisibleControl) {
199
199
  partials.push('wds-list-item-noMedia-hasControl');
200
200
  }
201
201
 
202
- if (!hasMedia && !hasControl) {
202
+ if (!hasMedia && !hasVisibleControl) {
203
203
  partials.push('wds-list-item-noMedia-noControl');
204
204
  }
205
205
 
@@ -226,29 +226,6 @@ export const ImageAlignment: StoryObj<PreviewStoryArgs> = {
226
226
  },
227
227
  };
228
228
 
229
- // Regression: when Navigation has hideIndicator + href, the control column should be
230
- // removed from the grid so no phantom gap appears on the right side of the item.
231
- export const NavigationHideIndicator: Story = {
232
- render: () => (
233
- <List>
234
- <ListItem
235
- title="Stocks"
236
- valueTitle="− 6.70 GBP"
237
- valueSubtitle="All time"
238
- media={<ListItem.AvatarView>{null}</ListItem.AvatarView>}
239
- control={<ListItem.Navigation href="/test" hideIndicator />}
240
- />
241
- <ListItem
242
- title="With chevron (baseline)"
243
- valueTitle="100 GBP"
244
- valueSubtitle="All time"
245
- media={<ListItem.AvatarView>{null}</ListItem.AvatarView>}
246
- control={<ListItem.Navigation href="/test" />}
247
- />
248
- </List>
249
- ),
250
- };
251
-
252
229
  // Our container queries left 1px gap between breakpoint definitions.
253
230
  // This test makes sure that LI placed in a parent of {width} still
254
231
  // works fine, where: {breakpoint} < {width} < {breakpoint} + 1px
@@ -39,27 +39,35 @@
39
39
  .np-panel[data-popper-placement^="top"] > .np-panel__content {
40
40
  transform: translateY(10px) scale(0.95);
41
41
  }
42
- .np-panel[data-popper-placement^="top"] > .np-panel__content .np-panel__arrow {
42
+ .np-panel[data-popper-placement^="top"] .np-panel__arrow {
43
43
  inset-block-end: -6px;
44
44
  }
45
45
  .np-panel[data-popper-placement^="right"] > .np-panel__content {
46
46
  transform: translateX(-10px) scale(0.95);
47
47
  }
48
- .np-panel[data-popper-placement^="right"] > .np-panel__content .np-panel__arrow {
48
+ .np-panel[data-popper-placement^="right"] .np-panel__arrow {
49
49
  inset-inline-start: -6px;
50
50
  }
51
+ [dir="rtl"] .np-panel[data-popper-placement^="right"] .np-panel__arrow {
52
+ inset-inline-start: unset;
53
+ inset-inline-end: -6px;
54
+ }
51
55
  .np-panel[data-popper-placement^="bottom"] > .np-panel__content {
52
56
  transform: translateY(-10px) scale(0.95);
53
57
  }
54
- .np-panel[data-popper-placement^="bottom"] > .np-panel__content .np-panel__arrow {
58
+ .np-panel[data-popper-placement^="bottom"] .np-panel__arrow {
55
59
  inset-block-start: -6px;
56
60
  }
57
61
  .np-panel[data-popper-placement^="left"] > .np-panel__content {
58
62
  transform: translateX(10px) scale(0.95);
59
63
  }
60
- .np-panel[data-popper-placement^="left"] > .np-panel__content .np-panel__arrow {
64
+ .np-panel[data-popper-placement^="left"] .np-panel__arrow {
61
65
  inset-inline-end: -6px;
62
66
  }
67
+ [dir="rtl"] .np-panel[data-popper-placement^="left"] .np-panel__arrow {
68
+ inset-inline-end: unset;
69
+ inset-inline-start: -6px;
70
+ }
63
71
  .np-theme-personal .np-panel__content {
64
72
  border-radius: 16px;
65
73
  border-radius: var(--radius-medium);
@@ -11,7 +11,7 @@
11
11
  opacity 0.2s cubic-bezier(0.6, 0.2, 0.1, 1),
12
12
  transform 0.2s cubic-bezier(0.6, 0.2, 0.1, 1),
13
13
  visibility 0s linear 0.2s;
14
-
14
+
15
15
  .np-theme-personal & {
16
16
  border-radius: var(--radius-small);
17
17
  }
@@ -45,35 +45,53 @@
45
45
  background: var(--color-background-elevated);
46
46
  }
47
47
 
48
- &[data-popper-placement^="top"] > &__content {
49
- transform: translateY(10px) scale(0.95);
48
+ &[data-popper-placement^="top"] {
49
+ > .np-panel__content {
50
+ transform: translateY(10px) scale(0.95);
51
+ }
50
52
 
51
53
  .np-panel__arrow {
52
54
  bottom: -6px;
53
55
  }
54
56
  }
55
57
 
56
- &[data-popper-placement^="right"] > &__content {
57
- transform: translateX(-10px) scale(0.95);
58
+ &[data-popper-placement^="right"] {
59
+ > .np-panel__content {
60
+ transform: translateX(-10px) scale(0.95);
61
+ }
58
62
 
59
63
  .np-panel__arrow {
60
64
  left: -6px;
65
+
66
+ [dir="rtl"] & {
67
+ left: unset;
68
+ right: -6px;
69
+ }
61
70
  }
62
71
  }
63
72
 
64
- &[data-popper-placement^="bottom"] > &__content {
65
- transform: translateY(-10px) scale(0.95);
73
+ &[data-popper-placement^="bottom"] {
74
+ > .np-panel__content {
75
+ transform: translateY(-10px) scale(0.95);
76
+ }
66
77
 
67
78
  .np-panel__arrow {
68
79
  top: -6px;
69
80
  }
70
81
  }
71
82
 
72
- &[data-popper-placement^="left"] > &__content {
73
- transform: translateX(10px) scale(0.95);
83
+ &[data-popper-placement^="left"] {
84
+ > .np-panel__content {
85
+ transform: translateX(10px) scale(0.95);
86
+ }
74
87
 
75
88
  .np-panel__arrow {
76
89
  right: -6px;
90
+
91
+ [dir="rtl"] & {
92
+ right: unset;
93
+ left: -6px;
94
+ }
77
95
  }
78
96
  }
79
97
  }
@@ -0,0 +1,79 @@
1
+ import type { Placement } from '@floating-ui/react';
2
+
3
+ export type FloatingPlacement =
4
+ | 'top'
5
+ | 'right'
6
+ | 'bottom'
7
+ | 'left'
8
+ | 'top-left'
9
+ | 'top-right'
10
+ | 'right-top'
11
+ | 'right-bottom'
12
+ | 'bottom-left'
13
+ | 'bottom-right'
14
+ | 'left-top'
15
+ | 'left-bottom';
16
+
17
+ export const POPOVER_OFFSET = 16;
18
+
19
+ // By default the flip positioning explores only the opposite alternative. So if left is passed and there's no enough space
20
+ // the right one gets chosen. If there's no space on both sides popover goes back to the initially chosen one left.
21
+ // This mapping forces popover to try the four available positions before going back to the initial chosen one.
22
+ export const fallbackPlacements: Record<Placement, Placement[]> = {
23
+ top: ['bottom', 'right', 'left'],
24
+ bottom: ['top', 'right', 'left'],
25
+ left: ['right', 'top', 'bottom'],
26
+ right: ['left', 'top', 'bottom'],
27
+ 'top-start': ['bottom-start', 'top-end', 'bottom-end'],
28
+ 'top-end': ['bottom-end', 'top-start', 'bottom-start'],
29
+ 'bottom-start': ['top-start', 'bottom-end', 'top-end'],
30
+ 'bottom-end': ['top-end', 'bottom-start', 'top-start'],
31
+ 'left-start': ['right-start', 'left-end', 'right-end'],
32
+ 'left-end': ['right-end', 'left-start', 'right-start'],
33
+ 'right-start': ['left-start', 'right-end', 'left-end'],
34
+ 'right-end': ['left-end', 'right-start', 'left-start'],
35
+ };
36
+
37
+ export const positionToPlacement = (position: FloatingPlacement): Placement => {
38
+ switch (position) {
39
+ case 'top-left':
40
+ return 'top-start';
41
+ case 'top-right':
42
+ return 'top-end';
43
+ case 'bottom-left':
44
+ return 'bottom-start';
45
+ case 'bottom-right':
46
+ return 'bottom-end';
47
+ case 'left-top':
48
+ return 'left-start';
49
+ case 'left-bottom':
50
+ return 'left-end';
51
+ case 'right-top':
52
+ return 'right-start';
53
+ case 'right-bottom':
54
+ return 'right-end';
55
+ default:
56
+ return position;
57
+ }
58
+ };
59
+
60
+ export const mirrorPositionForRtl = (position: FloatingPlacement): FloatingPlacement => {
61
+ switch (position) {
62
+ case 'left':
63
+ return 'right';
64
+ case 'right':
65
+ return 'left';
66
+ case 'left-top':
67
+ return 'right-top';
68
+ case 'left-bottom':
69
+ return 'right-bottom';
70
+ case 'right-top':
71
+ return 'left-top';
72
+ case 'right-bottom':
73
+ return 'left-bottom';
74
+ default:
75
+ // top-left/top-right/bottom-left/bottom-right map to start/end via
76
+ // positionToPlacement, and floating-ui already flips start/end in RTL.
77
+ return position;
78
+ }
79
+ };
@@ -1,6 +1,7 @@
1
1
  import { createRef } from 'react';
2
2
 
3
3
  import { Position } from '..';
4
+ import { DirectionProvider } from '../../Provider/Direction/DirectionProvider';
4
5
  import { render } from '../../test-utils';
5
6
 
6
7
  import Panel from './Panel';
@@ -93,4 +94,43 @@ describe('Panel', () => {
93
94
  expect(getPanel()).toHaveAttribute('data-popper-placement', expectedPlacement);
94
95
  });
95
96
  });
97
+
98
+ describe('RTL position mirroring', () => {
99
+ it.each<[PanelProps['position'], string]>([
100
+ [Position.LEFT, 'right'],
101
+ [Position.RIGHT, 'left'],
102
+ [Position.LEFT_TOP, 'right-start'],
103
+ [Position.LEFT_BOTTOM, 'right-end'],
104
+ [Position.RIGHT_TOP, 'left-start'],
105
+ [Position.RIGHT_BOTTOM, 'left-end'],
106
+ ])('mirrors %s in RTL to floating-ui placement %s', (position, expectedPlacement) => {
107
+ render(
108
+ <DirectionProvider direction="rtl">
109
+ <Panel {...props} position={position} />
110
+ </DirectionProvider>,
111
+ );
112
+
113
+ expect(getPanel()).toHaveAttribute('data-popper-placement', expectedPlacement);
114
+ });
115
+
116
+ it.each<[PanelProps['position'], string]>([
117
+ [Position.TOP, 'top'],
118
+ [Position.BOTTOM, 'bottom'],
119
+ [Position.TOP_LEFT, 'top-start'],
120
+ [Position.TOP_RIGHT, 'top-end'],
121
+ [Position.BOTTOM_LEFT, 'bottom-start'],
122
+ [Position.BOTTOM_RIGHT, 'bottom-end'],
123
+ ])(
124
+ 'does not mirror %s in RTL (floating-ui handles start/end flip)',
125
+ (position, expectedPlacement) => {
126
+ render(
127
+ <DirectionProvider direction="rtl">
128
+ <Panel {...props} position={position} />
129
+ </DirectionProvider>,
130
+ );
131
+
132
+ expect(getPanel()).toHaveAttribute('data-popper-placement', expectedPlacement);
133
+ },
134
+ );
135
+ });
96
136
  });
@@ -3,7 +3,6 @@ import {
3
3
  autoUpdate,
4
4
  flip as flipMiddleware,
5
5
  offset,
6
- type Placement,
7
6
  shift,
8
7
  size as sizeMiddleware,
9
8
  useFloating,
@@ -20,29 +19,17 @@ import {
20
19
  useRef,
21
20
  } from 'react';
22
21
 
23
- import { Position, type FloatingPlacement, positionToPlacement } from '..';
24
- import Dimmer from '../../Dimmer';
25
22
  import { OverlayIdContext } from '../../Provider/overlay/OverlayIdProvider';
26
-
27
- const POPOVER_OFFSET = 16;
28
-
29
- // By default the flip positioning explores only the opposite alternative. So if left is passed and there's no enough space
30
- // the right one gets chosen. If there's no space on both sides popover goes back to the initially chosen one left.
31
- // This mapping forces popover to try the four available positions before going back to the initial chosen one.
32
- const fallbackPlacements: Record<Placement, Placement[]> = {
33
- [Position.TOP]: [Position.BOTTOM, Position.RIGHT, Position.LEFT],
34
- [Position.BOTTOM]: [Position.TOP, Position.RIGHT, Position.LEFT],
35
- [Position.LEFT]: [Position.RIGHT, Position.TOP, Position.BOTTOM],
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'],
45
- };
23
+ import Dimmer from '../../Dimmer';
24
+ import useDirection from '../hooks/useDirection';
25
+ import { Position } from '..';
26
+ import {
27
+ type FloatingPlacement,
28
+ POPOVER_OFFSET,
29
+ fallbackPlacements,
30
+ mirrorPositionForRtl,
31
+ positionToPlacement,
32
+ } from './Panel.positionUtils';
46
33
 
47
34
  export type PanelProps = PropsWithChildren<{
48
35
  arrow?: boolean;
@@ -74,7 +61,9 @@ const Panel = forwardRef<HTMLDivElement, PanelProps>(function Panel(
74
61
  reference,
75
62
  ) {
76
63
  const arrowRef = useRef<HTMLDivElement | null>(null);
77
- const floatingPlacement = positionToPlacement(position);
64
+ const { isRTL } = useDirection();
65
+ const resolvedPosition = isRTL ? mirrorPositionForRtl(position) : position;
66
+ const floatingPlacement = positionToPlacement(resolvedPosition);
78
67
 
79
68
  const middleware = [];
80
69
 
@@ -0,0 +1,110 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-webpack5';
2
+ import { useRef } from 'react';
3
+ import { withRTLDiff } from '../../../../.storybook/decorators/RtlDiffDecorator';
4
+ import Button from '../../../Button';
5
+ import { Position } from '../../PropsValues/position';
6
+ import Panel from '../Panel';
7
+
8
+ export default {
9
+ component: Panel,
10
+ title: 'Dialogs/Panel/Tests/RTL Diff',
11
+ tags: ['!autodocs', '!manifest'],
12
+ } satisfies Meta<typeof Panel>;
13
+
14
+ type Story = StoryObj<typeof Panel>;
15
+
16
+ function PanelStory({
17
+ position,
18
+ label,
19
+ }: {
20
+ position: Parameters<typeof Panel>[0]['position'];
21
+ label: string;
22
+ }) {
23
+ const anchorRef = useRef<HTMLButtonElement>(null);
24
+ const margin =
25
+ position?.startsWith('bottom') || position?.startsWith('top') ? '3.5rem 0' : '2.5rem 0';
26
+
27
+ return (
28
+ <>
29
+ <Button ref={anchorRef} style={{ margin }}>
30
+ {label}
31
+ </Button>
32
+ <Panel
33
+ open
34
+ position={position}
35
+ anchorRef={anchorRef}
36
+ arrow
37
+ aria-label={`Panel positioned ${label}`}
38
+ >
39
+ <div style={{ padding: '16px', maxWidth: '200px' }}>
40
+ Panel content positioned at <strong>{label}</strong>
41
+ </div>
42
+ </Panel>
43
+ </>
44
+ );
45
+ }
46
+
47
+ export const Top: Story = {
48
+ render: () => (
49
+ <>
50
+ <PanelStory position={Position.TOP_LEFT} label="top-left" />
51
+ <PanelStory position={Position.TOP} label="top" />
52
+ <PanelStory position={Position.TOP_RIGHT} label="top-right" />
53
+ </>
54
+ ),
55
+ ...withRTLDiff({ renderAs: 'iframe', hAlign: 'center', vAlign: 'bottom' }),
56
+ };
57
+
58
+ export const Right: Story = {
59
+ render: () => (
60
+ <>
61
+ <PanelStory position={Position.RIGHT_TOP} label="right-top" />
62
+ <PanelStory position={Position.RIGHT} label="right" />
63
+ <PanelStory position={Position.RIGHT_BOTTOM} label="right-bottom" />
64
+ </>
65
+ ),
66
+ ...withRTLDiff({ renderAs: 'iframe', hAlign: 'left', vAlign: 'center' }),
67
+ };
68
+ export const RightFlipped: Story = {
69
+ render: () => (
70
+ <>
71
+ <PanelStory position={Position.RIGHT_TOP} label="right-top" />
72
+ <PanelStory position={Position.RIGHT} label="right" />
73
+ <PanelStory position={Position.RIGHT_BOTTOM} label="right-bottom" />
74
+ </>
75
+ ),
76
+ ...withRTLDiff({ renderAs: 'iframe', hAlign: 'right', vAlign: 'center' }),
77
+ };
78
+
79
+ export const Bottom: Story = {
80
+ render: () => (
81
+ <>
82
+ <PanelStory position={Position.BOTTOM_LEFT} label="bottom-left" />
83
+ <PanelStory position={Position.BOTTOM} label="bottom" />
84
+ <PanelStory position={Position.BOTTOM_RIGHT} label="bottom-right" />
85
+ </>
86
+ ),
87
+ ...withRTLDiff({ renderAs: 'iframe', hAlign: 'center' }),
88
+ };
89
+
90
+ export const Left: Story = {
91
+ render: () => (
92
+ <>
93
+ <PanelStory position={Position.LEFT_TOP} label="left-top" />
94
+ <PanelStory position={Position.LEFT} label="left" />
95
+ <PanelStory position={Position.LEFT_BOTTOM} label="left-bottom" />
96
+ </>
97
+ ),
98
+ ...withRTLDiff({ renderAs: 'iframe', hAlign: 'right', vAlign: 'center' }),
99
+ };
100
+
101
+ export const LeftFlipped: Story = {
102
+ render: () => (
103
+ <>
104
+ <PanelStory position={Position.LEFT_TOP} label="left-top" />
105
+ <PanelStory position={Position.LEFT} label="left" />
106
+ <PanelStory position={Position.LEFT_BOTTOM} label="left-bottom" />
107
+ </>
108
+ ),
109
+ ...withRTLDiff({ renderAs: 'iframe', hAlign: 'left', vAlign: 'center' }),
110
+ };
@@ -1,5 +1,3 @@
1
- import type { Placement } from '@floating-ui/react';
2
-
3
1
  export enum Position {
4
2
  BOTTOM = 'bottom',
5
3
  BOTTOM_LEFT = 'bottom-left',
@@ -21,51 +19,3 @@ export type PositionTop = 'top';
21
19
  export type PositionRight = 'right';
22
20
  export type PositionBottom = 'bottom';
23
21
  export type PositionLeft = 'left';
24
-
25
- export type PositionTopLeft = 'top-left';
26
- export type PositionTopRight = 'top-right';
27
- export type PositionBottomLeft = 'bottom-left';
28
- export type PositionBottomRight = 'bottom-right';
29
- export type PositionLeftTop = 'left-top';
30
- export type PositionLeftBottom = 'left-bottom';
31
- export type PositionRightTop = 'right-top';
32
- export type PositionRightBottom = 'right-bottom';
33
-
34
- /** Neptune position values that can be converted to floating-ui Placement */
35
- export type FloatingPlacement =
36
- | PositionTop
37
- | PositionRight
38
- | PositionBottom
39
- | PositionLeft
40
- | PositionTopLeft
41
- | PositionTopRight
42
- | PositionRightTop
43
- | PositionRightBottom
44
- | PositionBottomLeft
45
- | PositionBottomRight
46
- | PositionLeftTop
47
- | PositionLeftBottom;
48
-
49
- /** Maps Neptune position values to floating-ui Placement */
50
- export const positionToPlacement = (position: FloatingPlacement): Placement => {
51
- switch (position) {
52
- case 'top-left':
53
- return 'top-start';
54
- case 'top-right':
55
- return 'top-end';
56
- case 'bottom-left':
57
- return 'bottom-start';
58
- case 'bottom-right':
59
- return 'bottom-end';
60
- case 'left-top':
61
- return 'left-start';
62
- case 'left-bottom':
63
- return 'left-end';
64
- case 'right-top':
65
- return 'right-start';
66
- case 'right-bottom':
67
- return 'right-end';
68
- default:
69
- return position;
70
- }
71
- };
package/src/main.css CHANGED
@@ -21875,27 +21875,35 @@ button.np-option {
21875
21875
  .np-panel[data-popper-placement^="top"] > .np-panel__content {
21876
21876
  transform: translateY(10px) scale(0.95);
21877
21877
  }
21878
- .np-panel[data-popper-placement^="top"] > .np-panel__content .np-panel__arrow {
21878
+ .np-panel[data-popper-placement^="top"] .np-panel__arrow {
21879
21879
  inset-block-end: -6px;
21880
21880
  }
21881
21881
  .np-panel[data-popper-placement^="right"] > .np-panel__content {
21882
21882
  transform: translateX(-10px) scale(0.95);
21883
21883
  }
21884
- .np-panel[data-popper-placement^="right"] > .np-panel__content .np-panel__arrow {
21884
+ .np-panel[data-popper-placement^="right"] .np-panel__arrow {
21885
21885
  inset-inline-start: -6px;
21886
21886
  }
21887
+ [dir="rtl"] .np-panel[data-popper-placement^="right"] .np-panel__arrow {
21888
+ inset-inline-start: unset;
21889
+ inset-inline-end: -6px;
21890
+ }
21887
21891
  .np-panel[data-popper-placement^="bottom"] > .np-panel__content {
21888
21892
  transform: translateY(-10px) scale(0.95);
21889
21893
  }
21890
- .np-panel[data-popper-placement^="bottom"] > .np-panel__content .np-panel__arrow {
21894
+ .np-panel[data-popper-placement^="bottom"] .np-panel__arrow {
21891
21895
  inset-block-start: -6px;
21892
21896
  }
21893
21897
  .np-panel[data-popper-placement^="left"] > .np-panel__content {
21894
21898
  transform: translateX(10px) scale(0.95);
21895
21899
  }
21896
- .np-panel[data-popper-placement^="left"] > .np-panel__content .np-panel__arrow {
21900
+ .np-panel[data-popper-placement^="left"] .np-panel__arrow {
21897
21901
  inset-inline-end: -6px;
21898
21902
  }
21903
+ [dir="rtl"] .np-panel[data-popper-placement^="left"] .np-panel__arrow {
21904
+ inset-inline-end: unset;
21905
+ inset-inline-start: -6px;
21906
+ }
21899
21907
  .np-theme-personal .np-panel__content {
21900
21908
  border-radius: 16px;
21901
21909
  border-radius: var(--radius-medium);