@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.
Files changed (49) hide show
  1. package/build/index.esm.js +91 -68
  2. package/build/index.esm.js.map +1 -1
  3. package/build/index.js +91 -68
  4. package/build/index.js.map +1 -1
  5. package/build/main.css +1 -1
  6. package/build/styles/flowNavigation/FlowNavigation.css +1 -1
  7. package/build/styles/flowNavigation/animatedLabel/AnimatedLabel.css +1 -1
  8. package/build/styles/inputs/SelectInput.css +1 -1
  9. package/build/styles/main.css +1 -1
  10. package/build/styles/overlayHeader/OverlayHeader.css +1 -1
  11. package/build/types/common/flowHeader/FlowHeader.d.ts.map +1 -1
  12. package/build/types/flowNavigation/FlowNavigation.d.ts.map +1 -1
  13. package/build/types/flowNavigation/animatedLabel/AnimatedLabel.d.ts +2 -1
  14. package/build/types/flowNavigation/animatedLabel/AnimatedLabel.d.ts.map +1 -1
  15. package/build/types/flowNavigation/backButton/BackButton.d.ts +11 -10
  16. package/build/types/flowNavigation/backButton/BackButton.d.ts.map +1 -1
  17. package/build/types/inputs/SelectInput.d.ts +2 -1
  18. package/build/types/inputs/SelectInput.d.ts.map +1 -1
  19. package/build/types/overlayHeader/OverlayHeader.d.ts.map +1 -1
  20. package/package.json +1 -1
  21. package/src/common/flowHeader/FlowHeader.tsx +22 -4
  22. package/src/common/flowHeader/__snapshots__/FlowHeader.spec.js.snap +22 -6
  23. package/src/flowNavigation/FlowNavigation.css +1 -1
  24. package/src/flowNavigation/FlowNavigation.less +9 -0
  25. package/src/flowNavigation/FlowNavigation.spec.js +3 -3
  26. package/src/flowNavigation/FlowNavigation.story.js +189 -22
  27. package/src/flowNavigation/FlowNavigation.tsx +27 -22
  28. package/src/flowNavigation/__snapshots__/FlowNavigation.spec.js.snap +26 -18
  29. package/src/flowNavigation/animatedLabel/AnimatedLabel.css +1 -1
  30. package/src/flowNavigation/animatedLabel/AnimatedLabel.less +6 -0
  31. package/src/flowNavigation/animatedLabel/AnimatedLabel.spec.js +21 -7
  32. package/src/flowNavigation/animatedLabel/AnimatedLabel.story.js +22 -0
  33. package/src/flowNavigation/animatedLabel/AnimatedLabel.tsx +17 -8
  34. package/src/flowNavigation/animatedLabel/__snapshots__/AnimatedLabel.spec.js.snap +4 -4
  35. package/src/flowNavigation/backButton/BackButton.js +9 -14
  36. package/src/flowNavigation/backButton/BackButton.spec.js +2 -3
  37. package/src/flowNavigation/backButton/__snapshots__/BackButton.spec.js.snap +21 -28
  38. package/src/inputs/SelectInput.css +1 -1
  39. package/src/inputs/SelectInput.less +4 -0
  40. package/src/inputs/SelectInput.spec.tsx +40 -0
  41. package/src/inputs/SelectInput.story.tsx +18 -0
  42. package/src/inputs/SelectInput.tsx +38 -10
  43. package/src/inputs/_BottomSheet.less +1 -1
  44. package/src/main.css +1 -1
  45. package/src/overlayHeader/OverlayHeader.css +1 -1
  46. package/src/overlayHeader/OverlayHeader.js +4 -6
  47. package/src/overlayHeader/OverlayHeader.less +8 -0
  48. package/src/overlayHeader/OverlayHeader.spec.js +1 -1
  49. 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 justify-content-between flex__item--12 np-flow-navigation__content p-x-3 np-flow-navigation--xs-max"
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="m-x-1"
16
- data-testid="activeLabel-1"
12
+ class="np-flow-header__left flex__item--8"
17
13
  >
18
- AnimatedLabel
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 align-items-center"
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="m-x-1"
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 justify-content-between flex__item--12 np-flow-navigation__content p-x-3 np-flow-navigation--sm np-flow-navigation--lg"
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="d-flex align-items-center order-2"
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="m-x-1"
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)}
@@ -28,4 +28,10 @@
28
28
  transform: translateX(0);
