@transferwise/components 46.65.0 → 46.67.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/build/decision/Decision.js +4 -0
  2. package/build/decision/Decision.js.map +1 -1
  3. package/build/decision/Decision.mjs +4 -0
  4. package/build/decision/Decision.mjs.map +1 -1
  5. package/build/i18n/en.json +1 -0
  6. package/build/i18n/en.json.js +1 -0
  7. package/build/i18n/en.json.js.map +1 -1
  8. package/build/i18n/en.json.mjs +1 -0
  9. package/build/i18n/en.json.mjs.map +1 -1
  10. package/build/inputs/SelectInput.js +4 -0
  11. package/build/inputs/SelectInput.js.map +1 -1
  12. package/build/inputs/SelectInput.mjs +4 -0
  13. package/build/inputs/SelectInput.mjs.map +1 -1
  14. package/build/main.css +0 -24
  15. package/build/moneyInput/MoneyInput.js +8 -1
  16. package/build/moneyInput/MoneyInput.js.map +1 -1
  17. package/build/moneyInput/MoneyInput.messages.js +3 -0
  18. package/build/moneyInput/MoneyInput.messages.js.map +1 -1
  19. package/build/moneyInput/MoneyInput.messages.mjs +3 -0
  20. package/build/moneyInput/MoneyInput.messages.mjs.map +1 -1
  21. package/build/moneyInput/MoneyInput.mjs +8 -1
  22. package/build/moneyInput/MoneyInput.mjs.map +1 -1
  23. package/build/stepper/Stepper.js +12 -1
  24. package/build/stepper/Stepper.js.map +1 -1
  25. package/build/stepper/Stepper.mjs +12 -1
  26. package/build/stepper/Stepper.mjs.map +1 -1
  27. package/build/styles/main.css +0 -24
  28. package/build/styles/stepper/Stepper.css +0 -24
  29. package/build/tile/Tile.js +2 -0
  30. package/build/tile/Tile.js.map +1 -1
  31. package/build/tile/Tile.mjs +2 -0
  32. package/build/tile/Tile.mjs.map +1 -1
  33. package/build/types/decision/Decision.d.ts +1 -0
  34. package/build/types/decision/Decision.d.ts.map +1 -1
  35. package/build/types/inputs/SelectInput.d.ts +3 -1
  36. package/build/types/inputs/SelectInput.d.ts.map +1 -1
  37. package/build/types/moneyInput/MoneyInput.d.ts.map +1 -1
  38. package/build/types/moneyInput/MoneyInput.messages.d.ts +5 -0
  39. package/build/types/moneyInput/MoneyInput.messages.d.ts.map +1 -1
  40. package/build/types/stepper/Stepper.d.ts.map +1 -1
  41. package/build/types/tile/Tile.d.ts +3 -1
  42. package/build/types/tile/Tile.d.ts.map +1 -1
  43. package/package.json +3 -3
  44. package/src/decision/Decision.spec.tsx +166 -0
  45. package/src/decision/Decision.story.tsx +208 -202
  46. package/src/decision/Decision.tsx +26 -2
  47. package/src/flowNavigation/__snapshots__/FlowNavigation.spec.js.snap +1 -2
  48. package/src/i18n/en.json +1 -0
  49. package/src/inputs/SelectInput.tsx +8 -3
  50. package/src/main.css +0 -24
  51. package/src/moneyInput/MoneyInput.docs.mdx +34 -0
  52. package/src/moneyInput/MoneyInput.messages.ts +5 -0
  53. package/src/moneyInput/MoneyInput.rtl.spec.tsx +47 -0
  54. package/src/moneyInput/MoneyInput.spec.js +0 -11
  55. package/src/moneyInput/MoneyInput.story.tsx +10 -7
  56. package/src/moneyInput/MoneyInput.tsx +8 -1
  57. package/src/stepper/Stepper.css +0 -24
  58. package/src/stepper/Stepper.less +0 -17
  59. package/src/stepper/Stepper.spec.js +11 -5
  60. package/src/stepper/Stepper.tsx +13 -2
  61. package/src/tile/Tile.tsx +4 -0
  62. package/src/withId/withId.story.tsx +1 -0
  63. package/src/decision/Decision.spec.js +0 -127
