@transferwise/components 0.0.0-experimental-a087360 → 0.0.0-experimental-73ab65e
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/index.esm.js +91 -68
- package/build/index.esm.js.map +1 -1
- package/build/index.js +91 -68
- package/build/index.js.map +1 -1
- package/build/main.css +1 -1
- package/build/styles/flowNavigation/FlowNavigation.css +1 -1
- package/build/styles/flowNavigation/animatedLabel/AnimatedLabel.css +1 -1
- package/build/styles/inputs/SelectInput.css +1 -1
- package/build/styles/main.css +1 -1
- package/build/styles/overlayHeader/OverlayHeader.css +1 -1
- package/build/types/common/flowHeader/FlowHeader.d.ts.map +1 -1
- package/build/types/flowNavigation/FlowNavigation.d.ts.map +1 -1
- package/build/types/flowNavigation/animatedLabel/AnimatedLabel.d.ts +2 -1
- package/build/types/flowNavigation/animatedLabel/AnimatedLabel.d.ts.map +1 -1
- package/build/types/flowNavigation/backButton/BackButton.d.ts +11 -10
- package/build/types/flowNavigation/backButton/BackButton.d.ts.map +1 -1
- package/build/types/inputs/SelectInput.d.ts +2 -1
- package/build/types/inputs/SelectInput.d.ts.map +1 -1
- package/build/types/overlayHeader/OverlayHeader.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/common/flowHeader/FlowHeader.tsx +22 -4
- package/src/common/flowHeader/__snapshots__/FlowHeader.spec.js.snap +22 -6
- package/src/flowNavigation/FlowNavigation.css +1 -1
- package/src/flowNavigation/FlowNavigation.less +9 -0
- package/src/flowNavigation/FlowNavigation.spec.js +3 -3
- package/src/flowNavigation/FlowNavigation.story.js +189 -22
- package/src/flowNavigation/FlowNavigation.tsx +27 -22
- package/src/flowNavigation/__snapshots__/FlowNavigation.spec.js.snap +26 -18
- package/src/flowNavigation/animatedLabel/AnimatedLabel.css +1 -1
- package/src/flowNavigation/animatedLabel/AnimatedLabel.less +6 -0
- package/src/flowNavigation/animatedLabel/AnimatedLabel.spec.js +21 -7
- package/src/flowNavigation/animatedLabel/AnimatedLabel.story.js +22 -0
- package/src/flowNavigation/animatedLabel/AnimatedLabel.tsx +17 -8
- package/src/flowNavigation/animatedLabel/__snapshots__/AnimatedLabel.spec.js.snap +4 -4
- package/src/flowNavigation/backButton/BackButton.js +9 -14
- package/src/flowNavigation/backButton/BackButton.spec.js +2 -3
- package/src/flowNavigation/backButton/__snapshots__/BackButton.spec.js.snap +21 -28
- package/src/inputs/SelectInput.css +1 -1
- package/src/inputs/SelectInput.less +4 -0
- package/src/inputs/SelectInput.spec.tsx +40 -0
- package/src/inputs/SelectInput.story.tsx +18 -0
- package/src/inputs/SelectInput.tsx +38 -10
- package/src/inputs/_BottomSheet.less +1 -1
- package/src/main.css +1 -1
- package/src/overlayHeader/OverlayHeader.css +1 -1
- package/src/overlayHeader/OverlayHeader.js +4 -6
- package/src/overlayHeader/OverlayHeader.less +8 -0
- package/src/overlayHeader/OverlayHeader.spec.js +1 -1
- package/src/overlayHeader/__snapshots__/OverlayHeader.spec.js.snap +14 -10
|
@@ -6,19 +6,23 @@ exports[`FlowNavigation on mobile renders as expected 1`] = `
|
|
|
6
6
|
class="np-flow-navigation d-flex align-items-center justify-content-center p-y-3 np-flow-navigation--border-bottom"
|
|
7
7
|
>
|
|
8
8
|
<div
|
|
9
|
-
class="np-flow-header d-flex flex-wrap align-items-center
|
|
9
|
+
class="np-flow-header d-flex flex-wrap align-items-center np-flow-navigation__content p-x-3 np-flow-navigation--xs-max"
|
|
10
10
|
>
|
|
11
|
-
<div>
|
|
12
|
-
BackButton
|
|
13
|
-
</div>
|
|
14
11
|
<div
|
|
15
|
-
class="
|
|
16
|
-
data-testid="activeLabel-1"
|
|
12
|
+
class="np-flow-header__left flex__item--8"
|
|
17
13
|
>
|
|
18
|
-
|
|
14
|
+
<div>
|
|
15
|
+
BackButton
|
|
16
|
+
<div
|
|
17
|
+
class="m-x-1"
|
|
18
|
+
data-testid="activeLabel-0"
|
|
19
|
+
>
|
|
20
|
+
AnimatedLabel
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
19
23
|
</div>
|
|
20
24
|
<div
|
|
21
|
-
class="d-flex
|
|
25
|
+
class="align-items-center d-flex np-flow-header__right justify-content-end flex__item--4 "
|
|
22
26
|
>
|
|
23
27
|
<div
|
|
24
28
|
class="tw-avatar tw-avatar--48 tw-avatar--initials np-text-title-body"
|
|
@@ -30,7 +34,7 @@ exports[`FlowNavigation on mobile renders as expected 1`] = `
|
|
|
30
34
|
</div>
|
|
31
35
|
</div>
|
|
32
36
|
<span
|
|
33
|
-
class="
|
|
37
|
+
class="separator"
|
|
34
38
|
/>
|
|
35
39
|
<button
|
|
36
40
|
aria-label="Close"
|
|
@@ -118,16 +122,20 @@ exports[`FlowNavigation renders as expected 1`] = `
|
|
|
118
122
|
class="np-flow-navigation d-flex align-items-center justify-content-center p-y-3 np-flow-navigation--border-bottom"
|
|
119
123
|
>
|
|
120
124
|
<div
|
|
121
|
-
class="np-flow-header d-flex flex-wrap align-items-center
|
|
125
|
+
class="np-flow-header d-flex flex-wrap align-items-center np-flow-navigation__content p-x-3 np-flow-navigation--sm np-flow-navigation--lg"
|
|
122
126
|
>
|
|
123
|
-
<img
|
|
124
|
-
alt="logo"
|
|
125
|
-
height="24"
|
|
126
|
-
src="logo.svg"
|
|
127
|
-
width="138"
|
|
128
|
-
/>
|
|
129
127
|
<div
|
|
130
|
-
class="
|
|
128
|
+
class="np-flow-header__left"
|
|
129
|
+
>
|
|
130
|
+
<img
|
|
131
|
+
alt="logo"
|
|
132
|
+
height="24"
|
|
133
|
+
src="logo.svg"
|
|
134
|
+
width="138"
|
|
135
|
+
/>
|
|
136
|
+
</div>
|
|
137
|
+
<div
|
|
138
|
+
class="align-items-center d-flex np-flow-header__right justify-content-end order-2"
|
|
131
139
|
>
|
|
132
140
|
<div
|
|
133
141
|
class="tw-avatar tw-avatar--48 tw-avatar--initials np-text-title-body"
|
|
@@ -139,7 +147,7 @@ exports[`FlowNavigation renders as expected 1`] = `
|
|
|
139
147
|
</div>
|
|
140
148
|
</div>
|
|
141
149
|
<span
|
|
142
|
-
class="
|
|
150
|
+
class="separator"
|
|
143
151
|
/>
|
|
144
152
|
<button
|
|
145
153
|
aria-label="Close"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
.np-animated-label{height:24px;overflow:hidden;padding-top:1px;position:relative}.np-animated-label>*{height:0;opacity:0;position:absolute;transform:translateX(-8px);transition:all .3s ease-in}.np-animated-label--in{height:auto;opacity:1;position:relative;top:auto;transform:translateX(0);transition:all .3s ease-in .3s}
|
|
1
|
+
.np-animated-label{height:24px;overflow:hidden;padding-top:1px;position:relative}.np-animated-label>*{height:0;opacity:0;position:absolute;transform:translateX(-8px);transition:all .3s ease-in}.np-animated-label--in{height:auto;opacity:1;position:relative;top:auto;transform:translateX(0);transition:all .3s ease-in .3s}.np-animated-label--out{position:relative;top:-24px;transform:translateX(8px)}
|
|
@@ -14,6 +14,12 @@ describe('AnimatedLabel', () => {
|
|
|
14
14
|
expect(container).toMatchSnapshot();
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
+
it('renders aria-label if provided', () => {
|
|
18
|
+
render(<AnimatedLabel {...props} aria-label="hello" />);
|
|
19
|
+
const checkbox = screen.getByLabelText('hello');
|
|
20
|
+
expect(checkbox).toBeInTheDocument();
|
|
21
|
+
});
|
|
22
|
+
|
|
17
23
|
it('renders only one label with class in', () => {
|
|
18
24
|
const { container } = render(<AnimatedLabel {...props} />);
|
|
19
25
|
expect(screen.getByText(props.labels[0])).toHaveClass('np-animated-label--in');
|
|
@@ -22,33 +28,41 @@ describe('AnimatedLabel', () => {
|
|
|
22
28
|
|
|
23
29
|
it('renders only one label with class out', () => {
|
|
24
30
|
const { container } = render(<AnimatedLabel {...props} />);
|
|
25
|
-
expect(screen.getByText(props.labels[1])).
|
|
26
|
-
expect(container.querySelectorAll('.np-animated-label--
|
|
31
|
+
expect(screen.getByText(props.labels[1])).toHaveClass('np-animated-label--out');
|
|
32
|
+
expect(container.querySelectorAll('.np-animated-label--out')).toHaveLength(1);
|
|
27
33
|
});
|
|
28
34
|
|
|
29
35
|
it('when activeLabel increase it switches class accordingly', () => {
|
|
30
36
|
const { rerender } = render(<AnimatedLabel {...props} />);
|
|
31
37
|
expect(screen.getByText(props.labels[0])).toHaveClass('np-animated-label--in');
|
|
32
|
-
expect(screen.getByText(props.labels[1])).
|
|
38
|
+
expect(screen.getByText(props.labels[1])).toHaveClass('np-animated-label--out');
|
|
39
|
+
|
|
40
|
+
expect(screen.getByText(props.labels[2])).not.toHaveClass('np-animated-label--out');
|
|
33
41
|
expect(screen.getByText(props.labels[2])).not.toHaveClass('np-animated-label--in');
|
|
34
42
|
|
|
35
43
|
rerender(<AnimatedLabel {...props} activeLabel={1} />);
|
|
36
44
|
|
|
45
|
+
expect(screen.getByText(props.labels[0])).not.toHaveClass('np-animated-label--out');
|
|
37
46
|
expect(screen.getByText(props.labels[0])).not.toHaveClass('np-animated-label--in');
|
|
47
|
+
|
|
38
48
|
expect(screen.getByText(props.labels[1])).toHaveClass('np-animated-label--in');
|
|
39
|
-
expect(screen.getByText(props.labels[2])).
|
|
49
|
+
expect(screen.getByText(props.labels[2])).toHaveClass('np-animated-label--out');
|
|
40
50
|
});
|
|
41
51
|
|
|
42
52
|
it('when activeLabel decrease it switches class accordingly', () => {
|
|
43
53
|
const { rerender } = render(<AnimatedLabel {...props} activeLabel={1} />);
|
|
44
|
-
expect(screen.getByText(props.labels[0])).not.toHaveClass('np-animated-label--in');
|
|
45
54
|
expect(screen.getByText(props.labels[1])).toHaveClass('np-animated-label--in');
|
|
46
|
-
expect(screen.getByText(props.labels[2])).
|
|
55
|
+
expect(screen.getByText(props.labels[2])).toHaveClass('np-animated-label--out');
|
|
56
|
+
|
|
57
|
+
expect(screen.getByText(props.labels[0])).not.toHaveClass('np-animated-label--out');
|
|
58
|
+
expect(screen.getByText(props.labels[0])).not.toHaveClass('np-animated-label--in');
|
|
47
59
|
|
|
48
60
|
rerender(<AnimatedLabel {...props} activeLabel={0} />);
|
|
49
61
|
|
|
50
62
|
expect(screen.getByText(props.labels[0])).toHaveClass('np-animated-label--in');
|
|
51
|
-
expect(screen.getByText(props.labels[1])).
|
|
63
|
+
expect(screen.getByText(props.labels[1])).toHaveClass('np-animated-label--out');
|
|
64
|
+
|
|
65
|
+
expect(screen.getByText(props.labels[2])).not.toHaveClass('np-animated-label--out');
|
|
52
66
|
expect(screen.getByText(props.labels[2])).not.toHaveClass('np-animated-label--in');
|
|
53
67
|
});
|
|
54
68
|
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { select } from '@storybook/addon-knobs';
|
|
2
|
+
|
|
3
|
+
import AnimatedLabel from './AnimatedLabel';
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
component: AnimatedLabel,
|
|
7
|
+
title: 'Internal/AnimatedLabel',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const Basic = () => {
|
|
11
|
+
const labels = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
|
12
|
+
const activeLabel = select('active label', labels, 10);
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div style={{ width: '120px' }} className="p-a-1">
|
|
16
|
+
<AnimatedLabel
|
|
17
|
+
labels={labels.map((element) => `label-${element}`)}
|
|
18
|
+
activeLabel={activeLabel}
|
|
19
|
+
/>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
@@ -1,32 +1,41 @@
|
|
|
1
1
|
import classNames from 'classnames';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
|
|
4
|
-
import Body from '../../body';
|
|
5
|
-
import { Typography } from '../../common';
|
|
6
|
-
|
|
7
4
|
export interface AnimatedLabelProps {
|
|
8
5
|
activeLabel: number;
|
|
9
6
|
className?: string;
|
|
10
7
|
labels: React.ReactNode[];
|
|
8
|
+
'aria-label': string;
|
|
11
9
|
}
|
|
12
10
|
|
|
13
|
-
const AnimatedLabel = ({
|
|
11
|
+
const AnimatedLabel = ({
|
|
12
|
+
activeLabel,
|
|
13
|
+
className,
|
|
14
|
+
labels,
|
|
15
|
+
'aria-label': ariaLabel,
|
|
16
|
+
}: AnimatedLabelProps) => {
|
|
17
|
+
const numberLabels = labels.length - 1;
|
|
18
|
+
|
|
14
19
|
return (
|
|
15
|
-
<
|
|
20
|
+
<div
|
|
21
|
+
aria-label={ariaLabel}
|
|
22
|
+
className={classNames('np-animated-label', 'np-text-body-large-bold', className)}
|
|
23
|
+
>
|
|
16
24
|
{labels.map((label, index) => {
|
|
17
25
|
const nextLabel = index - 1;
|
|
18
26
|
return (
|
|
19
27
|
<div
|
|
20
28
|
key={nextLabel}
|
|
21
|
-
className={classNames('text-xs-
|
|
22
|
-
'np-animated-label--in
|
|
29
|
+
className={classNames('text-xs-left', {
|
|
30
|
+
'np-animated-label--in': index === activeLabel,
|
|
31
|
+
'np-animated-label--out': nextLabel === activeLabel && nextLabel !== numberLabels,
|
|
23
32
|
})}
|
|
24
33
|
>
|
|
25
34
|
{label}
|
|
26
35
|
</div>
|
|
27
36
|
);
|
|
28
37
|
})}
|
|
29
|
-
</
|
|
38
|
+
</div>
|
|
30
39
|
);
|
|
31
40
|
};
|
|
32
41
|
|
|
@@ -3,20 +3,20 @@
|
|
|
3
3
|
exports[`AnimatedLabel renders all labels 1`] = `
|
|
4
4
|
<div>
|
|
5
5
|
<div
|
|
6
|
-
class="np-text-body-large-bold
|
|
6
|
+
class="np-animated-label np-text-body-large-bold"
|
|
7
7
|
>
|
|
8
8
|
<div
|
|
9
|
-
class="text-xs-
|
|
9
|
+
class="text-xs-left np-animated-label--in"
|
|
10
10
|
>
|
|
11
11
|
label1
|
|
12
12
|
</div>
|
|
13
13
|
<div
|
|
14
|
-
class="text-xs-
|
|
14
|
+
class="text-xs-left np-animated-label--out"
|
|
15
15
|
>
|
|
16
16
|
label2
|
|
17
17
|
</div>
|
|
18
18
|
<div
|
|
19
|
-
class="text-xs-
|
|
19
|
+
class="text-xs-left"
|
|
20
20
|
>
|
|
21
21
|
label3
|
|
22
22
|
</div>
|
|
@@ -2,23 +2,18 @@ import { ArrowLeft as ArrowLeftIcon } from '@transferwise/icons';
|
|
|
2
2
|
import classNames from 'classnames';
|
|
3
3
|
import PropTypes from 'prop-types';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
>
|
|
15
|
-
<ArrowLeftIcon size={24} />
|
|
16
|
-
</button>
|
|
17
|
-
</Avatar>
|
|
5
|
+
const BackButton = ({ label, className, onClick }) => (
|
|
6
|
+
<button
|
|
7
|
+
type="button"
|
|
8
|
+
className={classNames('np-back-button', 'align-items-center', 'btn-unstyled', className)}
|
|
9
|
+
onClick={onClick}
|
|
10
|
+
>
|
|
11
|
+
<ArrowLeftIcon size={24} />
|
|
12
|
+
{label}
|
|
13
|
+
</button>
|
|
18
14
|
);
|
|
19
15
|
|
|
20
16
|
BackButton.propTypes = {
|
|
21
|
-
'aria-label': PropTypes.string.isRequired,
|
|
22
17
|
className: PropTypes.string,
|
|
23
18
|
label: PropTypes.element,
|
|
24
19
|
onClick: PropTypes.func,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
3
|
|
|
4
4
|
import BackButton from '.';
|
|
5
5
|
|
|
@@ -7,7 +7,6 @@ const props = {
|
|
|
7
7
|
label: <>label</>,
|
|
8
8
|
className: 'className',
|
|
9
9
|
onClick: jest.fn(),
|
|
10
|
-
'aria-label': messages.back.defaultMessage,
|
|
11
10
|
};
|
|
12
11
|
describe('BackButton', () => {
|
|
13
12
|
it(`renders as expected`, () => {
|
|
@@ -2,36 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
exports[`BackButton renders as expected 1`] = `
|
|
4
4
|
<div>
|
|
5
|
-
<
|
|
6
|
-
class="
|
|
5
|
+
<button
|
|
6
|
+
class="np-back-button align-items-center btn-unstyled className"
|
|
7
|
+
type="button"
|
|
7
8
|
>
|
|
8
|
-
<
|
|
9
|
-
|
|
9
|
+
<span
|
|
10
|
+
aria-hidden="true"
|
|
11
|
+
class="tw-icon tw-icon-arrow-left "
|
|
12
|
+
data-testid="arrow-left-icon"
|
|
13
|
+
role="presentation"
|
|
10
14
|
>
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
<svg
|
|
16
|
+
fill="currentColor"
|
|
17
|
+
focusable="false"
|
|
18
|
+
height="24"
|
|
19
|
+
viewBox="0 0 24 24"
|
|
20
|
+
width="24"
|
|
14
21
|
>
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
fill="currentColor"
|
|
23
|
-
focusable="false"
|
|
24
|
-
height="24"
|
|
25
|
-
viewBox="0 0 24 24"
|
|
26
|
-
width="24"
|
|
27
|
-
>
|
|
28
|
-
<path
|
|
29
|
-
d="M22.286 11.316H4.629l7.114-7.114-1.2-1.2-8.572 8.571a.829.829 0 0 0 0 1.2l8.572 8.572 1.2-1.2-7.114-7.114h17.657v-1.715Z"
|
|
30
|
-
/>
|
|
31
|
-
</svg>
|
|
32
|
-
</span>
|
|
33
|
-
</button>
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
22
|
+
<path
|
|
23
|
+
d="M22.286 11.316H4.629l7.114-7.114-1.2-1.2-8.572 8.571a.829.829 0 0 0 0 1.2l8.572 8.572 1.2-1.2-7.114-7.114h17.657v-1.715Z"
|
|
24
|
+
/>
|
|
25
|
+
</svg>
|
|
26
|
+
</span>
|
|
27
|
+
label
|
|
28
|
+
</button>
|
|
36
29
|
</div>
|
|
37
30
|
`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
.np-bottom-sheet-v2-container{position:relative;z-index:1060}.np-bottom-sheet-v2-backdrop-container--enter,.np-bottom-sheet-v2-backdrop-container--leave{transition-duration:.
|
|
1
|
+
.np-bottom-sheet-v2-container{position:relative;z-index:1060}.np-bottom-sheet-v2-backdrop-container--enter,.np-bottom-sheet-v2-backdrop-container--leave{transition-duration:.3s;transition-property:opacity;transition-timing-function:ease-out}.np-bottom-sheet-v2-backdrop-container--enter-from,.np-bottom-sheet-v2-backdrop-container--leave-to{opacity:0}.np-bottom-sheet-v2-backdrop{background-color:#37517e;background-color:var(--color-content-primary);inset:0;opacity:.4;position:fixed}.np-bottom-sheet-v2{display:flex;flex-direction:column;inset:0;justify-content:flex-end;padding-left:8px;padding-left:var(--size-8);padding-right:8px;padding-right:var(--size-8);padding-top:64px;padding-top:var(--size-64);position:fixed}.np-bottom-sheet-v2-content{max-height:100%}.np-bottom-sheet-v2-content--enter,.np-bottom-sheet-v2-content--leave{transition-duration:.3s;transition-property:transform;transition-timing-function:ease-out}@media (prefers-reduced-motion:reduce){.np-bottom-sheet-v2-content--enter,.np-bottom-sheet-v2-content--leave{transition-property:opacity}}@media (prefers-reduced-motion:no-preference){.np-bottom-sheet-v2-content--enter-from,.np-bottom-sheet-v2-content--leave-to{transform:translateY(100%)}}@media (prefers-reduced-motion:reduce){.np-bottom-sheet-v2-content--enter-from,.np-bottom-sheet-v2-content--leave-to{opacity:0}}.np-bottom-sheet-v2-content-inner-container{background-color:#fff;background-color:var(--color-background-elevated);border-top-left-radius:32px;border-top-right-radius:32px;box-shadow:0 0 40px rgba(69,71,69,.2);display:flex;flex-direction:column;height:100%}.np-bottom-sheet-v2-content-inner-container:focus{outline:none}.np-bottom-sheet-v2-header{align-self:flex-end;padding:16px;padding:var(--size-16)}.np-bottom-sheet-v2-content-inner{display:grid;grid-template-rows:repeat(1,minmax(0,1fr));overflow-y:auto;padding-top:0;row-gap:8px;row-gap:var(--size-8)}.np-bottom-sheet-v2-content-inner--has-title{grid-template-rows:auto 1fr}.np-bottom-sheet-v2-content-inner--padding-md{padding:16px;padding:var(--size-16)}.np-bottom-sheet-v2-title{color:#37517e;color:var(--color-content-primary)}.np-bottom-sheet-v2-body{color:#5d7079;color:var(--color-content-secondary)}.np-button-input{align-content:center;border-radius:10px;border-radius:var(--size-10);display:inline-grid;grid-auto-columns:minmax(0,1fr);text-align:start}.np-popover-v2-container{background-color:#fff;background-color:var(--color-background-elevated);border-radius:10px;border-radius:var(--radius-small);box-shadow:0 0 40px rgba(69,71,69,.2);display:flex;flex-direction:column;max-height:var(--max-height);min-width:20rem;width:var(--width);z-index:1060}.np-popover-v2-container:focus{outline:none}.np-popover-v2{display:grid;grid-template-rows:repeat(1,minmax(0,1fr));overflow-y:auto;row-gap:8px;row-gap:var(--size-8)}.np-popover-v2--has-title{grid-template-rows:auto 1fr}.np-popover-v2--padding-md{padding:16px;padding:var(--size-16)}.np-popover-v2-title{color:#37517e;color:var(--color-content-primary)}.np-popover-v2-content{color:#5d7079;color:var(--color-content-secondary)}.np-select-input-placeholder{color:#768e9c;color:var(--color-content-tertiary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.np-select-input-options-container{display:flex;flex-direction:column;height:100%}.np-select-input-options-container:focus{outline:none}@media (min-width:576px){.np-select-input-options-container{max-height:28rem}}.np-select-input-options-status{align-items:center;-moz-column-gap:8px;column-gap:8px;-moz-column-gap:var(--size-8);column-gap:var(--size-8);display:flex;padding:8px 24px 0;padding:var(--size-8) var(--size-24) 0}.np-select-input-options-status-icon{color:#768e9c;color:var(--color-content-tertiary);padding:0 4px;padding:0 var(--size-4)}.np-select-input-query-container{display:flex;flex-direction:column;padding:8px;padding:var(--size-8);padding-top:0}@media (min-width:576px){.np-select-input-query-container{padding-top:8px;padding-top:var(--size-8)}}.np-select-input-listbox-container{height:var(--initial-height);overflow-y:auto;position:relative;scroll-padding-bottom:8px;scroll-padding-bottom:var(--size-8);scroll-padding-top:8px;scroll-padding-top:var(--size-8)}@media (min-width:576px){.np-select-input-listbox-container{height:auto}}.np-select-input-listbox-container--has-group{scroll-padding-top:32px;scroll-padding-top:var(--size-32)}.np-select-input-listbox{padding:8px;padding:var(--size-8)}.np-select-input-listbox:focus{outline:none}.np-select-input-separator-item{border-top-width:1px;margin:8px;margin:var(--size-8)}.np-select-input-group-item--without-needle:first-child{margin-top:-8px;margin-top:calc(var(--size-8)*-1)}.np-select-input-group-item-header{background-color:#fff;background-color:var(--color-background-elevated);color:#5d7079;color:var(--color-content-secondary);padding:8px 16px 4px;padding:var(--size-8) var(--size-16) var(--size-4);position:sticky;top:0;z-index:10}.np-select-input-option-container{align-items:center;border-radius:10px;border-radius:var(--radius-small);color:var(--color-interactive-primary);-moz-column-gap:8px;column-gap:8px;-moz-column-gap:var(--size-8);column-gap:var(--size-8);cursor:default;display:flex;padding:12px 16px;padding:var(--size-12) var(--size-16);-webkit-user-select:none;-moz-user-select:none;user-select:none}.np-select-input-option-container--active{box-shadow:inset 0 0 0 1px #c9cbce;box-shadow:inset 0 0 0 1px var(--color-interactive-secondary)}.np-select-input-option-container--disabled{opacity:.45}.np-select-input-option-check--not-selected{visibility:hidden}.np-select-input-option{flex:1}.np-select-input-option-content-container{align-items:center;color:#37517e;color:var(--color-content-primary);-moz-column-gap:8px;column-gap:8px;-moz-column-gap:var(--size-8);column-gap:var(--size-8);display:flex}.np-select-input-option-content-icon{display:flex}.np-select-input-option-content-icon--not-within-trigger{align-self:flex-start}.np-select-input-option-content-text{display:flex;flex:1;flex-direction:column;overflow:hidden}.np-select-input-option-content-text-primary{font:inherit}.np-select-input-option-content-text-secondary{color:#5d7079;color:var(--color-content-secondary)}.np-select-input-option-content-text-within-trigger{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.np-select-input-option-content-text-line-1>:not([hidden])~:not([hidden]){margin-left:8px;margin-left:var(--size-8);margin-right:8px;margin-right:var(--size-8)}.np-select-input-footer{padding:4px 24px 16px;padding:var(--size-4) var(--size-24) var(--size-16)}.np-select-input-addon-container{align-items:center;display:inline-flex;pointer-events:none}.np-select-input-addon-container,.np-select-input-addon-container>:not([hidden])~:not([hidden]){margin-inline-start:4px;margin-inline-start:var(--size-4)}.np-select-input-addon{align-items:center;background:none;border-radius:.125rem;border-width:0;display:inline-flex;height:24px;height:var(--size-24);justify-content:center;width:24px;width:var(--size-24)}.np-select-input-addon--interactive{color:#c9cbce;color:var(--color-interactive-secondary);pointer-events:auto}.np-select-input-addon--interactive:hover{color:#b5b7ba;color:var(--color-interactive-secondary-hover)}.np-select-input-addon--interactive:focus{outline:none}.np-select-input-addon--interactive:focus-visible{outline:var(--ring-outline-color) solid var(--ring-outline-width);outline-offset:var(--ring-outline-offset)}.np-select-input-addon-separator{border-inline-start:1px solid #0000001a;border-inline-start:1px solid var(--color-border-neutral);height:24px;height:var(--size-24)}
|
|
@@ -47,6 +47,46 @@ describe('SelectInput', () => {
|
|
|
47
47
|
expect(screen.getByText('Currency')).toBeInTheDocument();
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
+
it('renders footer', async () => {
|
|
51
|
+
render(
|
|
52
|
+
<SelectInput
|
|
53
|
+
items={[
|
|
54
|
+
{ type: 'option', value: 'USD' },
|
|
55
|
+
{ type: 'option', value: 'EUR' },
|
|
56
|
+
]}
|
|
57
|
+
renderFooter={(normalizedQuery) =>
|
|
58
|
+
normalizedQuery != null ? (
|
|
59
|
+
<>Showing results for ‘{normalizedQuery}’</>
|
|
60
|
+
) : (
|
|
61
|
+
<>All items shown</>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
filterable
|
|
65
|
+
/>,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
69
|
+
await act(async () => {
|
|
70
|
+
userEvent.tab();
|
|
71
|
+
userEvent.keyboard(specialChars.enter);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const footer = screen.getByText('All items shown');
|
|
75
|
+
expect(footer).toBeInTheDocument();
|
|
76
|
+
|
|
77
|
+
userEvent.keyboard('u');
|
|
78
|
+
expect(footer).toHaveTextContent(/‘u’$/);
|
|
79
|
+
|
|
80
|
+
userEvent.keyboard('r');
|
|
81
|
+
expect(footer).toHaveTextContent(/‘ur’$/);
|
|
82
|
+
|
|
83
|
+
userEvent.keyboard('x');
|
|
84
|
+
expect(footer).toHaveTextContent(/‘urx’$/);
|
|
85
|
+
|
|
86
|
+
userEvent.keyboard(specialChars.backspace);
|
|
87
|
+
expect(footer).toHaveTextContent(/‘ur’$/);
|
|
88
|
+
});
|
|
89
|
+
|
|
50
90
|
it('shows item selected via mouse', async () => {
|
|
51
91
|
render(
|
|
52
92
|
<SelectInput
|
|
@@ -266,6 +266,24 @@ export const Currencies: StoryObj<{
|
|
|
266
266
|
icon={<Flag code={currency.code} intrinsicSize={24} />}
|
|
267
267
|
/>
|
|
268
268
|
)}
|
|
269
|
+
renderFooter={(normalizedQuery) =>
|
|
270
|
+
normalizedQuery != null && normalizedQuery.length === 3 ? (
|
|
271
|
+
<>
|
|
272
|
+
It’s not possible use {normalizedQuery.toUpperCase()} yet.{' '}
|
|
273
|
+
<a href="#_" className="np-text-link-default">
|
|
274
|
+
Email me when it’s available.
|
|
275
|
+
</a>
|
|
276
|
+
</>
|
|
277
|
+
) : (
|
|
278
|
+
<>
|
|
279
|
+
Can’t find it?{' '}
|
|
280
|
+
<a href="#_" className="np-text-link-default">
|
|
281
|
+
Request the currency you need,
|
|
282
|
+
</a>{' '}
|
|
283
|
+
and we’ll notify you once it’s available.
|
|
284
|
+
</>
|
|
285
|
+
)
|
|
286
|
+
}
|
|
269
287
|
filterable
|
|
270
288
|
filterPlaceholder="Type a currency / country"
|
|
271
289
|
size="lg"
|
|
@@ -135,6 +135,7 @@ export interface SelectInputProps<T = string> {
|
|
|
135
135
|
| (keyof NonNullable<T> & string)
|
|
136
136
|
| ((a: T | undefined, b: T | undefined) => boolean);
|
|
137
137
|
renderValue?: (value: NonNullable<T>, withinTrigger: boolean) => React.ReactNode;
|
|
138
|
+
renderFooter?: (normalizedQuery: string | null | undefined) => React.ReactNode;
|
|
138
139
|
renderTrigger?: (args: {
|
|
139
140
|
content: React.ReactNode;
|
|
140
141
|
placeholderShown: boolean;
|
|
@@ -211,6 +212,7 @@ export function SelectInput<T = string>({
|
|
|
211
212
|
value: controlledValue,
|
|
212
213
|
compareValues,
|
|
213
214
|
renderValue = wrapInFragment,
|
|
215
|
+
renderFooter,
|
|
214
216
|
renderTrigger = defaultRenderTrigger,
|
|
215
217
|
filterable,
|
|
216
218
|
filterPlaceholder,
|
|
@@ -296,6 +298,7 @@ export function SelectInput<T = string>({
|
|
|
296
298
|
<SelectInputOptions
|
|
297
299
|
items={items}
|
|
298
300
|
renderValue={renderValue}
|
|
301
|
+
renderFooter={renderFooter}
|
|
299
302
|
filterable={filterable}
|
|
300
303
|
filterPlaceholder={filterPlaceholder}
|
|
301
304
|
searchInputRef={searchInputRef}
|
|
@@ -351,17 +354,22 @@ const SelectInputOptionsContainer = forwardRef(function SelectInputOptionsContai
|
|
|
351
354
|
}, [ariaActiveDescendant, handleAriaActiveDescendantChange]);
|
|
352
355
|
|
|
353
356
|
return (
|
|
354
|
-
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
|
355
357
|
<div
|
|
356
358
|
ref={ref}
|
|
359
|
+
role="none"
|
|
357
360
|
onKeyDown={(event) => {
|
|
358
|
-
// Prevent
|
|
359
|
-
if (event.key === '
|
|
361
|
+
// Prevent confirmation close without an active item
|
|
362
|
+
if (event.key === 'Enter' && ariaActiveDescendant == null) {
|
|
360
363
|
return;
|
|
361
364
|
}
|
|
362
365
|
|
|
363
|
-
// Prevent
|
|
364
|
-
if (event.key === '
|
|
366
|
+
// Prevent absorbing actions early
|
|
367
|
+
if (event.key === 'Escape' || event.key === 'Tab') {
|
|
368
|
+
onKeyDown?.({
|
|
369
|
+
...event,
|
|
370
|
+
preventDefault: () => {},
|
|
371
|
+
stopPropagation: () => {},
|
|
372
|
+
});
|
|
365
373
|
return;
|
|
366
374
|
}
|
|
367
375
|
|
|
@@ -373,7 +381,10 @@ const SelectInputOptionsContainer = forwardRef(function SelectInputOptionsContai
|
|
|
373
381
|
});
|
|
374
382
|
|
|
375
383
|
interface SelectInputOptionsProps<T = string>
|
|
376
|
-
extends Pick<
|
|
384
|
+
extends Pick<
|
|
385
|
+
SelectInputProps<T>,
|
|
386
|
+
'items' | 'renderValue' | 'renderFooter' | 'filterable' | 'filterPlaceholder'
|
|
387
|
+
> {
|
|
377
388
|
searchInputRef: React.RefObject<HTMLInputElement>;
|
|
378
389
|
listboxRef: React.RefObject<HTMLDivElement>;
|
|
379
390
|
}
|
|
@@ -381,6 +392,7 @@ interface SelectInputOptionsProps<T = string>
|
|
|
381
392
|
function SelectInputOptions<T = string>({
|
|
382
393
|
items,
|
|
383
394
|
renderValue = wrapInFragment,
|
|
395
|
+
renderFooter,
|
|
384
396
|
filterable = false,
|
|
385
397
|
filterPlaceholder,
|
|
386
398
|
searchInputRef,
|
|
@@ -451,7 +463,7 @@ function SelectInputOptions<T = string>({
|
|
|
451
463
|
</div>
|
|
452
464
|
) : null}
|
|
453
465
|
|
|
454
|
-
<
|
|
466
|
+
<section
|
|
455
467
|
ref={listboxContainerRef}
|
|
456
468
|
className={classNames(
|
|
457
469
|
'np-select-input-listbox-container',
|
|
@@ -471,7 +483,7 @@ function SelectInputOptions<T = string>({
|
|
|
471
483
|
id={listboxId}
|
|
472
484
|
role="listbox"
|
|
473
485
|
aria-orientation="vertical"
|
|
474
|
-
tabIndex={0}
|
|
486
|
+
tabIndex={!filterable ? 0 : undefined}
|
|
475
487
|
className="np-select-input-listbox"
|
|
476
488
|
>
|
|
477
489
|
{(needle != null ? dedupeSelectInputItems(items) : items).map((item, index) => (
|
|
@@ -484,7 +496,23 @@ function SelectInputOptions<T = string>({
|
|
|
484
496
|
/>
|
|
485
497
|
))}
|
|
486
498
|
</div>
|
|
487
|
-
|
|
499
|
+
|
|
500
|
+
{renderFooter != null ? (
|
|
501
|
+
<footer className="np-select-input-footer">
|
|
502
|
+
<div
|
|
503
|
+
role="none"
|
|
504
|
+
onKeyDown={(event) => {
|
|
505
|
+
// Prevent interfering with Headless UI
|
|
506
|
+
if (event.key !== 'Escape') {
|
|
507
|
+
event.stopPropagation();
|
|
508
|
+
}
|
|
509
|
+
}}
|
|
510
|
+
>
|
|
511
|
+
{renderFooter(needle)}
|
|
512
|
+
</div>
|
|
513
|
+
</footer>
|
|
514
|
+
) : null}
|
|
515
|
+
</section>
|
|
488
516
|
</ListboxBase.Options>
|
|
489
517
|
);
|
|
490
518
|
}
|
|
@@ -547,7 +575,7 @@ function SelectInputGroupItemView<T = string>({
|
|
|
547
575
|
{needle == null ? (
|
|
548
576
|
<header
|
|
549
577
|
id={headerId}
|
|
550
|
-
role="
|
|
578
|
+
role="none"
|
|
551
579
|
className="np-select-input-group-item-header np-text-title-group"
|
|
552
580
|
>
|
|
553
581
|
{item.label}
|