@arbor-education/design-system.components 0.13.1 → 0.14.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.
- package/CHANGELOG.md +8 -0
- package/dist/components/articleCard/ArticleCard.d.ts +30 -0
- package/dist/components/articleCard/ArticleCard.d.ts.map +1 -0
- package/dist/components/articleCard/ArticleCard.js +24 -0
- package/dist/components/articleCard/ArticleCard.js.map +1 -0
- package/dist/components/articleCard/ArticleCard.stories.d.ts +18 -0
- package/dist/components/articleCard/ArticleCard.stories.d.ts.map +1 -0
- package/dist/components/articleCard/ArticleCard.stories.js +112 -0
- package/dist/components/articleCard/ArticleCard.stories.js.map +1 -0
- package/dist/components/articleCard/ArticleCard.test.d.ts +2 -0
- package/dist/components/articleCard/ArticleCard.test.d.ts.map +1 -0
- package/dist/components/articleCard/ArticleCard.test.js +49 -0
- package/dist/components/articleCard/ArticleCard.test.js.map +1 -0
- package/dist/components/card/Card.d.ts +41 -12
- package/dist/components/card/Card.d.ts.map +1 -1
- package/dist/components/card/Card.js +46 -17
- package/dist/components/card/Card.js.map +1 -1
- package/dist/components/card/Card.stories.d.ts +9 -84
- package/dist/components/card/Card.stories.d.ts.map +1 -1
- package/dist/components/card/Card.stories.js +15 -73
- package/dist/components/card/Card.stories.js.map +1 -1
- package/dist/components/card/Card.test.js +50 -152
- package/dist/components/card/Card.test.js.map +1 -1
- package/dist/components/formField/inputs/time/TimeInput.d.ts +1 -1
- package/dist/components/formField/inputs/time/TimeInput.stories.d.ts +1 -1
- package/dist/components/icoText/IcoText.d.ts +37 -0
- package/dist/components/icoText/IcoText.d.ts.map +1 -0
- package/dist/components/icoText/IcoText.js +29 -0
- package/dist/components/icoText/IcoText.js.map +1 -0
- package/dist/components/icoText/IcoText.stories.d.ts +34 -0
- package/dist/components/icoText/IcoText.stories.d.ts.map +1 -0
- package/dist/components/icoText/IcoText.stories.js +24 -0
- package/dist/components/icoText/IcoText.stories.js.map +1 -0
- package/dist/components/icoText/IcoText.test.d.ts +2 -0
- package/dist/components/icoText/IcoText.test.d.ts.map +1 -0
- package/dist/components/icoText/IcoText.test.js +27 -0
- package/dist/components/icoText/IcoText.test.js.map +1 -0
- package/dist/components/kpiCard/KPICard.d.ts +13 -0
- package/dist/components/kpiCard/KPICard.d.ts.map +1 -0
- package/dist/components/kpiCard/KPICard.js +8 -0
- package/dist/components/kpiCard/KPICard.js.map +1 -0
- package/dist/components/kpiCard/KPICard.stories.d.ts +9 -0
- package/dist/components/kpiCard/KPICard.stories.d.ts.map +1 -0
- package/dist/components/kpiCard/KPICard.stories.js +18 -0
- package/dist/components/kpiCard/KPICard.stories.js.map +1 -0
- package/dist/components/kpiCard/KPICard.test.d.ts +2 -0
- package/dist/components/kpiCard/KPICard.test.d.ts.map +1 -0
- package/dist/components/kpiCard/KPICard.test.js +37 -0
- package/dist/components/kpiCard/KPICard.test.js.map +1 -0
- package/dist/components/kvpList/KVPList.d.ts +34 -0
- package/dist/components/kvpList/KVPList.d.ts.map +1 -0
- package/dist/components/kvpList/KVPList.js +20 -0
- package/dist/components/kvpList/KVPList.js.map +1 -0
- package/dist/components/kvpList/KVPList.stories.d.ts +27 -0
- package/dist/components/kvpList/KVPList.stories.d.ts.map +1 -0
- package/dist/components/kvpList/KVPList.stories.js +18 -0
- package/dist/components/kvpList/KVPList.stories.js.map +1 -0
- package/dist/components/kvpList/KVPList.test.d.ts +2 -0
- package/dist/components/kvpList/KVPList.test.d.ts.map +1 -0
- package/dist/components/kvpList/KVPList.test.js +29 -0
- package/dist/components/kvpList/KVPList.test.js.map +1 -0
- package/dist/components/singleUser/SingleUser.d.ts +1 -1
- package/dist/components/tabs/TabsItem.stories.d.ts +2 -2
- package/dist/index.css +186 -22
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +13 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/articleCard/ArticleCard.stories.tsx +132 -0
- package/src/components/articleCard/ArticleCard.test.tsx +121 -0
- package/src/components/articleCard/ArticleCard.tsx +100 -0
- package/src/components/articleCard/articleCard.scss +39 -0
- package/src/components/card/Card.stories.tsx +35 -79
- package/src/components/card/Card.test.tsx +72 -190
- package/src/components/card/Card.tsx +117 -58
- package/src/components/card/card.scss +18 -31
- package/src/components/icoText/IcoText.stories.tsx +47 -0
- package/src/components/icoText/IcoText.test.tsx +41 -0
- package/src/components/icoText/IcoText.tsx +93 -0
- package/src/components/icoText/icoText.scss +34 -0
- package/src/components/kpiCard/KPICard.stories.tsx +47 -0
- package/src/components/kpiCard/KPICard.test.tsx +60 -0
- package/src/components/kpiCard/KPICard.tsx +45 -0
- package/src/components/kpiCard/kpiCard.scss +35 -0
- package/src/components/kvpList/KVPList.stories.tsx +51 -0
- package/src/components/kvpList/KVPList.test.tsx +66 -0
- package/src/components/kvpList/KVPList.tsx +109 -0
- package/src/components/kvpList/kvpList.scss +64 -0
- package/src/index.scss +4 -0
- package/src/index.ts +13 -4
- package/src/tokens.scss +6 -0
- package/tokens/json/Arbor.json +30 -0
|
@@ -1,99 +1,55 @@
|
|
|
1
|
-
import type { Meta } from '@storybook/react-vite';
|
|
2
|
-
import { Card } from './Card';
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
3
2
|
import { fn } from 'storybook/test';
|
|
3
|
+
import { Card } from './Card';
|
|
4
4
|
|
|
5
|
-
const meta
|
|
5
|
+
const meta = {
|
|
6
6
|
title: 'Components/Card',
|
|
7
7
|
component: Card,
|
|
8
|
-
}
|
|
8
|
+
} satisfies Meta<typeof Card>;
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
args: {
|
|
12
|
-
title: 'Title of Card',
|
|
13
|
-
paragraph: 'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
|
|
14
|
-
disabled: false,
|
|
15
|
-
onClick: fn(),
|
|
16
|
-
onKeyDown: fn(),
|
|
17
|
-
},
|
|
18
|
-
};
|
|
10
|
+
type Story = StoryObj<typeof meta>;
|
|
19
11
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
onClick: fn(),
|
|
27
|
-
onKeyDown: fn(),
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export const TheEverythingCard = {
|
|
32
|
-
args: {
|
|
33
|
-
title: 'Title of Card',
|
|
34
|
-
paragraph: 'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
|
|
35
|
-
icon: 'eye',
|
|
36
|
-
disabled: false,
|
|
37
|
-
pillText: 'argle bargle',
|
|
38
|
-
pillColor: 'orange',
|
|
39
|
-
onClick: fn(),
|
|
40
|
-
onKeyDown: fn(),
|
|
41
|
-
},
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
export const CardWithTitleParagraphAndPill = {
|
|
45
|
-
args: {
|
|
46
|
-
title: 'Title of Card',
|
|
47
|
-
paragraph: 'Lorem ipsum dolor sit amet consectetur adipiscing elit.',
|
|
48
|
-
disabled: false,
|
|
49
|
-
pillText: 'argle bargle',
|
|
50
|
-
pillColor: 'orange',
|
|
51
|
-
onClick: fn(),
|
|
52
|
-
onKeyDown: fn(),
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
export const CardWithTitleAndIcon = {
|
|
57
|
-
args: {
|
|
58
|
-
title: 'Title of Card',
|
|
59
|
-
icon: 'eye',
|
|
60
|
-
disabled: false,
|
|
61
|
-
onClick: fn(),
|
|
62
|
-
onKeyDown: fn(),
|
|
63
|
-
},
|
|
64
|
-
};
|
|
12
|
+
const sampleCardContent = (
|
|
13
|
+
<>
|
|
14
|
+
<h3>Attendance summary</h3>
|
|
15
|
+
<p>View the latest attendance and behaviour insights for this cohort.</p>
|
|
16
|
+
</>
|
|
17
|
+
);
|
|
65
18
|
|
|
66
|
-
export const
|
|
19
|
+
export const StaticCard: Story = {
|
|
67
20
|
args: {
|
|
68
|
-
|
|
69
|
-
disabled: false,
|
|
70
|
-
onClick: fn(),
|
|
71
|
-
onKeyDown: fn(),
|
|
21
|
+
'aria-label': 'Static summary card',
|
|
72
22
|
},
|
|
23
|
+
render: args => (
|
|
24
|
+
<Card {...args}>
|
|
25
|
+
{sampleCardContent}
|
|
26
|
+
</Card>
|
|
27
|
+
),
|
|
73
28
|
};
|
|
74
29
|
|
|
75
|
-
export const
|
|
30
|
+
export const ClickableCard: Story = {
|
|
76
31
|
args: {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
disabled: true,
|
|
81
|
-
pillText: 'argle bargle',
|
|
82
|
-
pillColor: 'orange',
|
|
83
|
-
onClick: fn(),
|
|
84
|
-
onKeyDown: fn(),
|
|
32
|
+
'onClick': fn(),
|
|
33
|
+
'onKeyDown': fn(),
|
|
34
|
+
'aria-label': 'Clickable card',
|
|
85
35
|
},
|
|
36
|
+
render: args => (
|
|
37
|
+
<Card {...args}>
|
|
38
|
+
{sampleCardContent}
|
|
39
|
+
</Card>
|
|
40
|
+
),
|
|
86
41
|
};
|
|
87
42
|
|
|
88
|
-
export const
|
|
43
|
+
export const DenseCard: Story = {
|
|
89
44
|
args: {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
icon: 'eye',
|
|
93
|
-
disabled: true,
|
|
94
|
-
pillText: 'argle bargle',
|
|
95
|
-
pillColor: 'orange',
|
|
45
|
+
'aria-label': 'Dense card',
|
|
46
|
+
'spacing': 'dense',
|
|
96
47
|
},
|
|
48
|
+
render: args => (
|
|
49
|
+
<Card {...args}>
|
|
50
|
+
{sampleCardContent}
|
|
51
|
+
</Card>
|
|
52
|
+
),
|
|
97
53
|
};
|
|
98
54
|
|
|
99
55
|
export default meta;
|
|
@@ -4,222 +4,104 @@ import { render, screen, fireEvent } from '@testing-library/react';
|
|
|
4
4
|
import { Card } from './Card';
|
|
5
5
|
import '@testing-library/jest-dom/vitest';
|
|
6
6
|
|
|
7
|
-
describe('Card
|
|
8
|
-
test('renders
|
|
9
|
-
render(
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
describe('Card', () => {
|
|
8
|
+
test('renders children inside the card shell', () => {
|
|
9
|
+
const { container } = render(
|
|
10
|
+
<Card aria-label="Summary card">
|
|
11
|
+
<div>Custom content</div>
|
|
12
|
+
</Card>,
|
|
13
|
+
);
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
render(<Card title="Test Title" />);
|
|
17
|
-
expect(screen.getByRole('heading', { level: 4 })).toHaveTextContent('Test Title');
|
|
18
|
-
expect(screen.getByText('Test Title')).toHaveClass('ds-card__title');
|
|
19
|
-
});
|
|
15
|
+
const card = container.querySelector('figure');
|
|
20
16
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
expect(screen.getByText('
|
|
24
|
-
expect(screen.getByText('Test paragraph content')).toHaveClass('ds-card__paragraph');
|
|
17
|
+
expect(card).toBeInTheDocument();
|
|
18
|
+
expect(card).toHaveAttribute('aria-label', 'Summary card');
|
|
19
|
+
expect(screen.getByText('Custom content')).toBeInTheDocument();
|
|
25
20
|
});
|
|
26
21
|
|
|
27
|
-
test('renders
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
test('renders clickable affordances when interactive', () => {
|
|
23
|
+
const mockClick = vi.fn();
|
|
24
|
+
const { container } = render(
|
|
25
|
+
<Card onClick={mockClick} aria-label="Clickable card">
|
|
26
|
+
<div>Clickable content</div>
|
|
27
|
+
</Card>,
|
|
28
|
+
);
|
|
32
29
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
expect(container.querySelector('.ds-icon-eye')).toBeInTheDocument();
|
|
36
|
-
});
|
|
30
|
+
const card = screen.getByRole('button', { name: 'Clickable card' });
|
|
31
|
+
fireEvent.click(card);
|
|
37
32
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
expect(
|
|
33
|
+
expect(mockClick).toHaveBeenCalledTimes(1);
|
|
34
|
+
expect(card).toHaveClass('ds-card__container--clickable');
|
|
35
|
+
expect(card).toHaveAttribute('tabIndex', '0');
|
|
36
|
+
expect(container.querySelector('.ds-icon-chevron-right')).toBeInTheDocument();
|
|
37
|
+
expect(container.querySelector('.ds-icon-arrow-right')).toBeInTheDocument();
|
|
41
38
|
});
|
|
42
39
|
|
|
43
|
-
test('
|
|
40
|
+
test('does not render clickable affordances when disabled', () => {
|
|
44
41
|
const mockClick = vi.fn();
|
|
45
|
-
const mockKeyDown = vi.fn();
|
|
46
|
-
|
|
47
42
|
const { container } = render(
|
|
48
|
-
<Card
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
icon="eye"
|
|
52
|
-
iconColor="#blue"
|
|
53
|
-
tagText="Complete"
|
|
54
|
-
tagColor="green"
|
|
55
|
-
onClick={mockClick}
|
|
56
|
-
onKeyDown={mockKeyDown}
|
|
57
|
-
/>,
|
|
43
|
+
<Card disabled onClick={mockClick} aria-label="Disabled card">
|
|
44
|
+
<div>Disabled content</div>
|
|
45
|
+
</Card>,
|
|
58
46
|
);
|
|
59
47
|
|
|
60
|
-
|
|
61
|
-
expect(screen.getByText('This is a complete card')).toBeInTheDocument();
|
|
62
|
-
expect(container.querySelector('.ds-icon-eye')).toBeInTheDocument();
|
|
63
|
-
expect(screen.getByText('Complete')).toBeInTheDocument();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
describe('Click interactions', () => {
|
|
67
|
-
test('calls onClick handler when card is clicked', () => {
|
|
68
|
-
const mockClick = vi.fn();
|
|
69
|
-
render(<Card title="Clickable Card" onClick={mockClick} />);
|
|
70
|
-
|
|
71
|
-
const card = screen.getByRole('article');
|
|
72
|
-
fireEvent.click(card);
|
|
73
|
-
|
|
74
|
-
expect(mockClick).toHaveBeenCalledTimes(1);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test('does not call onClick when card is disabled', () => {
|
|
78
|
-
const mockClick = vi.fn();
|
|
79
|
-
render(<Card title="Disabled Card" onClick={mockClick} disabled />);
|
|
80
|
-
|
|
81
|
-
const card = screen.getByRole('article');
|
|
82
|
-
fireEvent.click(card);
|
|
83
|
-
|
|
84
|
-
expect(mockClick).not.toHaveBeenCalled();
|
|
85
|
-
});
|
|
48
|
+
const card = screen.getByRole('button', { name: 'Disabled card' });
|
|
86
49
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
50
|
+
expect(card).toHaveClass('ds-card__container--disabled');
|
|
51
|
+
expect(card).toHaveAttribute('aria-disabled', 'true');
|
|
52
|
+
expect(card).toHaveAttribute('role', 'button');
|
|
53
|
+
expect(card).toHaveAttribute('tabIndex', '0');
|
|
54
|
+
expect(container.querySelector('.ds-icon-chevron-right')).not.toBeInTheDocument();
|
|
90
55
|
|
|
91
|
-
|
|
92
|
-
|
|
56
|
+
card.focus();
|
|
57
|
+
expect(card).toHaveFocus();
|
|
93
58
|
|
|
94
|
-
|
|
95
|
-
|
|
59
|
+
fireEvent.click(card);
|
|
60
|
+
fireEvent.keyDown(card, { key: 'Enter' });
|
|
61
|
+
fireEvent.keyDown(card, { key: ' ' });
|
|
96
62
|
|
|
97
|
-
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test('does not show click arrow icon when card is disabled', () => {
|
|
101
|
-
const mockClick = vi.fn();
|
|
102
|
-
const { container } = render(<Card title="Disabled Card" onClick={mockClick} disabled />);
|
|
103
|
-
|
|
104
|
-
expect(container.querySelector('.ds-icon-chevron-right')).not.toBeInTheDocument();
|
|
105
|
-
});
|
|
63
|
+
expect(mockClick).not.toHaveBeenCalled();
|
|
106
64
|
});
|
|
107
65
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
render(<Card title="Keyboard Card" onClick={vi.fn()} onKeyDown={mockKeyDown} />);
|
|
112
|
-
|
|
113
|
-
const card = screen.getByRole('article');
|
|
114
|
-
fireEvent.keyDown(card, { key: 'Enter' });
|
|
115
|
-
|
|
116
|
-
expect(mockKeyDown).toHaveBeenCalledTimes(1);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test('does not call onKeyDown when card is disabled', () => {
|
|
120
|
-
const mockKeyDown = vi.fn();
|
|
121
|
-
render(<Card title="Disabled Card" onClick={vi.fn()} onKeyDown={mockKeyDown} disabled />);
|
|
122
|
-
|
|
123
|
-
const card = screen.getByRole('article');
|
|
124
|
-
fireEvent.keyDown(card, { key: 'Enter' });
|
|
125
|
-
|
|
126
|
-
expect(mockKeyDown).not.toHaveBeenCalled();
|
|
127
|
-
});
|
|
66
|
+
test('calls onKeyDown and keyboard-activates the card on Enter', () => {
|
|
67
|
+
const mockClick = vi.fn();
|
|
68
|
+
const mockKeyDown = vi.fn();
|
|
128
69
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
70
|
+
render(
|
|
71
|
+
<Card onClick={mockClick} onKeyDown={mockKeyDown} aria-label="Keyboard card">
|
|
72
|
+
<div>Keyboard content</div>
|
|
73
|
+
</Card>,
|
|
74
|
+
);
|
|
132
75
|
|
|
133
|
-
|
|
134
|
-
|
|
76
|
+
const card = screen.getByRole('button', { name: 'Keyboard card' });
|
|
77
|
+
fireEvent.keyDown(card, { key: 'Enter' });
|
|
135
78
|
|
|
136
|
-
|
|
137
|
-
|
|
79
|
+
expect(mockKeyDown).toHaveBeenCalledTimes(1);
|
|
80
|
+
expect(mockClick).toHaveBeenCalledTimes(1);
|
|
138
81
|
});
|
|
139
82
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
test('applies disabled class when disabled', () => {
|
|
150
|
-
render(<Card disabled />);
|
|
151
|
-
|
|
152
|
-
const card = screen.getByRole('article');
|
|
153
|
-
expect(card).toHaveClass('ds-card__container--disabled');
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
test('does not apply clickable class when disabled', () => {
|
|
157
|
-
const mockClick = vi.fn();
|
|
158
|
-
render(<Card onClick={mockClick} disabled />);
|
|
159
|
-
|
|
160
|
-
const card = screen.getByRole('article');
|
|
161
|
-
expect(card).not.toHaveClass('ds-card__container--clickable');
|
|
162
|
-
expect(card).toHaveClass('ds-card__container--disabled');
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
test('sets correct tabIndex for clickable card', () => {
|
|
166
|
-
const mockClick = vi.fn();
|
|
167
|
-
render(<Card onClick={mockClick} />);
|
|
168
|
-
|
|
169
|
-
const card = screen.getByRole('article');
|
|
170
|
-
expect(card).toHaveAttribute('tabIndex', '0');
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
test('sets correct tabIndex for non-clickable card', () => {
|
|
174
|
-
render(<Card />);
|
|
175
|
-
|
|
176
|
-
const card = screen.getByRole('article');
|
|
177
|
-
expect(card).toHaveAttribute('tabIndex', '-1');
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
test('sets correct tabIndex for disabled card', () => {
|
|
181
|
-
const mockClick = vi.fn();
|
|
182
|
-
render(<Card onClick={mockClick} disabled />);
|
|
183
|
-
|
|
184
|
-
const card = screen.getByRole('article');
|
|
185
|
-
expect(card).toHaveAttribute('tabIndex', '-1');
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
test('has correct aria-label', () => {
|
|
189
|
-
render(<Card />);
|
|
83
|
+
test('passes through aria-labelledby', () => {
|
|
84
|
+
const { container } = render(
|
|
85
|
+
<>
|
|
86
|
+
<h2 id="card-heading">Linked heading</h2>
|
|
87
|
+
<Card aria-labelledby="card-heading">
|
|
88
|
+
<div>Linked content</div>
|
|
89
|
+
</Card>
|
|
90
|
+
</>,
|
|
91
|
+
);
|
|
190
92
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
});
|
|
93
|
+
const card = container.querySelector('figure');
|
|
94
|
+
expect(card).toHaveAttribute('aria-labelledby', 'card-heading');
|
|
194
95
|
});
|
|
195
96
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
fireEvent.click(card);
|
|
203
|
-
|
|
204
|
-
expect(mockClick).toHaveBeenCalledTimes(1);
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
test('handles onKeyDown without onClick', () => {
|
|
208
|
-
const mockKeyDown = vi.fn();
|
|
209
|
-
render(<Card title="KeyDown only" onKeyDown={mockKeyDown} />);
|
|
210
|
-
|
|
211
|
-
const card = screen.getByRole('article');
|
|
212
|
-
fireEvent.keyDown(card, { key: 'Enter' });
|
|
213
|
-
|
|
214
|
-
// Should not be called because card is not clickable (no onClick)
|
|
215
|
-
expect(mockKeyDown).not.toHaveBeenCalled();
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
test('does not throw error when clicking card without handlers', () => {
|
|
219
|
-
render(<Card title="No handlers" />);
|
|
97
|
+
test('applies dense spacing when requested', () => {
|
|
98
|
+
const { container } = render(
|
|
99
|
+
<Card spacing="dense">
|
|
100
|
+
<div>Dense content</div>
|
|
101
|
+
</Card>,
|
|
102
|
+
);
|
|
220
103
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
});
|
|
104
|
+
const card = container.querySelector('figure');
|
|
105
|
+
expect(card).toHaveClass('ds-card__container--dense');
|
|
224
106
|
});
|
|
225
107
|
});
|
|
@@ -1,86 +1,142 @@
|
|
|
1
1
|
import classNames from 'classnames';
|
|
2
|
-
|
|
3
2
|
import { Icon } from '../icon/Icon';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
title?: string;
|
|
9
|
-
paragraph?: string;
|
|
10
|
-
icon?: keyof typeof allowedIcons;
|
|
11
|
-
iconColor?: string;
|
|
3
|
+
|
|
4
|
+
type CardBaseProps = {
|
|
5
|
+
children?: React.ReactNode;
|
|
6
|
+
className?: string;
|
|
12
7
|
disabled?: boolean;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
spacing?: 'default' | 'dense';
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type CardAccessibleNameProps
|
|
12
|
+
= | {
|
|
13
|
+
'aria-label': string;
|
|
14
|
+
'aria-labelledby'?: string;
|
|
15
|
+
}
|
|
16
|
+
| {
|
|
17
|
+
'aria-label'?: string;
|
|
18
|
+
'aria-labelledby': string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type CardInteractiveProps = CardAccessibleNameProps & {
|
|
22
|
+
onClick: (e: React.MouseEvent<HTMLElement>) => void;
|
|
16
23
|
onKeyDown?: (e: React.KeyboardEvent<HTMLElement>) => void;
|
|
17
|
-
iconScreenReaderText?: string;
|
|
18
24
|
};
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
26
|
+
type CardStaticProps = {
|
|
27
|
+
'onClick'?: undefined;
|
|
28
|
+
'onKeyDown'?: (e: React.KeyboardEvent<HTMLElement>) => void;
|
|
29
|
+
'aria-label'?: string;
|
|
30
|
+
'aria-labelledby'?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type CardInteractionProps = CardInteractiveProps | CardStaticProps;
|
|
34
|
+
|
|
35
|
+
type CardResolvedInteractionProps
|
|
36
|
+
= | {
|
|
37
|
+
'onClick': (e: React.MouseEvent<HTMLElement>) => void;
|
|
38
|
+
'onKeyDown'?: (e: React.KeyboardEvent<HTMLElement>) => void;
|
|
39
|
+
'aria-label': string;
|
|
40
|
+
}
|
|
41
|
+
| {
|
|
42
|
+
'onClick': (e: React.MouseEvent<HTMLElement>) => void;
|
|
43
|
+
'onKeyDown'?: (e: React.KeyboardEvent<HTMLElement>) => void;
|
|
44
|
+
'aria-labelledby': string;
|
|
45
|
+
}
|
|
46
|
+
| {
|
|
47
|
+
'onClick'?: undefined;
|
|
48
|
+
'onKeyDown'?: (e: React.KeyboardEvent<HTMLElement>) => void;
|
|
49
|
+
'aria-label'?: string;
|
|
50
|
+
'aria-labelledby'?: string;
|
|
36
51
|
};
|
|
37
52
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
53
|
+
export type CardProps = CardBaseProps & CardInteractionProps;
|
|
54
|
+
|
|
55
|
+
export const getCardInteractionProps = (props: CardInteractionProps): CardResolvedInteractionProps => {
|
|
56
|
+
if (props.onClick === undefined) {
|
|
57
|
+
return {
|
|
58
|
+
'onKeyDown': props.onKeyDown,
|
|
59
|
+
'aria-label': props['aria-label'],
|
|
60
|
+
'aria-labelledby': props['aria-labelledby'],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (props['aria-label'] !== undefined) {
|
|
65
|
+
return {
|
|
66
|
+
'onClick': props.onClick,
|
|
67
|
+
'onKeyDown': props.onKeyDown,
|
|
68
|
+
'aria-label': props['aria-label'],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (props['aria-labelledby'] === undefined) {
|
|
73
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
74
|
+
throw new Error('Interactive Card requires aria-label or aria-labelledby.');
|
|
41
75
|
}
|
|
76
|
+
|
|
77
|
+
console.error('Interactive Card requires aria-label or aria-labelledby.');
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
onClick: props.onClick,
|
|
81
|
+
onKeyDown: props.onKeyDown,
|
|
82
|
+
} as CardResolvedInteractionProps;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
'onClick': props.onClick,
|
|
87
|
+
'onKeyDown': props.onKeyDown,
|
|
88
|
+
'aria-labelledby': props['aria-labelledby'],
|
|
42
89
|
};
|
|
90
|
+
};
|
|
43
91
|
|
|
44
|
-
|
|
92
|
+
export const Card = ({
|
|
93
|
+
children,
|
|
94
|
+
className,
|
|
95
|
+
onClick,
|
|
96
|
+
onKeyDown,
|
|
97
|
+
disabled = false,
|
|
98
|
+
spacing = 'default',
|
|
99
|
+
'aria-label': ariaLabel,
|
|
100
|
+
'aria-labelledby': ariaLabelledBy,
|
|
101
|
+
}: CardProps): React.JSX.Element => {
|
|
102
|
+
const isCardInteractive = Boolean(onClick);
|
|
103
|
+
const isCardClickable = isCardInteractive && !disabled;
|
|
45
104
|
|
|
46
105
|
return (
|
|
47
|
-
<
|
|
48
|
-
className={classNames('ds-card__container', {
|
|
106
|
+
<figure
|
|
107
|
+
className={classNames('ds-card__container', className, {
|
|
49
108
|
'ds-card__container--clickable': isCardClickable,
|
|
50
109
|
'ds-card__container--disabled': disabled,
|
|
110
|
+
'ds-card__container--dense': spacing === 'dense',
|
|
51
111
|
})}
|
|
52
112
|
onClick={(e) => {
|
|
53
113
|
if (isCardClickable) {
|
|
54
|
-
|
|
114
|
+
onClick?.(e);
|
|
55
115
|
}
|
|
56
116
|
}}
|
|
57
117
|
onKeyDown={(e) => {
|
|
118
|
+
if (!isCardInteractive) return;
|
|
119
|
+
|
|
120
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
|
|
123
|
+
if (isCardClickable) {
|
|
124
|
+
e.currentTarget.click();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
58
128
|
if (isCardClickable) {
|
|
59
|
-
|
|
129
|
+
onKeyDown?.(e);
|
|
60
130
|
}
|
|
61
131
|
}}
|
|
62
|
-
aria-
|
|
63
|
-
|
|
132
|
+
aria-disabled={isCardInteractive && disabled ? true : undefined}
|
|
133
|
+
aria-label={ariaLabel}
|
|
134
|
+
aria-labelledby={ariaLabelledBy}
|
|
135
|
+
role={isCardInteractive ? 'button' : undefined}
|
|
136
|
+
tabIndex={isCardInteractive ? 0 : undefined}
|
|
64
137
|
>
|
|
65
138
|
<div className="ds-card__content">
|
|
66
|
-
{
|
|
67
|
-
<Icon
|
|
68
|
-
name={icon}
|
|
69
|
-
className="ds-card__icon-left"
|
|
70
|
-
screenReaderText={iconScreenReaderText}
|
|
71
|
-
color={iconColor}
|
|
72
|
-
size={24}
|
|
73
|
-
/>
|
|
74
|
-
)}
|
|
75
|
-
<div className="ds-card__text">
|
|
76
|
-
{title && (
|
|
77
|
-
<span className="ds-card__title-container">
|
|
78
|
-
{title && <h4 className="ds-card__title">{title}</h4>}
|
|
79
|
-
</span>
|
|
80
|
-
)}
|
|
81
|
-
{paragraph && <p className="ds-card__paragraph">{paragraph}</p>}
|
|
82
|
-
{tagText && <Tag color={tagColor}>{tagText}</Tag>}
|
|
83
|
-
</div>
|
|
139
|
+
<div className="ds-card__body">{children}</div>
|
|
84
140
|
{isCardClickable && (
|
|
85
141
|
<>
|
|
86
142
|
<Icon
|
|
@@ -96,10 +152,13 @@ export const Card = ({
|
|
|
96
152
|
</>
|
|
97
153
|
)}
|
|
98
154
|
</div>
|
|
99
|
-
</
|
|
155
|
+
</figure>
|
|
100
156
|
);
|
|
101
157
|
};
|
|
102
158
|
|
|
103
159
|
export namespace Card {
|
|
104
160
|
export type Props = CardProps;
|
|
161
|
+
export type AccessibleNameProps = CardAccessibleNameProps;
|
|
162
|
+
export type InteractiveProps = CardInteractiveProps;
|
|
163
|
+
export type InteractionProps = CardInteractionProps;
|
|
105
164
|
}
|