@transferwise/components 46.160.1 → 46.160.3
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/build/Calendar/Calendar.js +9 -3
- package/build/Calendar/Calendar.js.map +1 -1
- package/build/Calendar/Calendar.mjs +9 -3
- package/build/Calendar/Calendar.mjs.map +1 -1
- package/build/Carousel/Carousel.js +17 -10
- package/build/Carousel/Carousel.js.map +1 -1
- package/build/Carousel/Carousel.mjs +17 -10
- package/build/Carousel/Carousel.mjs.map +1 -1
- package/build/Inputs/SelectInput/Options/SelectInputOptions.js +33 -5
- package/build/Inputs/SelectInput/Options/SelectInputOptions.js.map +1 -1
- package/build/Inputs/SelectInput/Options/SelectInputOptions.mjs +34 -6
- package/build/Inputs/SelectInput/Options/SelectInputOptions.mjs.map +1 -1
- package/build/Inputs/SelectInput/SelectInput.js +28 -4
- package/build/Inputs/SelectInput/SelectInput.js.map +1 -1
- package/build/Inputs/SelectInput/SelectInput.mjs +29 -5
- package/build/Inputs/SelectInput/SelectInput.mjs.map +1 -1
- package/build/Inputs/SelectInput/SelectInput.utils.js +0 -2
- package/build/Inputs/SelectInput/SelectInput.utils.js.map +1 -1
- package/build/Inputs/SelectInput/SelectInput.utils.mjs +1 -2
- package/build/Inputs/SelectInput/SelectInput.utils.mjs.map +1 -1
- package/build/Inputs/SelectInput/TriggerButton/SelectInputTriggerButton.js +5 -1
- package/build/Inputs/SelectInput/TriggerButton/SelectInputTriggerButton.js.map +1 -1
- package/build/Inputs/SelectInput/TriggerButton/SelectInputTriggerButton.mjs +5 -1
- package/build/Inputs/SelectInput/TriggerButton/SelectInputTriggerButton.mjs.map +1 -1
- package/build/Inputs/SelectInput/constants.js +8 -0
- package/build/Inputs/SelectInput/constants.js.map +1 -0
- package/build/Inputs/SelectInput/constants.mjs +5 -0
- package/build/Inputs/SelectInput/constants.mjs.map +1 -0
- package/build/Inputs/SelectInput/hooks/useTypeahead.js +57 -0
- package/build/Inputs/SelectInput/hooks/useTypeahead.js.map +1 -0
- package/build/Inputs/SelectInput/hooks/useTypeahead.mjs +54 -0
- package/build/Inputs/SelectInput/hooks/useTypeahead.mjs.map +1 -0
- package/build/SegmentedControl/SegmentedControl.js +9 -2
- package/build/SegmentedControl/SegmentedControl.js.map +1 -1
- package/build/SegmentedControl/SegmentedControl.mjs +9 -2
- package/build/SegmentedControl/SegmentedControl.mjs.map +1 -1
- package/build/Tooltip/Tooltip.js +6 -1
- package/build/Tooltip/Tooltip.js.map +1 -1
- package/build/Tooltip/Tooltip.mjs +6 -1
- package/build/Tooltip/Tooltip.mjs.map +1 -1
- package/build/i18n/ja.json +1 -1
- package/build/i18n/ja.json.js +1 -1
- package/build/i18n/ja.json.mjs +1 -1
- package/build/i18n/tr.json +5 -0
- package/build/i18n/tr.json.js +5 -0
- package/build/i18n/tr.json.js.map +1 -1
- package/build/i18n/tr.json.mjs +5 -0
- package/build/i18n/tr.json.mjs.map +1 -1
- package/build/i18n/zh-CN.json +5 -0
- package/build/i18n/zh-CN.json.js +5 -0
- package/build/i18n/zh-CN.json.js.map +1 -1
- package/build/i18n/zh-CN.json.mjs +5 -0
- package/build/i18n/zh-CN.json.mjs.map +1 -1
- package/build/main.css +34 -22
- package/build/styles/AvatarLayout/AvatarLayout.css +26 -9
- package/build/styles/SegmentedControl/SegmentedControl.css +2 -1
- package/build/styles/Tooltip/Tooltip.css +0 -6
- package/build/styles/common/Panel/Panel.css +6 -6
- package/build/styles/main.css +34 -22
- package/build/types/Calendar/Calendar.d.ts.map +1 -1
- package/build/types/Carousel/Carousel.d.ts.map +1 -1
- package/build/types/Carousel/_stories/constants.d.ts +3 -0
- package/build/types/Carousel/_stories/constants.d.ts.map +1 -0
- package/build/types/Inputs/SelectInput/Options/SelectInputOptions.d.ts +3 -1
- package/build/types/Inputs/SelectInput/Options/SelectInputOptions.d.ts.map +1 -1
- package/build/types/Inputs/SelectInput/SelectInput.d.ts.map +1 -1
- package/build/types/Inputs/SelectInput/SelectInput.utils.d.ts +0 -1
- package/build/types/Inputs/SelectInput/SelectInput.utils.d.ts.map +1 -1
- package/build/types/Inputs/SelectInput/TriggerButton/SelectInputTriggerButton.d.ts.map +1 -1
- package/build/types/Inputs/SelectInput/constants.d.ts +3 -0
- package/build/types/Inputs/SelectInput/constants.d.ts.map +1 -0
- package/build/types/Inputs/SelectInput/hooks/useTypeahead.d.ts +8 -0
- package/build/types/Inputs/SelectInput/hooks/useTypeahead.d.ts.map +1 -0
- package/build/types/SegmentedControl/SegmentedControl.d.ts.map +1 -1
- package/build/types/Tooltip/Tooltip.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/AvatarLayout/AvatarLayout.css +26 -9
- package/src/AvatarLayout/AvatarLayout.less +46 -22
- package/src/AvatarLayout/_stories/AvatarLayout.test.story.tsx +17 -42
- package/src/Calendar/Calendar.test.tsx +82 -0
- package/src/Calendar/Calendar.tsx +6 -3
- package/src/Carousel/Carousel.test.tsx +50 -2
- package/src/Carousel/Carousel.tsx +13 -10
- package/src/Carousel/_stories/Carousel.story.tsx +12 -52
- package/src/Carousel/_stories/Carousel.test.story.tsx +7 -43
- package/src/Carousel/_stories/constants.tsx +46 -0
- package/src/DateLookup/_stories/DateLookup.rtl.test.story.tsx +59 -48
- package/src/Field/_stories/Field.test.story.tsx +48 -37
- package/src/FlowNavigation/_stories/FlowNavigation.rtl.test.story.tsx +78 -0
- package/src/FlowNavigation/_stories/FlowNavigation.test.story.tsx +0 -44
- package/src/Inputs/SelectInput/Options/SelectInputOptions.tsx +36 -6
- package/src/Inputs/SelectInput/SelectInput.test.tsx +143 -0
- package/src/Inputs/SelectInput/SelectInput.tsx +31 -5
- package/src/Inputs/SelectInput/SelectInput.utils.ts +0 -2
- package/src/Inputs/SelectInput/TriggerButton/SelectInputTriggerButton.tsx +3 -0
- package/src/Inputs/SelectInput/constants.ts +3 -0
- package/src/Inputs/SelectInput/hooks/useTypeahead.ts +82 -0
- package/src/Label/_stories/Label.test.story.tsx +35 -22
- package/src/Popover/_stories/Popover.rtl.test.story.tsx +40 -50
- package/src/SegmentedControl/SegmentedControl.css +2 -1
- package/src/SegmentedControl/SegmentedControl.less +4 -2
- package/src/SegmentedControl/SegmentedControl.tsx +8 -2
- package/src/SegmentedControl/_stories/SegmentedControl.test.story.tsx +16 -8
- package/src/Tooltip/Tooltip.css +0 -6
- package/src/Tooltip/Tooltip.less +0 -12
- package/src/Tooltip/Tooltip.test.tsx +51 -0
- package/src/Tooltip/Tooltip.tsx +11 -1
- package/src/Tooltip/_stories/Tooltip.rtl.test.story.tsx +16 -34
- package/src/common/Panel/Panel.css +6 -6
- package/src/common/Panel/Panel.less +6 -6
- package/src/i18n/ja.json +1 -1
- package/src/i18n/tr.json +5 -0
- package/src/i18n/zh-CN.json +5 -0
- package/src/main.css +34 -22
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import mergeProps from 'merge-props';
|
|
2
|
-
import { useCallback, useEffect, useRef, useState
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { Listbox as ListboxBase } from '@headlessui/react';
|
|
4
4
|
import { Breakpoint } from '@transferwise/neptune-tokens';
|
|
5
5
|
import { useScreenSize } from '../../common/hooks/useScreenSize';
|
|
@@ -10,6 +10,7 @@ import { SelectInputBottomSheet } from './BottomSheet';
|
|
|
10
10
|
import { SelectInputPopover } from './Popover';
|
|
11
11
|
import { SelectInputOptions } from './Options';
|
|
12
12
|
import { DefaultRenderTrigger } from './DefaultRenderTrigger';
|
|
13
|
+
import { isTypingKey } from './hooks/useTypeahead';
|
|
13
14
|
|
|
14
15
|
import {
|
|
15
16
|
SelectInputOptionContentWithinTriggerContext,
|
|
@@ -74,7 +75,6 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
74
75
|
}, [open]);
|
|
75
76
|
|
|
76
77
|
const [filterQuery, _setFilterQuery] = useState('');
|
|
77
|
-
const deferredFilterQuery = useDeferredValue(filterQuery);
|
|
78
78
|
const previousFilterQueryRef = useRef(filterQuery);
|
|
79
79
|
|
|
80
80
|
const setFilterQuery = useCallback(
|
|
@@ -94,6 +94,7 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
94
94
|
const internalTriggerRef = useRef<HTMLButtonElement | null>(null);
|
|
95
95
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
96
96
|
const listboxRef = useRef<HTMLDivElement>(null);
|
|
97
|
+
const [initialTypeaheadKey, setInitialTypeaheadKey] = useState<string | null>(null);
|
|
97
98
|
const controllerRef = filterable ? searchInputRef : listboxRef;
|
|
98
99
|
|
|
99
100
|
const screenSm = useScreenSize(Breakpoint.SMALL);
|
|
@@ -175,13 +176,33 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
175
176
|
setOpen((prev) => !prev);
|
|
176
177
|
},
|
|
177
178
|
onKeyDown: (event: React.KeyboardEvent) => {
|
|
179
|
+
if (event.key === 'Enter') {
|
|
180
|
+
event.preventDefault();
|
|
181
|
+
event.currentTarget.dispatchEvent(
|
|
182
|
+
new KeyboardEvent('keydown', { key: ' ', bubbles: true }),
|
|
183
|
+
);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!open && isTypingKey(event)) {
|
|
188
|
+
event.preventDefault();
|
|
189
|
+
|
|
190
|
+
if (filterable) {
|
|
191
|
+
setFilterQuery(event.key);
|
|
192
|
+
} else {
|
|
193
|
+
setInitialTypeaheadKey(event.key);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
(event.currentTarget as HTMLButtonElement).click();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
178
200
|
if (
|
|
179
201
|
event.key === ' ' ||
|
|
180
|
-
event.key === 'Enter' ||
|
|
181
202
|
event.key === 'ArrowDown' ||
|
|
182
203
|
event.key === 'ArrowUp'
|
|
183
204
|
) {
|
|
184
|
-
setOpen(
|
|
205
|
+
setOpen(true);
|
|
185
206
|
}
|
|
186
207
|
},
|
|
187
208
|
},
|
|
@@ -225,6 +246,7 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
225
246
|
setOpen(false);
|
|
226
247
|
}}
|
|
227
248
|
onCloseEnd={() => {
|
|
249
|
+
setInitialTypeaheadKey(null);
|
|
228
250
|
setFilterQuery('');
|
|
229
251
|
}}
|
|
230
252
|
>
|
|
@@ -240,10 +262,14 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
240
262
|
sortFilteredOptions={sortFilteredOptions}
|
|
241
263
|
searchInputRef={searchInputRef}
|
|
242
264
|
listboxRef={listboxRef}
|
|
243
|
-
filterQuery={
|
|
265
|
+
filterQuery={filterQuery}
|
|
266
|
+
initialTypeaheadKey={initialTypeaheadKey}
|
|
244
267
|
autocomplete={autocomplete}
|
|
245
268
|
name={name}
|
|
246
269
|
onFilterChange={setFilterQuery}
|
|
270
|
+
onInitialTypeaheadHandled={() => {
|
|
271
|
+
setInitialTypeaheadKey(null);
|
|
272
|
+
}}
|
|
247
273
|
onAutocompleteSelect={(matchedValue) => {
|
|
248
274
|
onChange?.(matchedValue as M extends true ? T[] : T);
|
|
249
275
|
if (!multiple) {
|
|
@@ -29,6 +29,9 @@ export function SelectInputTriggerButton<T extends SelectInputTriggerButtonEleme
|
|
|
29
29
|
ref={ref}
|
|
30
30
|
as={PolymorphicWithOverrides}
|
|
31
31
|
role="combobox"
|
|
32
|
+
// Safari can omit buttons from sequential keyboard navigation unless their
|
|
33
|
+
// focus order is explicit. Zero keeps the trigger in the natural tab order.
|
|
34
|
+
tabIndex={0}
|
|
32
35
|
__overrides={{ as, size, ...interactionProps }}
|
|
33
36
|
{...mergeProps({ onClick, onKeyDown }, restProps)}
|
|
34
37
|
/>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
import { TYPEAHEAD_QUERY_RESET_TIMEOUT_MS } from '../constants';
|
|
4
|
+
import { searchableString } from '../SelectInput.utils';
|
|
5
|
+
|
|
6
|
+
const resetsTypeaheadQuery = (key: string) =>
|
|
7
|
+
key === 'ArrowDown' ||
|
|
8
|
+
key === 'ArrowUp' ||
|
|
9
|
+
key === 'Home' ||
|
|
10
|
+
key === 'End' ||
|
|
11
|
+
key === 'PageDown' ||
|
|
12
|
+
key === 'PageUp';
|
|
13
|
+
|
|
14
|
+
export const isTypingKey = (event: React.KeyboardEvent, query = '') =>
|
|
15
|
+
!event.metaKey &&
|
|
16
|
+
!event.ctrlKey &&
|
|
17
|
+
!event.nativeEvent.isComposing &&
|
|
18
|
+
/^.$/u.test(event.key) &&
|
|
19
|
+
(event.key !== ' ' || query !== '');
|
|
20
|
+
|
|
21
|
+
interface UseTypeaheadParams {
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
onMatch?: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function useTypeahead({ disabled = false, onMatch }: UseTypeaheadParams = {}) {
|
|
27
|
+
const queryRef = useRef('');
|
|
28
|
+
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
29
|
+
|
|
30
|
+
const resetQuery = useCallback(() => {
|
|
31
|
+
queryRef.current = '';
|
|
32
|
+
if (timeoutRef.current != null) {
|
|
33
|
+
clearTimeout(timeoutRef.current);
|
|
34
|
+
timeoutRef.current = null;
|
|
35
|
+
}
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
useEffect(() => resetQuery, [resetQuery]);
|
|
39
|
+
|
|
40
|
+
return useCallback(
|
|
41
|
+
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
42
|
+
if (disabled || !isTypingKey(event, queryRef.current)) {
|
|
43
|
+
if (resetsTypeaheadQuery(event.key)) {
|
|
44
|
+
resetQuery();
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
event.preventDefault();
|
|
50
|
+
event.stopPropagation();
|
|
51
|
+
|
|
52
|
+
queryRef.current += event.key;
|
|
53
|
+
const needle = searchableString(queryRef.current);
|
|
54
|
+
|
|
55
|
+
if (timeoutRef.current != null) {
|
|
56
|
+
clearTimeout(timeoutRef.current);
|
|
57
|
+
}
|
|
58
|
+
timeoutRef.current = setTimeout(resetQuery, TYPEAHEAD_QUERY_RESET_TIMEOUT_MS);
|
|
59
|
+
|
|
60
|
+
const options = Array.from(
|
|
61
|
+
event.currentTarget.querySelectorAll<HTMLElement>(
|
|
62
|
+
'[role="option"]:not([aria-disabled="true"])',
|
|
63
|
+
),
|
|
64
|
+
);
|
|
65
|
+
const optionLabels = options.map((option) => searchableString(option.textContent ?? ''));
|
|
66
|
+
const matchingIndex = optionLabels.findIndex((label) => label.startsWith(needle));
|
|
67
|
+
const closestIndex =
|
|
68
|
+
matchingIndex === -1
|
|
69
|
+
? optionLabels.findIndex((label) => label.includes(needle))
|
|
70
|
+
: matchingIndex;
|
|
71
|
+
|
|
72
|
+
if (closestIndex === -1) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
options[closestIndex].focus({ preventScroll: true });
|
|
77
|
+
event.currentTarget.focus({ preventScroll: true });
|
|
78
|
+
onMatch?.();
|
|
79
|
+
},
|
|
80
|
+
[disabled, onMatch, resetQuery],
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-webpack5';
|
|
2
|
-
import { withRTLDiff } from '../../../.storybook/decorators/RtlDiffDecorator';
|
|
2
|
+
import { useRtlDiffI18n, withRTLDiff } from '../../../.storybook/decorators/RtlDiffDecorator';
|
|
3
3
|
import Info from '../../Info';
|
|
4
4
|
import Label from '..';
|
|
5
5
|
|
|
@@ -12,25 +12,38 @@ export default {
|
|
|
12
12
|
type Story = StoryObj<typeof Label>;
|
|
13
13
|
|
|
14
14
|
export const RTLDiff: Story = {
|
|
15
|
-
render: () =>
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
<Label.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
<Label
|
|
29
|
-
Label
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
15
|
+
render: () => {
|
|
16
|
+
const i18n = useRtlDiffI18n();
|
|
17
|
+
return (
|
|
18
|
+
<>
|
|
19
|
+
<Label>{i18n.plain}</Label>
|
|
20
|
+
<Label>
|
|
21
|
+
<Label.Optional>{i18n.optional}</Label.Optional>
|
|
22
|
+
</Label>
|
|
23
|
+
<Label>
|
|
24
|
+
<div>
|
|
25
|
+
{i18n.withInfo} <Info content={i18n.tooltip} aria-label={i18n.moreInfo} />
|
|
26
|
+
</div>
|
|
27
|
+
</Label>
|
|
28
|
+
<Label>
|
|
29
|
+
<Label.Optional>
|
|
30
|
+
{i18n.optionalWithInfo} <Info content={i18n.tooltip} aria-label={i18n.moreInfo} />
|
|
31
|
+
</Label.Optional>
|
|
32
|
+
</Label>
|
|
33
|
+
</>
|
|
34
|
+
);
|
|
35
|
+
},
|
|
36
|
+
...withRTLDiff({
|
|
37
|
+
i18n: {
|
|
38
|
+
plain: { ltr: 'Plain label text', rtl: 'نص التسمية العادي' },
|
|
39
|
+
optional: { ltr: 'Label with optional marker', rtl: 'تسمية مع علامة اختيارية' },
|
|
40
|
+
withInfo: { ltr: 'Label with info icon', rtl: 'تسمية مع أيقونة معلومات' },
|
|
41
|
+
optionalWithInfo: {
|
|
42
|
+
ltr: 'Label with optional and info icon',
|
|
43
|
+
rtl: 'تسمية مع اختيارية وأيقونة معلومات',
|
|
44
|
+
},
|
|
45
|
+
tooltip: { ltr: 'Helpful tooltip content', rtl: 'محتوى تلميح مفيد' },
|
|
46
|
+
moreInfo: { ltr: 'More information', rtl: 'مزيد من المعلومات' },
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
36
49
|
};
|
|
@@ -14,16 +14,6 @@ export default {
|
|
|
14
14
|
|
|
15
15
|
type Story = StoryObj<typeof Popover>;
|
|
16
16
|
|
|
17
|
-
const iframeDiff = withRTLDiff({ renderAs: 'iframe' });
|
|
18
|
-
|
|
19
|
-
const containerStyle: React.CSSProperties = {
|
|
20
|
-
display: 'flex',
|
|
21
|
-
justifyContent: 'center',
|
|
22
|
-
alignItems: 'center',
|
|
23
|
-
minHeight: '24rem',
|
|
24
|
-
width: '100%',
|
|
25
|
-
};
|
|
26
|
-
|
|
27
17
|
const clickTrigger = async ({ canvasElement }: { canvasElement: HTMLElement }) => {
|
|
28
18
|
const canvas = within(canvasElement);
|
|
29
19
|
const trigger = canvas.queryByRole('button');
|
|
@@ -33,64 +23,64 @@ const clickTrigger = async ({ canvasElement }: { canvasElement: HTMLElement }) =
|
|
|
33
23
|
|
|
34
24
|
export const PlacementRight: Story = {
|
|
35
25
|
render: () => (
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
>
|
|
42
|
-
|
|
43
|
-
</
|
|
44
|
-
</
|
|
26
|
+
<Popover
|
|
27
|
+
preferredPlacement={Position.RIGHT}
|
|
28
|
+
title="Guaranteed rate"
|
|
29
|
+
content="You'll get this rate as long as we receive your 10 EUR within the next 51 hours."
|
|
30
|
+
>
|
|
31
|
+
<Button size="sm" v2>
|
|
32
|
+
right
|
|
33
|
+
</Button>
|
|
34
|
+
</Popover>
|
|
45
35
|
),
|
|
46
36
|
play: clickTrigger,
|
|
47
|
-
...
|
|
37
|
+
...withRTLDiff({ renderAs: 'iframe', hAlign: 'left', vAlign: 'center' }),
|
|
48
38
|
};
|
|
49
39
|
|
|
50
40
|
export const PlacementLeft: Story = {
|
|
51
41
|
render: () => (
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
>
|
|
58
|
-
|
|
59
|
-
</
|
|
60
|
-
</
|
|
42
|
+
<Popover
|
|
43
|
+
preferredPlacement={Position.LEFT}
|
|
44
|
+
title="Guaranteed rate"
|
|
45
|
+
content="You'll get this rate as long as we receive your 10 EUR within the next 51 hours."
|
|
46
|
+
>
|
|
47
|
+
<Button size="sm" v2>
|
|
48
|
+
left
|
|
49
|
+
</Button>
|
|
50
|
+
</Popover>
|
|
61
51
|
),
|
|
62
52
|
play: clickTrigger,
|
|
63
|
-
...
|
|
53
|
+
...withRTLDiff({ renderAs: 'iframe', hAlign: 'right', vAlign: 'center' }),
|
|
64
54
|
};
|
|
65
55
|
|
|
66
56
|
export const PlacementTop: Story = {
|
|
67
57
|
render: () => (
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
>
|
|
74
|
-
|
|
75
|
-
</
|
|
76
|
-
</
|
|
58
|
+
<Popover
|
|
59
|
+
preferredPlacement={Position.TOP}
|
|
60
|
+
title="Guaranteed rate"
|
|
61
|
+
content="You'll get this rate as long as we receive your 10 EUR within the next 51 hours."
|
|
62
|
+
>
|
|
63
|
+
<Button size="sm" v2>
|
|
64
|
+
top
|
|
65
|
+
</Button>
|
|
66
|
+
</Popover>
|
|
77
67
|
),
|
|
78
68
|
play: clickTrigger,
|
|
79
|
-
...
|
|
69
|
+
...withRTLDiff({ renderAs: 'iframe', vAlign: 'bottom', hAlign: 'center' }),
|
|
80
70
|
};
|
|
81
71
|
|
|
82
72
|
export const PlacementBottom: Story = {
|
|
83
73
|
render: () => (
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
>
|
|
90
|
-
|
|
91
|
-
</
|
|
92
|
-
</
|
|
74
|
+
<Popover
|
|
75
|
+
preferredPlacement={Position.BOTTOM}
|
|
76
|
+
title="Guaranteed rate"
|
|
77
|
+
content="You'll get this rate as long as we receive your 10 EUR within the next 51 hours."
|
|
78
|
+
>
|
|
79
|
+
<Button size="sm" v2>
|
|
80
|
+
bottom
|
|
81
|
+
</Button>
|
|
82
|
+
</Popover>
|
|
93
83
|
),
|
|
94
84
|
play: clickTrigger,
|
|
95
|
-
...
|
|
85
|
+
...withRTLDiff({ renderAs: 'iframe', hAlign: 'center' }),
|
|
96
86
|
};
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
background: var(--color-background-screen);
|
|
36
36
|
border-radius: 24px;
|
|
37
37
|
border-radius: var(--size-24);
|
|
38
|
-
transition: left 300ms
|
|
38
|
+
transition: left 300ms,
|
|
39
|
+
right 300ms;
|
|
39
40
|
}
|
|
40
41
|
.segmented-control__segments--no-animate::after {
|
|
41
42
|
transition: none !important;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
justify-content: center;
|
|
13
13
|
align-items: center;
|
|
14
14
|
background: var(--color-background-neutral);
|
|
15
|
-
border-radius: var(--size-24);
|
|
15
|
+
border-radius: var(--size-24);
|
|
16
16
|
transition: outline 300ms;
|
|
17
17
|
outline: 2px solid transparent;
|
|
18
18
|
}
|
|
@@ -31,7 +31,9 @@
|
|
|
31
31
|
z-index: 0;
|
|
32
32
|
background: var(--color-background-screen);
|
|
33
33
|
border-radius: var(--size-24);
|
|
34
|
-
transition:
|
|
34
|
+
transition:
|
|
35
|
+
left 300ms,
|
|
36
|
+
right 300ms;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
.segmented-control__segments--no-animate::after {
|
|
@@ -3,6 +3,7 @@ import { createRef, useEffect, useRef, useState } from 'react';
|
|
|
3
3
|
|
|
4
4
|
import Body from '../Body';
|
|
5
5
|
import { Typography } from '../common';
|
|
6
|
+
import { useDirection } from '../common/hooks';
|
|
6
7
|
|
|
7
8
|
type SegmentBase = { id: string; label: string; value: string };
|
|
8
9
|
|
|
@@ -40,6 +41,7 @@ const SegmentedControl = ({
|
|
|
40
41
|
}: SegmentedControlProps) => {
|
|
41
42
|
const isMountedRef = useRef(false);
|
|
42
43
|
const [animate, setAnimate] = useState(false);
|
|
44
|
+
const { isRTL } = useDirection();
|
|
43
45
|
|
|
44
46
|
const segmentsRef = useRef<HTMLDivElement>(null);
|
|
45
47
|
|
|
@@ -62,8 +64,12 @@ const SegmentedControl = ({
|
|
|
62
64
|
// This is so we can animate the highlight to the selected segment
|
|
63
65
|
if (selectedSegmentRef?.current && segmentsRef.current) {
|
|
64
66
|
const { style } = segmentsRef.current;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
const segment = selectedSegmentRef.current;
|
|
68
|
+
const inlineOffset = isRTL
|
|
69
|
+
? segmentsRef.current.offsetWidth - segment.offsetLeft - segment.offsetWidth
|
|
70
|
+
: segment.offsetLeft;
|
|
71
|
+
style.setProperty('--segment-highlight-width', `${segment.offsetWidth}px`);
|
|
72
|
+
style.setProperty('--segment-highlight-x', `${inlineOffset}px`);
|
|
67
73
|
}
|
|
68
74
|
};
|
|
69
75
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
1
2
|
import type { Meta, StoryObj } from '@storybook/react-webpack5';
|
|
2
3
|
import { withRTLDiff } from '../../../.storybook/decorators/RtlDiffDecorator';
|
|
3
4
|
import SegmentedControl from '../SegmentedControl';
|
|
@@ -22,24 +23,31 @@ const viewSegments = [
|
|
|
22
23
|
{ id: 'seg-gamma', label: 'Gamma', value: 'gamma', controls: 'panel-gamma' },
|
|
23
24
|
] as const;
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
function InteractiveSegmentedControl() {
|
|
27
|
+
const [inputValue, setInputValue] = useState('one');
|
|
28
|
+
const [viewValue, setViewValue] = useState('beta');
|
|
29
|
+
|
|
30
|
+
return (
|
|
27
31
|
<>
|
|
28
32
|
<SegmentedControl
|
|
29
33
|
name="rtl-input"
|
|
30
34
|
mode="input"
|
|
31
35
|
segments={inputSegments}
|
|
32
|
-
value=
|
|
33
|
-
onChange={
|
|
36
|
+
value={inputValue}
|
|
37
|
+
onChange={setInputValue}
|
|
34
38
|
/>
|
|
35
39
|
<SegmentedControl
|
|
36
40
|
name="rtl-view"
|
|
37
41
|
mode="view"
|
|
38
42
|
segments={viewSegments}
|
|
39
|
-
value=
|
|
40
|
-
onChange={
|
|
43
|
+
value={viewValue}
|
|
44
|
+
onChange={setViewValue}
|
|
41
45
|
/>
|
|
42
46
|
</>
|
|
43
|
-
)
|
|
44
|
-
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const RTLDiff: Story = {
|
|
51
|
+
render: () => <InteractiveSegmentedControl />,
|
|
52
|
+
...withRTLDiff({ width: 'stretch', renderAs: 'iframe' }),
|
|
45
53
|
};
|
package/src/Tooltip/Tooltip.css
CHANGED
|
@@ -42,9 +42,3 @@
|
|
|
42
42
|
visibility: visible;
|
|
43
43
|
visibility: initial;
|
|
44
44
|
}
|
|
45
|
-
.np-panel[data-popper-placement^="right"] > .np-panel__content.tooltip-inner .np-panel__arrow {
|
|
46
|
-
inset-inline-start: -7px;
|
|
47
|
-
}
|
|
48
|
-
.np-panel[data-popper-placement^="left"] > .np-panel__content.tooltip-inner .np-panel__arrow {
|
|
49
|
-
inset-inline-end: -7px;
|
|
50
|
-
}
|
package/src/Tooltip/Tooltip.less
CHANGED
|
@@ -48,15 +48,3 @@
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
|
|
52
|
-
.np-panel[data-popper-placement^="right"] > .np-panel__content.tooltip-inner {
|
|
53
|
-
.np-panel__arrow {
|
|
54
|
-
left: -7px;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
.np-panel[data-popper-placement^="left"] > .np-panel__content.tooltip-inner {
|
|
59
|
-
.np-panel__arrow {
|
|
60
|
-
right: -7px;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { render, screen } from '@testing-library/react';
|
|
2
2
|
import { userEvent } from '@testing-library/user-event';
|
|
3
3
|
|
|
4
|
+
import { DirectionProvider } from '../Provider/Direction/DirectionProvider';
|
|
4
5
|
import Tooltip from '.';
|
|
5
6
|
|
|
6
7
|
describe('Tooltip Component', () => {
|
|
@@ -102,4 +103,54 @@ describe('Tooltip Component', () => {
|
|
|
102
103
|
const tooltipElement = screen.getByRole('tooltip', { hidden: true });
|
|
103
104
|
expect(tooltipElement).toBeVisible();
|
|
104
105
|
});
|
|
106
|
+
|
|
107
|
+
describe('RTL position mirroring', () => {
|
|
108
|
+
it('mirrors position="left" to right in RTL', async () => {
|
|
109
|
+
render(
|
|
110
|
+
<DirectionProvider direction="rtl">
|
|
111
|
+
<Tooltip label="Test Tooltip" position="left">
|
|
112
|
+
<span>Hover me</span>
|
|
113
|
+
</Tooltip>
|
|
114
|
+
</DirectionProvider>,
|
|
115
|
+
);
|
|
116
|
+
const triggerElement = screen.getByText('Hover me');
|
|
117
|
+
await userEvent.hover(triggerElement);
|
|
118
|
+
|
|
119
|
+
const tooltip = screen.getByRole('tooltip', { hidden: true });
|
|
120
|
+
expect(tooltip).toHaveAttribute('data-popper-placement', 'right');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('mirrors position="right" to left in RTL', async () => {
|
|
124
|
+
render(
|
|
125
|
+
<DirectionProvider direction="rtl">
|
|
126
|
+
<Tooltip label="Test Tooltip" position="right">
|
|
127
|
+
<span>Hover me</span>
|
|
128
|
+
</Tooltip>
|
|
129
|
+
</DirectionProvider>,
|
|
130
|
+
);
|
|
131
|
+
const triggerElement = screen.getByText('Hover me');
|
|
132
|
+
await userEvent.hover(triggerElement);
|
|
133
|
+
|
|
134
|
+
const tooltip = screen.getByRole('tooltip', { hidden: true });
|
|
135
|
+
expect(tooltip).toHaveAttribute('data-popper-placement', 'left');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it.each(['top', 'bottom'] as const)(
|
|
139
|
+
'does not mirror position="%s" in RTL',
|
|
140
|
+
async (position) => {
|
|
141
|
+
render(
|
|
142
|
+
<DirectionProvider direction="rtl">
|
|
143
|
+
<Tooltip label="Test Tooltip" position={position}>
|
|
144
|
+
<span>Hover me</span>
|
|
145
|
+
</Tooltip>
|
|
146
|
+
</DirectionProvider>,
|
|
147
|
+
);
|
|
148
|
+
const triggerElement = screen.getByText('Hover me');
|
|
149
|
+
await userEvent.hover(triggerElement);
|
|
150
|
+
|
|
151
|
+
const tooltip = screen.getByRole('tooltip', { hidden: true });
|
|
152
|
+
expect(tooltip).toHaveAttribute('data-popper-placement', position);
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
});
|
|
105
156
|
});
|
package/src/Tooltip/Tooltip.tsx
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from 'react';
|
|
20
20
|
|
|
21
21
|
import { CommonProps, Position } from '../common';
|
|
22
|
+
import useDirection from '../common/hooks/useDirection';
|
|
22
23
|
import {
|
|
23
24
|
PositionBottom,
|
|
24
25
|
PositionLeft,
|
|
@@ -42,12 +43,21 @@ const Tooltip = ({
|
|
|
42
43
|
}: TooltipProps) => {
|
|
43
44
|
const [open, setOpen] = useState(false);
|
|
44
45
|
const arrowRef = useRef<HTMLDivElement | null>(null);
|
|
46
|
+
const { isRTL } = useDirection();
|
|
47
|
+
|
|
48
|
+
const resolvedPosition = isRTL
|
|
49
|
+
? position === 'left'
|
|
50
|
+
? 'right'
|
|
51
|
+
: position === 'right'
|
|
52
|
+
? 'left'
|
|
53
|
+
: position
|
|
54
|
+
: position;
|
|
45
55
|
|
|
46
56
|
const fallbackId = useId();
|
|
47
57
|
const tooltipId = id ?? fallbackId;
|
|
48
58
|
|
|
49
59
|
const { refs, floatingStyles, placement, middlewareData } = useFloating({
|
|
50
|
-
placement:
|
|
60
|
+
placement: resolvedPosition,
|
|
51
61
|
middleware: [
|
|
52
62
|
offset(16),
|
|
53
63
|
flip({ fallbackPlacements: [Position.TOP] }),
|