@@ -68,7 +68,6 @@ exports[`FlowNavigation on mobile renders as expected 1`] = `
68
68
  >
69
69
  <div
70
70
  class="progress-bar"
71
- style="width: 50%;"
72
71
  />
73
72
  </div>
74
73
  <ol
@@ -184,7 +183,7 @@ exports[`FlowNavigation renders as expected 1`] = `
184
183
  >
185
184
  <div
186
185
  class="progress-bar"
187
- style="width: 0%;"
186
+ style="width: 0px;"
188
187
  />
189
188
  </div>
190
189
  <ol
package/src/i18n/en.json CHANGED
@@ -21,6 +21,7 @@
21
21
  "neptune.Label.optional": "(Optional)",
22
22
  "neptune.Link.opensInNewTab": "(opens in new tab)",
23
23
  "neptune.MoneyInput.Select.placeholder": "Select an option...",
24
+ "neptune.MoneyInput.Select.selectCurrencyLabel": "Select currency",
24
25
  "neptune.PhoneNumberInput.SelectInput.placeholder": "Select an option...",
25
26
  "neptune.Select.searchPlaceholder": "Search...",
26
27
  "neptune.SelectInput.noResultsFound": "No results found",
@@ -165,7 +165,9 @@ export interface SelectInputProps<T = string, M extends boolean = false> {
165
165
  disabled?: boolean;
166
166
  size?: 'sm' | 'md' | 'lg';
167
167
  className?: string;
168
- UNSAFE_triggerButtonProps?: WithInputAttributesProps['inputAttributes'];
168
+ UNSAFE_triggerButtonProps?: WithInputAttributesProps['inputAttributes'] & {
169
+ 'aria-label'?: string;
170
+ };
169
171
  onFilterChange?: (args: { query: string; queryNormalized: string | null }) => void;
170
172
  onChange?: (value: M extends true ? T[] : T) => void;
171
173
  onClose?: () => void;
@@ -258,7 +260,6 @@ export function SelectInput<T = string, M extends boolean = false>({
258
260
  }: SelectInputProps<T, M>) {
259
261
  const inputAttributes = useInputAttributes();
260
262
  const id = idProp ?? inputAttributes.id;
261
-
262
263
  const [open, setOpen] = useState(false);
263
264
 
264
265
  const initialized = useRef(false);
@@ -387,6 +388,7 @@ export function SelectInput<T = string, M extends boolean = false>({
387
388
  }}
388
389
  >
389
390
  <SelectInputOptions
391
+ id={`${id}Search`}
390
392
  items={items}
391
393
  renderValue={renderValue}
392
394
  renderFooter={renderFooter}
@@ -488,7 +490,7 @@ const SelectInputOptionsContainer = forwardRef(function SelectInputOptionsContai
488
490
  interface SelectInputOptionsProps<T = string>
489
491
  extends Pick<
490
492
  SelectInputProps<T>,
491
- 'items' | 'renderValue' | 'renderFooter' | 'filterable' | 'filterPlaceholder'
493
+ 'items' | 'renderValue' | 'renderFooter' | 'filterable' | 'filterPlaceholder' | 'id'
492
494
  > {
493
495
  searchInputRef: React.MutableRefObject<HTMLInputElement | null>;
494
496
  listboxRef: React.MutableRefObject<HTMLDivElement | null>;
@@ -497,6 +499,7 @@ interface SelectInputOptionsProps<T = string>
497
499
  }
498
500
 
499
501
  function SelectInputOptions<T = string>({
502
+ id,
500
503
  items,
501
504
  renderValue = String,
502
505
  renderFooter,
@@ -609,9 +612,11 @@ function SelectInputOptions<T = string>({
609
612
  <div className="np-select-input-query-container">
610
613
  <SearchInput
611
614
  ref={searchInputRef}
615
+ id={id}
612
616
  role="combobox"
613
617
  shape="rectangle"
614
618
  placeholder={filterPlaceholder}
619
+ aria-label={filterPlaceholder}
615
620
  defaultValue={filterQuery}
616
621
  aria-autocomplete="list"
617
622
  aria-expanded
package/src/main.css CHANGED
@@ -4368,35 +4368,11 @@ html:not([dir="rtl"]) .np-navigation-option {
4368
4368
  padding: 0 ;
4369
4369
  }
4370
4370
  .progress-bar {
4371
- float: left;
4372
4371
  -webkit-backface-visibility: hidden;
4373
- height: 100%;
4374
4372
  background-color: var(--color-interactive-primary);
4375
- border-top-left-radius: 1px;
4376
- border-bottom-left-radius: 1px;
4377
4373
  transition: width 0.6s ease-in-out;
4378
4374
  will-change: width;
4379
4375
  }
4380
- [dir="rtl"] .progress-bar {
4381
- float: right;
4382
- }
4383
- .progress-bar::after {
4384
- float: right;
4385
- width: 8px;
4386
- height: 8px;
4387
- margin-top: -3px;
4388
- margin-right: -4px;
4389
- content: "";
4390
- border-radius: 4px;
4391
- }
4392
- [dir="rtl"] .progress-bar::after {
4393
- float: left;
4394
- }
4395
- [dir="rtl"] .progress-bar::after {
4396
- margin-left: -4px;
4397
- margin-right: 0;
4398
- margin-right: initial;
4399
- }
4400
4376
  .btn-unstyled {
4401
4377
  background: none;
4402
4378
  border: none;
@@ -0,0 +1,34 @@
1
+ import { Meta } from '@storybook/blocks';
2
+
3
+ <Meta title="Forms/MoneyInput/Known issues" />
4
+
5
+ # Known accessibility issues
6
+
7
+ There are few issues reported by Axe that are not fixable or safe to ignore at this point in time.
8
+
9
+ ## Form elements should have a visible label / Form elements must have labels
10
+
11
+ While this requirement is normally considered as very serious, it's missing the fact that the input is wrapped by a labelled element with `role="group"`. We’ve tested it in NVDA on Windows and VoiceOver on MacOS, and discovered that if we were to satisfy Axe’s requirements, the screen reader would read out the label twice, something similar to `{Label Text} group, {Label Text} editable` (depending on the SR and the browser) which would result in an unnecessary noise. While the rule is valid, in this particular case the group label seems sufficient.
12
+
13
+ #### Further resources
14
+
15
+ - [Deque reference 1](https://dequeuniversity.com/rules/axe/4.9/label?application=axeAPI)
16
+ - [Deque reference 2](https://dequeuniversity.com/rules/axe/4.9/label-title-only?application=axeAPI)
17
+ - ['Group Labels Do Not Guarantee… Uniquity?' by Adrian Roselli](https://adrianroselli.com/2019/06/group-labels-do-not-guarantee-uniquity.html)
18
+
19
+ ## Buttons must have discernible text
20
+
21
+ This affects HTML buttons with `role="combobox"` and deeply nested label text, not being discoverable by Axe. It is a known false-positive.
22
+
23
+ #### Further resources
24
+
25
+ - [Deque reference](https://dequeuniversity.com/rules/axe/4.9/button-name?application=axeAPI)
26
+ - [axe-core github issue](https://github.com/dequelabs/axe-core/issues/4472)
27
+
28
+ ## ARIA hidden element must not be focusable or contain focusable elements
29
+
30
+ This is a genuine problem – according to the accessibility guidelines, no element with `aria-hidden="true"` should have any focusable children. This issue is caused by the `SelectInput`'s third party dependency (`Headless UI`) and, by extension, is visible in few other places. Work is planned to address it.
31
+
32
+ #### Further resources
33
+
34
+ - [Deque reference](https://dequeuniversity.com/rules/axe/4.9/aria-hidden-focus?application=axeAPI)
@@ -5,4 +5,9 @@ export default defineMessages({
5
5
  id: 'neptune.MoneyInput.Select.placeholder',
6
6
  defaultMessage: 'Select an option...',
7
7
  },
8
+ selectCurrencyLabel: {
9
+ id: 'neptune.MoneyInput.Select.selectCurrencyLabel',
10
+ defaultMessage: 'Select currency',
11
+ description: 'Visually hidden label for the currency selector input.',
12
+ },
8
13
  });
@@ -2,6 +2,7 @@ import { Field } from '../field/Field';
2
2
  import { mockMatchMedia, mockResizeObserver, render, screen, userEvent } from '../test-utils';
3
3
 
4
4
  import MoneyInput from './MoneyInput';
5
+ import messages from './MoneyInput.messages';
5
6
 
6
7
  mockMatchMedia();
7
8
  mockResizeObserver();
@@ -51,6 +52,7 @@ describe('MoneyInput', () => {
51
52
  const props = {
52
53
  currencies,
53
54
  selectedCurrency: currencies[1],
55
+ searchPlaceholder: 'Type a currency / country',
54
56
  amount: 1000,
55
57
  onAmountChange: jest.fn(),
56
58
  onCurrencyChange: jest.fn(),
@@ -90,4 +92,49 @@ describe('MoneyInput', () => {
90
92
  );
91
93
  expect(screen.getAllByRole('group')[0]).toHaveAccessibleName(/^Recipient gets/);
92
94
  });
95
+
96
+ describe('ids', () => {
97
+ it('should guarantee id and connect the input with the selected currency via withId HoC', () => {
98
+ render(<MoneyInput {...props} />);
99
+ const input = screen.getByRole('textbox');
100
+ const button = screen.getByRole('combobox');
101
+ expect(input.getAttribute('id')).toBeTruthy();
102
+ expect(input).toHaveAttribute('aria-describedby', button.getAttribute('id'));
103
+ });
104
+
105
+ it('should have unique id for the select filter with predefined id', async () => {
106
+ const fieldId = 'myFieldId';
107
+ render(
108
+ <Field label="Multiple currencies" id={fieldId}>
109
+ <MoneyInput {...props} />
110
+ </Field>,
111
+ );
112
+ await userEvent.click(screen.getByRole('combobox'));
113
+ expect(screen.getByLabelText(props.searchPlaceholder)).toHaveAttribute(
114
+ 'id',
115
+ `${fieldId}SelectedCurrencySearch`,
116
+ );
117
+ });
118
+
119
+ it('should have unique id for the select filter without predefined id', async () => {
120
+ render(
121
+ <Field label="Multiple currencies">
122
+ <MoneyInput {...props} />
123
+ </Field>,
124
+ );
125
+ await userEvent.click(screen.getByRole('combobox'));
126
+ expect(screen.getByLabelText(props.searchPlaceholder)).toHaveAttribute(
127
+ 'id',
128
+ expect.stringMatching(/^:.*?:SelectedCurrencySearch$/),
129
+ );
130
+ });
131
+ });
132
+
133
+ it('should have AT label for the currency dropdown', () => {
134
+ render(<MoneyInput {...props} />);
135
+ expect(screen.getByRole('combobox')).toHaveAttribute(
136
+ 'aria-label',
137
+ messages.selectCurrencyLabel.defaultMessage,
138
+ );
139
+ });
93
140
  });
@@ -1,5 +1,4 @@
1
1
  import { shallow } from 'enzyme';
2
- import { render, screen } from '@testing-library/react';
3
2
 
4
3
  import { MoneyInput, Title, Input, SelectInput } from '..';
5
4
  import { mockMatchMedia, mockResizeObserver } from '../test-utils';
@@ -818,14 +817,4 @@ describe('Money Input', () => {
818
817
  });
819
818
  });
820
819
  });
821
-
822
- describe('withId', () => {
823
- it('should guarantee id and connect the input with the selected currency', () => {
824
- render(<MoneyInput {...props} />);
825
- const input = screen.getByRole('textbox');
826
- const button = screen.getByRole('combobox');
827
- expect(input.getAttribute('id')).toBeTruthy();
828
- expect(input).toHaveAttribute('aria-describedby', button.getAttribute('id'));
829
- });
830
- });
831
820
  });
@@ -9,13 +9,13 @@ import { Field } from '../field/Field';
9
9
  export default {
10
10
  component: MoneyInput,
11
11
  title: 'Forms/MoneyInput',
12
- render: function Render(args) {
12
+ render: function Render({ id, ...args }) {
13
13
  const [selectedCurrency, setSelectedCurrency] = useState(args.selectedCurrency);
14
14
 
15
15
  const handleOnCurrencyChange = (value: CurrencyOptionItem) => setSelectedCurrency(value);
16
16
 
17
17
  return (
18
- <Field label="Editable money input label" required>
18
+ <Field label="Editable money input label" required id={id}>
19
19
  <MoneyInput
20
20
  {...args}
21
21
  selectedCurrency={selectedCurrency}
@@ -129,6 +129,7 @@ export const MultipleCurrencies: Story = {
129
129
  },
130
130
  selectedCurrency: exampleCurrency.usd,
131
131
  searchPlaceholder: 'Type a currency / country',
132
+ id: 'moneyInput',
132
133
  },
133
134
  };
134
135
 
@@ -155,20 +156,22 @@ export const OpenedInput: Story = {
155
156
  };
156
157
 
157
158
  export const SmallInput: Story = {
158
- render: (args) => {
159
+ render: ({ id, ...args }) => {
159
160
  return (
160
161
  <>
161
- <Field id={args.id} label="Money inputs" required>
162
+ <Field label="Money inputs" required>
162
163
  <MoneyInput {...args} {...SingleCurrency.args} />
163
164
  </Field>
164
165
  <br />
165
- <MoneyInput {...args} {...MultipleCurrencies.args} />
166
+ <Field label="Multiple currencies">
167
+ <MoneyInput {...args} {...MultipleCurrencies.args} />
168
+ </Field>
166
169
  <hr />
167
- <Field id={args.id} label="Error states" sentiment="negative" required>
170
+ <Field label="Error state" sentiment="negative" required>
168
171
  <MoneyInput {...args} {...SingleCurrency.args} />
169
172
  </Field>
170
173
  <br />
171
- <Field sentiment="negative">
174
+ <Field label="Multiple currencies: error state" sentiment="negative">
172
175
  <MoneyInput {...args} {...MultipleCurrencies.args} />
173
176
  </Field>
174
177
  </>
@@ -314,7 +314,7 @@ class MoneyInput extends Component<MoneyInputPropsWithInputAttributes, MoneyInpu
314
314
 
315
315
  const isFixedCurrency = (!this.state.searchQuery && hasSingleCurrency()) || !onCurrencyChange;
316
316
  const disabled = !this.props.onAmountChange;
317
- const selectedCurrencyElementId = `${amountInputId}SelectedCurrency`;
317
+ const selectedCurrencyElementId = `${inputAttributes?.id ?? amountInputId}SelectedCurrency`;
318
318
 
319
319
  return (
320
320
  <div
@@ -387,6 +387,13 @@ class MoneyInput extends Component<MoneyInputPropsWithInputAttributes, MoneyInpu
387
387
  )}
388
388
  >
389
389
  <SelectInput
390
+ UNSAFE_triggerButtonProps={{
391
+ id: undefined,
392
+ 'aria-labelledby': undefined,
393
+ 'aria-describedby': undefined,
394
+ 'aria-invalid': undefined,
395
+ 'aria-label': this.props.intl.formatMessage(messages.selectCurrencyLabel),
396
+ }}
390
397
  id={selectedCurrencyElementId}
391
398
  items={selectOptions}
392
399
  value={selectedCurrency}
@@ -70,35 +70,11 @@
70
70
  padding: 0 ;
71
71
  }
72
72
  .progress-bar {
73
- float: left;
74
73
  -webkit-backface-visibility: hidden;
75
- height: 100%;
76
74
  background-color: var(--color-interactive-primary);
77
- border-top-left-radius: 1px;
78
- border-bottom-left-radius: 1px;
79
75
  transition: width 0.6s ease-in-out;
80
76
  will-change: width;
81
77
  }
82
- [dir="rtl"] .progress-bar {
83
- float: right;
84
- }
85
- .progress-bar::after {
86
- float: right;
87
- width: 8px;
88
- height: 8px;
89
- margin-top: -3px;
90
- margin-right: -4px;
91
- content: "";
92
- border-radius: 4px;
93
- }
94
- [dir="rtl"] .progress-bar::after {
95
- float: left;
96
- }
97
- [dir="rtl"] .progress-bar::after {
98
- margin-left: -4px;
99
- margin-right: 0;
100
- margin-right: initial;
101
- }
102
78
  .btn-unstyled {
103
79
  background: none;
104
80
  border: none;
@@ -78,27 +78,10 @@
78
78
  }
79
79
 
80
80
  .progress-bar {
81
- .float(left);
82
-
83
81
  -webkit-backface-visibility: hidden;
84
- height: 100%;
85
82
  background-color: var(--color-interactive-primary);
86
- border-top-left-radius: 1px;
87
- border-bottom-left-radius: 1px;
88
83
  transition: width 0.6s ease-in-out;
89
84
  will-change: width;
90
-
91
- &::after {
92
- .float(right);
93
-
94
- width: 8px;
95
- height: 8px;
96
- margin-top: -3px;
97
- .margin(right, -4px);
98
-
99
- content: "";
100
- border-radius: 4px;
101
- }
102
85
  }
103
86
 
104
87
  .btn-unstyled {
@@ -40,15 +40,21 @@ describe('Stepper', () => {
40
40
  });
41
41
 
42
42
  it('sets the widths of the progress bar to match where you are in the flow', () => {
43
- expect(totalWidth()).toBe('0%');
43
+ expect(totalWidth()).toBe('0px');
44
44
  activeStep(2);
45
- expect(totalWidth()).toBe('100%');
45
+ expect(totalWidth()).toBe(
46
+ 'calc(100% + var(--progress-bar-start-shift) + var(--progress-bar-border-width))',
47
+ );
46
48
  steps(5);
47
- expect(totalWidth()).toBe('50%');
49
+ expect(totalWidth()).toBe(
50
+ 'calc(50% + var(--progress-bar-start-shift) + var(--progress-bar-border-width))',
51
+ );
48
52
  activeStep(10000);
49
- expect(totalWidth()).toBe('100%');
53
+ expect(totalWidth()).toBe(
54
+ 'calc(100% + var(--progress-bar-start-shift) + var(--progress-bar-border-width))',
55
+ );
50
56
  activeStep(-10);
51
- expect(totalWidth()).toBe('0%');
57
+ expect(totalWidth()).toBe('0px');
52
58
  });
53
59
  });
54
60
 
@@ -38,6 +38,18 @@ const Stepper = ({ steps, activeStep = 0, className }: StepperProps) => {
38
38
  const stepPercentage = 1 / (steps.length - 1);
39
39
  const percentageCompleted = activeStepIndex / (steps.length - 1);
40
40
 
41
+ const getProgressWidth = (): string => {
42
+ if (percentageCompleted === 0) {
43
+ return '0px';
44
+ }
45
+ /**
46
+ * Progress bar starts with left/right (depends on rtl) shift `--progress-bar-start-shift` for hiding Progress bar's left and right borders
47
+ * which are used for progress vertical delimiter.
48
+ * When progress is completed, we need to add `--progress-bar-border-width` to the width to allow the right border be outside of the progress area.
49
+ */
50
+ return `calc(${percentageCompleted * 100}% + var(--progress-bar-start-shift) + var(--progress-bar-border-width))`;
51
+ };
52
+
41
53
  const renderStep = (step: Step, index: number) => {
42
54
  const active = index === activeStepIndex;
43
55
  const clickable = step.onClick && !active;
@@ -85,9 +97,8 @@ const Stepper = ({ steps, activeStep = 0, className }: StepperProps) => {
85
97
  return (
86
98
  <div className={clsx('tw-stepper', className)}>
87
99
  <div className="progress">
88
- <div className="progress-bar" style={{ width: `${percentageCompleted * 100}%` }} />
100
+ <div className="progress-bar" style={{ width: getProgressWidth() }} />
89
101
  </div>
90
-
91
102
  <ol className="tw-stepper-steps p-t-1 m-b-0">{steps.map(renderStep)}</ol>
92
103
  </div>
93
104
  );
package/src/tile/Tile.tsx CHANGED
@@ -6,6 +6,8 @@ import Title from '../title';
6
6
 
7
7
  export interface TileProps {
8
8
  /** Classes to apply to the Tile container */
9
+ /** A label for the accordion item, used for accessibility purposes. */
10
+ 'aria-label'?: string;
9
11
  className?: string;
10
12
  description?: React.ReactNode;
11
13
  disabled?: boolean;
@@ -21,6 +23,7 @@ export interface TileProps {
21
23
  }
22
24
 
23
25
  export default function Tile({
26
+ 'aria-label': ariaLabel,
24
27
  className,
25
28
  description,
26
29
  disabled,
@@ -51,6 +54,7 @@ export default function Tile({
51
54
  )}
52
55
  href={href}
53
56
  target={target}
57
+ aria-label={ariaLabel}
54
58
  onClick={disabled ? undefined : onClick}
55
59
  onKeyDown={
56
60
  disabled
@@ -24,6 +24,7 @@ DescribedButton.displayName = 'DescribedButton';
24
24
 
25
25
  export default {
26
26
  component: DescribedButton,
27
+ tags: ['docs-only'],
27
28
  } satisfies Meta<typeof withId>;
28
29
 
29
30
  export const WithoutId: Story = {};
@@ -1,127 +0,0 @@
1
- import Avatar from '../avatar';
2
- import { Breakpoint, Size } from '../common';
3
- import { mockMatchMedia } from '../mocks';
4
- import { render, fireEvent, screen } from '../test-utils';
5
-
6
- import Decision, { DecisionPresentation, DecisionType } from '.';
7
-
8
- mockMatchMedia(jest);
9
-
10
- describe('Decision', () => {
11
- const commonProps = {
12
- options: [
13
- {
14
- media: {
15
- list: <Avatar type="initials">HM</Avatar>,
16
- block: <img src="img.jpg" alt="alt" />,
17
- },
18
- href: '#href',
19
- title: 'title',
20
- description: 'description',
21
- onClick: jest.fn(),
22
- },
23
- ],
24
- presentation: DecisionPresentation.LIST_BLOCK,
25
- type: DecisionType.NAVIGATION,
26
- };
27
-
28
- const initialInnerWidth = window.innerWidth;
29
- const resetClientWidth = (width) => {
30
- window.innerWidth = width;
31
- };
32
-
33
- afterAll(() => {
34
- window.innerWidth = initialInnerWidth;
35
- });
36
-
37
- beforeEach(() => {
38
- resetClientWidth(Breakpoint.EXTRA_SMALL - 1);
39
- });
40
-
41
- describe(`when presentation is ${DecisionPresentation.LIST_BLOCK}`, () => {
42
- const props = { ...commonProps };
43
-
44
- it('renders only Navigation Option before first breakpoint', () => {
45
- const { container } = render(<Decision {...props} />);
46
-
47
- expect(getNavigationOption(container)).toBeInTheDocument();
48
- expect(getTile(container)).not.toBeInTheDocument();
49
- });
50
-
51
- it('renders only Tile after first breakpoint', () => {
52
- resetClientWidth(Breakpoint.SMALL);
53
- const { container } = render(<Decision {...props} />);
54
-
55
- expect(getNavigationOption(container)).not.toBeInTheDocument();
56
- expect(getTile(container)).toBeInTheDocument();
57
- expect(getTile(container)).not.toHaveClass('np-tile--small');
58
- });
59
- });
60
-
61
- describe(`when presentation is ${DecisionPresentation.LIST_BLOCK_GRID}`, () => {
62
- const props = { ...commonProps, presentation: DecisionPresentation.LIST_BLOCK_GRID };
63
-
64
- it('renders only Navigation Option before first breakpoint', () => {
65
- const { container } = render(<Decision {...props} />);
66
-
67
- expect(getNavigationOption(container)).toBeInTheDocument();
68
- expect(getTile(container)).not.toBeInTheDocument();
69
- });
70
-
71
- it('renders only Tile after first breakpoint', () => {
72
- resetClientWidth(Breakpoint.SMALL);
73
- const { container } = render(<Decision {...props} />);
74
-
75
- expect(getNavigationOption(container)).not.toBeInTheDocument();
76
- expect(getTile(container)).toBeInTheDocument();
77
- expect(getTile(container)).not.toHaveClass('np-tile--small');
78
- });
79
-
80
- it('renders container as a grid', () => {
81
- resetClientWidth(Breakpoint.SMALL);
82
- const { container } = render(<Decision {...props} />);
83
-
84
- const decisionElement = container.querySelector('.np-decision');
85
- expect(decisionElement).toHaveClass('np-decision--grid');
86
- expect(decisionElement).toHaveClass('flex-wrap');
87
- });
88
- });
89
-
90
- describe(`when presentation is ${DecisionPresentation.LIST_BLOCK} and size is Small`, () => {
91
- const props = {
92
- ...commonProps,
93
- presentation: DecisionPresentation.LIST_BLOCK,
94
- size: Size.SMALL,
95
- };
96
-
97
- it('renders only Navigation Option before breakpoint', () => {
98
- const { container } = render(<Decision {...props} />);
99
-
100
- expect(getNavigationOption(container)).toBeInTheDocument();
101
- expect(getTile(container)).not.toBeInTheDocument();
102
- });
103
-
104
- it('renders Small Tile after breakpoint', () => {
105
- resetClientWidth(Breakpoint.EXTRA_SMALL);
106
- const { container } = render(<Decision {...props} />);
107
-
108
- expect(getNavigationOption(container)).not.toBeInTheDocument();
109
- expect(getTile(container)).toBeInTheDocument();
110
- expect(getTile(container)).toHaveClass('np-tile--small');
111
- });
112
- });
113
-
114
- describe(`when presentation is ${DecisionPresentation.LIST}`, () => {
115
- const props = { ...commonProps, presentation: DecisionPresentation.LIST };
116
-
117
- it('renders Navigation Option before breakpoint', () => {
118
- const { container } = render(<Decision {...props} />);
119
-
120
- expect(getNavigationOption(container)).toBeInTheDocument();
121
- expect(getTile(container)).not.toBeInTheDocument();
122
- });
123
- });
124
-
125
- const getNavigationOption = (container) => container.querySelector('.np-navigation-option');
126
- const getTile = (container) => container.querySelector('.np-tile');
127
- });