@transferwise/components 46.104.0 → 46.105.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 (82) hide show
  1. package/build/header/Header.js +60 -43
  2. package/build/header/Header.js.map +1 -1
  3. package/build/header/Header.mjs +57 -43
  4. package/build/header/Header.mjs.map +1 -1
  5. package/build/i18n/cs.json +2 -0
  6. package/build/i18n/cs.json.js +2 -0
  7. package/build/i18n/cs.json.js.map +1 -1
  8. package/build/i18n/cs.json.mjs +2 -0
  9. package/build/i18n/cs.json.mjs.map +1 -1
  10. package/build/i18n/es.json +2 -0
  11. package/build/i18n/es.json.js +2 -0
  12. package/build/i18n/es.json.js.map +1 -1
  13. package/build/i18n/es.json.mjs +2 -0
  14. package/build/i18n/es.json.mjs.map +1 -1
  15. package/build/i18n/th.json +2 -0
  16. package/build/i18n/th.json.js +2 -0
  17. package/build/i18n/th.json.js.map +1 -1
  18. package/build/i18n/th.json.mjs +2 -0
  19. package/build/i18n/th.json.mjs.map +1 -1
  20. package/build/index.js +1 -1
  21. package/build/index.mjs +1 -1
  22. package/build/inputs/SelectInput.js +1 -1
  23. package/build/inputs/SelectInput.js.map +1 -1
  24. package/build/inputs/SelectInput.mjs +1 -1
  25. package/build/listItem/ListItem.js +4 -2
  26. package/build/listItem/ListItem.js.map +1 -1
  27. package/build/listItem/ListItem.mjs +4 -2
  28. package/build/listItem/ListItem.mjs.map +1 -1
  29. package/build/main.css +24 -14
  30. package/build/styles/header/Header.css +21 -14
  31. package/build/styles/listItem/ListItem.css +3 -0
  32. package/build/styles/main.css +24 -14
  33. package/build/title/Title.js +10 -4
  34. package/build/title/Title.js.map +1 -1
  35. package/build/title/Title.mjs +6 -4
  36. package/build/title/Title.mjs.map +1 -1
  37. package/build/types/header/Header.d.ts +27 -11
  38. package/build/types/header/Header.d.ts.map +1 -1
  39. package/build/types/header/index.d.ts +1 -0
  40. package/build/types/header/index.d.ts.map +1 -1
  41. package/build/types/index.d.ts +1 -0
  42. package/build/types/index.d.ts.map +1 -1
  43. package/build/types/listItem/ListItem.d.ts.map +1 -1
  44. package/build/types/listItem/_stories/subcomponents.d.ts +1 -1
  45. package/build/types/listItem/_stories/subcomponents.d.ts.map +1 -1
  46. package/build/types/title/Title.d.ts +4 -5
  47. package/build/types/title/Title.d.ts.map +1 -1
  48. package/package.json +1 -1
  49. package/src/actionButton/ActionButton.story.tsx +1 -1
  50. package/src/avatar/Avatar.story.tsx +1 -1
  51. package/src/avatarWrapper/AvatarWrapper.story.tsx +1 -1
  52. package/src/badge/Badge.story.tsx +1 -1
  53. package/src/button/Button.spec.tsx +25 -1
  54. package/src/button/Button.story.tsx +1 -1
  55. package/src/button/LegacyButton.story.tsx +1 -1
  56. package/src/header/Header.accessibility.docs.mdx +85 -0
  57. package/src/header/Header.css +21 -14
  58. package/src/header/Header.less +17 -10
  59. package/src/header/Header.spec.tsx +68 -50
  60. package/src/header/Header.story.tsx +190 -36
  61. package/src/header/Header.tsx +96 -65
  62. package/src/header/index.ts +1 -0
  63. package/src/i18n/cs.json +2 -0
  64. package/src/i18n/es.json +2 -0
  65. package/src/i18n/th.json +2 -0
  66. package/src/iconButton/iconButton.spec.tsx +31 -0
  67. package/src/index.ts +1 -0
  68. package/src/listItem/Button/ListItemButton.spec.tsx +23 -1
  69. package/src/listItem/IconButton/ListItemIconButton.spec.tsx +14 -2
  70. package/src/listItem/ListItem.css +3 -0
  71. package/src/listItem/ListItem.less +4 -0
  72. package/src/listItem/ListItem.tsx +4 -2
  73. package/src/listItem/Navigation/ListItemNavigation.spec.tsx +8 -0
  74. package/src/listItem/Navigation/ListItemNavigation.story.tsx +4 -2
  75. package/src/listItem/_stories/ListItem.layout.test.story.tsx +20 -0
  76. package/src/listItem/_stories/ListItem.story.tsx +1 -1
  77. package/src/listItem/_stories/ListItem.variants.test.story.tsx +3 -0
  78. package/src/listItem/_stories/subcomponents.tsx +2 -0
  79. package/src/main.css +24 -14
  80. package/src/primitives/PrimitiveAnchor/test/PrimitiveAnchor.spec.tsx +15 -4
  81. package/src/select/Select.story.tsx +1 -1
  82. package/src/title/Title.tsx +25 -12