29
29
  transition: all @transition-duration ease-in @transition-duration;
30
30
  }
31
+
32
+ &--out {
33
+ position: relative;
34
+ top: -@label-height;
35
+ transform: translateX(@slide-length);
36
+ }
31
37
  }
@@ -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])).not.toHaveClass('np-animated-label--in');
26
- expect(container.querySelectorAll('.np-animated-label--in')).toHaveLength(1);
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])).not.toHaveClass('np-animated-label--in');
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])).not.toHaveClass('np-animated-label--in');
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])).not.toHaveClass('np-animated-label--in');
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])).not.toHaveClass('np-animated-label--in');
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 = ({ activeLabel, className, labels }: AnimatedLabelProps) => {
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
- <Body type={Typography.BODY_LARGE_BOLD} className={classNames('np-animated-label', className)}>
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-center', {
22
- 'np-animated-label--in text-ellipsis': index === activeLabel,
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
- </Body>
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 np-animated-label"
6
+ class="np-animated-label np-text-body-large-bold"
7
7
  >
8
8
  <div
9
- class="text-xs-center np-animated-label--in text-ellipsis"
9
+ class="text-xs-left np-animated-label--in"
10
10
  >
11
11
  label1
12
12
  </div>
13
13
  <div
14
- class="text-xs-center"
14
+ class="text-xs-left np-animated-label--out"
15
15
  >
16
16
  label2
17
17
  </div>
18
18
  <div
19
- class="text-xs-center"
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
- import Avatar, { AvatarType } from '../../avatar';
6
-
7
- const BackButton = ({ className, onClick, 'aria-label': ariaLabel }) => (
8
- <Avatar type={AvatarType.ICON} size={40}>
9
- <button
10
- type="button"
11
- aria-label={ariaLabel}
12
- className={classNames('np-back-button', 'btn-unstyled', className)}
13
- onClick={onClick}
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 { render } from '../../test-utils';
2
- import messages from '../FlowNavigation.messages';
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
- <div
6
- class="tw-avatar tw-avatar--40 tw-avatar--icon"
5
+ <button
6
+ class="np-back-button align-items-center btn-unstyled className"
7
+ type="button"
7
8
  >
8
- <div
9
- class="tw-avatar__content"
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
- <button
12
- class="np-back-button btn-unstyled className"
13
- type="button"
15
+ <svg
16
+ fill="currentColor"
17
+ focusable="false"
18
+ height="24"
19
+ viewBox="0 0 24 24"
20
+ width="24"
14
21
  >
15
- <span
16
- aria-hidden="true"
17
- class="tw-icon tw-icon-arrow-left "
18
- data-testid="arrow-left-icon"
19
- role="presentation"
20
- >
21
- <svg
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:.15s;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-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)}
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)}
@@ -169,6 +169,10 @@
169
169
  }
170
170
  }
171
171
 
172
+ .np-select-input-footer {
173
+ padding: var(--size-4) var(--size-24) var(--size-16);
174
+ }
175
+
172
176
  .np-select-input-addon-container {
173
177
  pointer-events: none;
174
178
  margin-inline-start: var(--size-4);
@@ -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 absorbing dismissal requests too early
359
- if (event.key === 'Escape') {
361
+ // Prevent confirmation close without an active item
362
+ if (event.key === 'Enter' && ariaActiveDescendant == null) {
360
363
  return;
361
364
  }
362
365
 
363
- // Prevent confirmation close without an active item
364
- if (event.key === 'Enter' && ariaActiveDescendant == null) {
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<SelectInputProps<T>, 'items' | 'renderValue' | 'filterable' | 'filterPlaceholder'> {
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
- <div
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
- </div>
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="presentation"
578
+ role="none"
551
579
  className="np-select-input-group-item-header np-text-title-group"
552
580
  >
553
581
  {item.label}
@@ -7,7 +7,7 @@
7
7
  &--enter, &--leave {
8
8
  transition-property: opacity;
9
9
  transition-timing-function: ease-out;
10
- transition-duration: 150ms;
10
+ transition-duration: 300ms;
11
11
  }
12
12
 
13
13
  &--enter-from, &--leave-to {