@@ -0,0 +1,85 @@
1
+ import { Meta, Canvas, Source } from '@storybook/addon-docs/blocks';
2
+ import { NavigationOption } from '..';
3
+ import { Bulb } from '@transferwise/icons';
4
+ import * as stories from './Header.story';
5
+
6
+ <Meta title="Typography/Header/Accessibility" />
7
+
8
+ # Accessibility
9
+
10
+ Given the `Header` is a key component for structuring content and conveying hierarchy, ensuring its accessibility is crucial for an inclusive experience.
11
+
12
+ <NavigationOption
13
+ media={<Bulb size={24} />}
14
+ title="Design guidance"
15
+ content="Before you start, familiarise yourself with the dedicated accessibility documentation."
16
+ href="https://wise.design/components/section-header#accessibility"
17
+ />
18
+
19
+ <br />
20
+ <br />
21
+
22
+ ## Semantic headings
23
+
24
+ The `Header` component should always use semantic HTML heading tags (`<h1>` to `<h6>`) to convey the document structure. Avoid using non-semantic tags unless absolutely necessary.
25
+
26
+ <Source dark code={`
27
+ // ✅ semantic heading
28
+ <Header as="h1">Main heading</Header>
29
+
30
+ // ⚠️ use with care
31
+
32
+ <Header as="div">Non-semantic heading</Header>
33
+ `}/>
34
+
35
+ **Additional resources:**
36
+
37
+ 1. [Deque: Headings must follow a logical order](https://dequeuniversity.com/rules/axe/4.2/heading-order)
38
+ 2. [MDN: Using headings](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Heading_Elements)
39
+
40
+ <br />
41
+
42
+ ## ARIA roles
43
+
44
+ If the `Header` is used in a non-semantic context, ensure it is accessible by applying appropriate ARIA roles, such as `role="heading"`, and providing a `aria-level` attribute to indicate its level in the hierarchy.
45
+
46
+ <Source
47
+ dark
48
+ code={`
49
+ // ⚠️ use with care
50
+ <Header as="div" role="heading" aria-level="1">
51
+ Main heading
52
+ </Header>
53
+ `}
54
+ />
55
+
56
+ <br />
57
+
58
+ ## Accessibility with actions
59
+
60
+ When using the `Header` with actions (e.g., buttons or links), ensure the actions are accessible by providing clear labels and ARIA attributes.
61
+
62
+ <Source
63
+ dark
64
+ code={`
65
+ // ✅ accessible action
66
+ <Header
67
+ as="h2"
68
+ action={{
69
+ 'aria-label': 'Edit section',
70
+ text: 'Edit',
71
+ onClick: () => alert('Edit clicked'),
72
+ }}
73
+ >
74
+ Section heading
75
+ </Header>
76
+ `}
77
+ />
78
+
79
+ <br />
80
+
81
+ ## Visual hierarchy
82
+
83
+ Ensure the `Header` visually aligns with its semantic level. For example, an `<h1>` should be the largest and most prominent, while an `<h6>` should be the smallest.
84
+
85
+ <Canvas of={stories.Playground} />
@@ -1,26 +1,33 @@
1
1
  .np-header {
2
- display: flex;
3
- justify-content: space-between;
4
- align-items: flex-end;
2
+ display: grid;
3
+ grid-template-columns: 1fr auto;
4
+ grid-column-gap: 24px;
5
+ grid-column-gap: var(--size-24);
6
+ -moz-column-gap: 24px;
7
+ column-gap: 24px;
8
+ -moz-column-gap: var(--size-24);
9
+ column-gap: var(--size-24);
10
+ align-items: center;
11
+ margin-bottom: 8px;
12
+ margin-bottom: var(--size-8);
5
13
  max-width: 100%;
6
14
  padding: 8px 0;
7
15
  padding: var(--size-8) 0;
16
+ width: 100%;
17
+ }
18
+ .np-header--group {
19
+ align-items: flex-end;
8
20
  border-bottom: 1px solid rgba(0,0,0,0.10196);
9
21
  border-bottom: 1px solid var(--color-border-neutral);
10
- margin-bottom: 8px;
11
- margin-bottom: var(--size-8);
12
- -moz-column-gap: 24px;
13
- column-gap: 24px;
14
- -moz-column-gap: var(--size-24);
15
- column-gap: var(--size-24);
16
22
  }
17
23
  .np-header__title {
18
24
  color: #5d7079;
19
25
  color: var(--color-content-secondary);
26
+ margin: 0;
20
27
  }
21
- .np-header__button {
22
- margin-inline: calc(16px * -1);
23
- margin-inline: calc(var(--size-16) * -1);
24
- margin-bottom: calc(4px * -1);
25
- margin-bottom: calc(var(--size-4) * -1);
28
+ .np-header__action {
29
+ margin: 0;
30
+ height: 20px;
31
+ display: flex;
32
+ align-items: center;
26
33
  }
@@ -1,20 +1,27 @@
1
1
  .np-header {
2
- display: flex;
3
- justify-content: space-between;
4
- align-items: flex-end;
2
+ display: grid;
3
+ grid-template-columns: 1fr auto;
4
+ column-gap: var(--size-24);
5
+ align-items: center;
6
+ margin-bottom: var(--size-8);
5
7
  max-width: 100%;
6
8
  padding: var(--size-8) 0;
7
- border-bottom: 1px solid var(--color-border-neutral);
8
- margin-bottom: var(--size-8);
9
+ width: 100%;
10
+
11
+ &--group {
12
+ align-items: flex-end;
13
+ border-bottom: 1px solid var(--color-border-neutral);
14
+ }
9
15
 
10
16
  &__title {
11
17
  color: var(--color-content-secondary);
18
+ margin: 0;
12
19
  }
13
20
 
14
- column-gap: var(--size-24);
15
-
16
- &__button {
17
- margin-inline: calc(var(--size-16) * -1);
18
- margin-bottom: calc(var(--size-4) * -1);
21
+ &__action {
22
+ margin: 0;
23
+ height: 20px;
24
+ display: flex;
25
+ align-items: center;
19
26
  }
20
27
  }
@@ -1,95 +1,113 @@
1
1
  import { render, screen, userEvent } from '../test-utils';
2
-
3
- import Header from '.';
2
+ import Header, { HeaderProps } from '.';
4
3
 
5
4
  describe('Header', () => {
6
- it('can set header title', () => {
7
- render(<Header title="Header title" />);
5
+ const defaultProps: HeaderProps = {
6
+ title: 'Header title',
7
+ };
8
+
9
+ const renderHeader = (props: Partial<HeaderProps> = {}) => {
10
+ return render(<Header {...defaultProps} {...props} />);
11
+ };
8
12
 
13
+ it('can set header title', () => {
14
+ renderHeader();
9
15
  expect(screen.getByText('Header title')).toBeInTheDocument();
10
16
  });
11
17
 
12
18
  it('can trigger header action', async () => {
13
19
  const onHeaderActionClick = jest.fn();
14
-
15
- render(
16
- <Header
17
- title="Header title"
18
- action={{
19
- text: 'Click me!',
20
- onClick: onHeaderActionClick,
21
- }}
22
- />,
23
- );
20
+ renderHeader({
21
+ action: {
22
+ text: 'Click me!',
23
+ onClick: onHeaderActionClick,
24
+ },
25
+ });
24
26
 
25
27
  await userEvent.click(screen.getByRole('button', { name: 'Click me!' }));
26
-
27
28
  expect(onHeaderActionClick).toHaveBeenCalledTimes(1);
28
29
  });
29
30
 
30
31
  it('can set aria-label property for header action', async () => {
31
32
  const onHeaderActionClick = jest.fn();
32
-
33
- render(
34
- <Header
35
- title="Header title"
36
- action={{
37
- 'aria-label': 'Magic',
38
- text: 'Click me!',
39
- onClick: onHeaderActionClick,
40
- }}
41
- />,
42
- );
33
+ renderHeader({
34
+ action: {
35
+ 'aria-label': 'Magic',
36
+ text: 'Click me!',
37
+ onClick: onHeaderActionClick,
38
+ },
39
+ });
43
40
 
44
41
  await userEvent.click(screen.getByRole('button', { name: 'Magic' }));
45
-
46
42
  expect(onHeaderActionClick).toHaveBeenCalledTimes(1);
47
43
  });
48
44
 
49
45
  it('renders header action as a link when href is provided', () => {
50
- render(
51
- <Header
52
- title="Header title"
53
- action={{
54
- 'aria-label': 'Click me!',
55
- text: 'I am a link',
56
- href: 'https://wise.com',
57
- }}
58
- />,
59
- );
46
+ renderHeader({
47
+ action: {
48
+ 'aria-label': 'Click me!',
49
+ text: 'I am a link',
50
+ href: 'https://wise.com',
51
+ },
52
+ });
60
53
 
61
54
  const link = screen.getByRole('link', { name: 'Click me!' });
62
-
63
55
  expect(link).toHaveAttribute('href', 'https://wise.com');
64
56
  });
65
57
 
66
58
  it('renders header with h5 heading tag by default', () => {
67
- render(<Header title="Header title" />);
68
-
59
+ renderHeader();
69
60
  expect(screen.getByRole('heading', { name: 'Header title', level: 5 })).toBeInTheDocument();
70
61
  });
71
62
 
72
63
  it('can render header with specific heading tag', () => {
73
- render(<Header as="h3" title="Header title" />);
74
-
64
+ renderHeader({ as: 'h3' });
75
65
  expect(screen.getByRole('heading', { name: 'Header title', level: 3 })).toBeInTheDocument();
76
66
  });
77
67
 
78
- it('runs onClick if specified even when it got href prop', async () => {
79
- const callback = jest.fn();
68
+ it('renders header with group level', () => {
69
+ renderHeader({ level: 'group' });
70
+ const header = screen.getByRole('heading', { name: 'Header title' });
71
+ expect(header).toHaveClass('np-header--group');
72
+ });
73
+
74
+ it('warns if Header as legend is not inside a fieldset', () => {
75
+ const consoleWarnMock = jest.spyOn(console, 'warn').mockImplementation();
76
+
77
+ renderHeader({ as: 'legend' });
78
+
79
+ expect(consoleWarnMock).toHaveBeenCalledWith(
80
+ 'Legends should be the first child in a fieldset, and this is not possible when including an action',
81
+ );
82
+
83
+ consoleWarnMock.mockRestore();
84
+ });
85
+
86
+ it('does not warn if Header as legend is inside a fieldset', () => {
87
+ const consoleWarnMock = jest.spyOn(console, 'warn').mockImplementation();
88
+
80
89
  render(
81
- <Header
82
- as="h3"
83
- title="Header title"
84
- action={{ text: 'Click me', href: '#', onClick: callback }}
85
- />,
90
+ <fieldset>
91
+ <Header as="legend" title="Header title" />
92
+ </fieldset>,
86
93
  );
87
94
 
95
+ expect(consoleWarnMock).not.toHaveBeenCalled();
96
+
97
+ consoleWarnMock.mockRestore();
98
+ });
99
+
100
+ it('runs onClick if specified even when it got href prop', async () => {
101
+ const callback = jest.fn();
102
+ renderHeader({
103
+ as: 'h3',
104
+ action: { text: 'Click me', href: '#', onClick: callback },
105
+ });
106
+
88
107
  const button = screen.getByRole('link', { name: 'Click me' });
89
108
  expect(button).toBeInTheDocument();
90
109
 
91
110
  await userEvent.click(button);
92
-
93
111
  expect(callback).toHaveBeenCalledTimes(1);
94
112
  });
95
113
  });
@@ -1,53 +1,207 @@
1
- import Header from './Header';
1
+ import { Meta, StoryObj } from '@storybook/react-webpack5';
2
+ import Header, { HeaderProps } from './Header';
3
+ import { storyConfig } from '../test-utils';
2
4
 
3
- export default {
4
- component: Header,
5
- title: 'Typography/Header',
6
- };
5
+ const withContainer = (Story: any) => (
6
+ <div style={{ display: 'flex', justifyContent: 'center' }}>
7
+ <Story />
8
+ </div>
9
+ );
10
+
11
+ /**
12
+ * Not all stories need access to all controls as it causes unnecessary UI noise.
13
+ */
14
+ const hideControls = (args: string[]) =>
15
+ Object.fromEntries(args.map((item) => [item, { table: { disable: true } }]));
7
16
 
8
- export const Basic = () => {
9
- return <Header title="Header title" />;
17
+ /**
18
+ * Reusable render logic for wrapping `Header` in a `<fieldset>` if `as` is `legend`.
19
+ */
20
+ const renderHeader = (args: HeaderProps) => {
21
+ if (args.as === 'legend') {
22
+ return (
23
+ <fieldset style={{ width: '100%' }}>
24
+ <Header {...args} />
25
+ </fieldset>
26
+ );
27
+ }
28
+ return <Header {...args} />;
10
29
  };
11
30
 
12
- export const WithAction = () => {
13
- return (
31
+ /**
32
+ * Helper to generate variants for `AllVariants` story.
33
+ */
34
+ const renderVariants = () => (
35
+ <div
36
+ className="header-variants"
37
+ style={{ display: 'flex', flexWrap: 'wrap', gap: '16px', maxWidth: '1200px' }}
38
+ >
39
+ {(['h1', 'h2', 'h3', 'legend'] as const).map((as) => (
40
+ <Header key={as} as={as} title={`Header as ${as}`} />
41
+ ))}
42
+ {(['section', 'group'] as const).map((level) => (
43
+ <Header key={level} level={level} title={`Header level ${level}`} />
44
+ ))}
14
45
  <Header
15
- title="Header title"
46
+ as="h2"
47
+ title="Header with Action"
16
48
  action={{
17
- 'aria-label': 'Magic',
18
- text: 'Click me!',
19
- onClick: () => alert('Action!'),
49
+ 'aria-label': 'Action',
50
+ text: 'Action',
51
+ onClick: () => alert('Action clicked!'),
20
52
  }}
21
53
  />
22
- );
23
- };
24
-
25
- export const WithActionAsLink = () => {
26
- return (
27
54
  <Header
28
- title="Header title"
55
+ as="h2"
56
+ title="Header with link"
29
57
  action={{
30
- text: 'This is a link',
58
+ 'aria-label': 'Action',
59
+ text: 'Action',
31
60
  href: 'https://wise.com',
32
61
  }}
33
62
  />
34
- );
63
+ </div>
64
+ );
65
+
66
+ /**
67
+ * The stories below document the `Header` component, which is used to structure content and convey hierarchy. <br />
68
+ * For more details, refer to the [design documentation](https://wise.design/components/section-header).
69
+ */
70
+ const meta: Meta<typeof Header> = {
71
+ component: Header,
72
+ title: 'Typography/Header',
73
+ argTypes: {
74
+ level: {
75
+ type: {
76
+ name: 'enum',
77
+ value: ['section', 'group'],
78
+ },
79
+ table: {
80
+ type: {
81
+ summary: 'HeaderLevel',
82
+ },
83
+ },
84
+ description: 'Defines the hierarchical level of the header.',
85
+ },
86
+ as: {
87
+ type: {
88
+ name: 'enum',
89
+ value: ['h1', 'h2', 'h3', 'legend'],
90
+ },
91
+ table: {
92
+ type: {
93
+ summary: 'HeaderAs',
94
+ },
95
+ },
96
+ description:
97
+ 'Defines which HTML element the Header will render as. Use `legend` only as the first child of a `<fieldset>` — otherwise a warning will appear. The `action` prop is not supported with `legend`. You can also use heading tags like `h1`, `h2`, `h3`, or `header`.',
98
+ },
99
+ action: {
100
+ table: {
101
+ type: {
102
+ summary: 'HeaderAction',
103
+ },
104
+ },
105
+ description: 'Defines an optional action (e.g., button or link) associated with the header.',
106
+ },
107
+ },
108
+ args: {
109
+ title: 'Default Header',
110
+ level: 'group',
111
+ as: 'h1',
112
+ action: undefined,
113
+ },
114
+ tags: ['autodocs'],
115
+ decorators: [withContainer],
116
+ parameters: {
117
+ docs: {
118
+ toc: true,
119
+ },
120
+ },
35
121
  };
36
122
 
37
- export const WithActionAsLinkPreventingNavigationWithTracking = () => {
38
- return (
39
- <Header
40
- title="Header title"
41
- action={{
42
- text: 'This is a link',
43
- href: 'https://wise.com',
44
- onClick: (event: React.MouseEvent<HTMLElement>) => {
45
- alert('Running onClick handler');
123
+ export default meta;
46
124
 
47
- // we can stop the navigation from happening as onClick will always run before href redirect
48
- event.preventDefault();
49
- },
50
- }}
51
- />
52
- );
125
+ type Story = StoryObj<typeof Header>;
126
+
127
+ export const Playground: Story = {
128
+ render: renderHeader,
129
+ args: {
130
+ title: 'Playground Header',
131
+ level: 'group',
132
+ as: 'h2',
133
+ action: {
134
+ 'aria-label': 'Action',
135
+ text: 'Click me',
136
+ onClick: () => alert('Action clicked!'),
137
+ },
138
+ },
139
+ };
140
+
141
+ /**
142
+ * Demonstrates a `Header` with an associated action.
143
+ */
144
+ export const Action: Story = {
145
+ args: {
146
+ title: 'Header with Action',
147
+ action: {
148
+ 'aria-label': 'Action',
149
+ text: 'Action',
150
+ onClick: () => alert('Action clicked!'),
151
+ },
152
+ },
153
+ argTypes: hideControls(['as', 'className', 'level', 'title', 'testId']),
53
154
  };
155
+
156
+ /**
157
+ * Demonstrates a `Header` with an associated action as a link.
158
+ */
159
+ export const ActionAsLink: Story = {
160
+ args: {
161
+ title: 'Header with Action as Link',
162
+ action: {
163
+ 'aria-label': 'Learn more about this section',
164
+ text: 'Learn more',
165
+ href: 'https://wise.com',
166
+ target: '_blank',
167
+ },
168
+ },
169
+ argTypes: hideControls(['as', 'className', 'level', 'title', 'testId']),
170
+ };
171
+
172
+ /**
173
+ * Demonstrates a `Header` rendered as a custom HTML element.
174
+ */
175
+ export const CustomElement: Story = {
176
+ render: renderHeader,
177
+ args: {
178
+ title: 'Legend Header',
179
+ as: 'legend',
180
+ },
181
+ argTypes: hideControls(['action', 'className', 'level', 'testId']),
182
+ };
183
+
184
+ /**
185
+ * Demonstrates a `Header` with a specific hierarchical level.
186
+ */
187
+ export const SectionLevel: Story = {
188
+ args: {
189
+ title: 'Section Header',
190
+ level: 'section',
191
+ },
192
+ argTypes: hideControls(['action', 'as', 'className', 'title', 'testId']),
193
+ };
194
+
195
+ /**
196
+ * Displays all variants of the `Header` component, including different levels, tags, and actions.
197
+ */
198
+ export const AllVariants = storyConfig(
199
+ {
200
+ tags: ['!autodocs'],
201
+ parameters: {
202
+ padding: '0',
203
+ },
204
+ render: renderVariants,
205
+ },
206
+ { variants: ['default', 'dark', 'bright-green', 'forest-green', 'rtl'] },
207
+ );