akeneo-design-system 0.1.226 → 0.1.227
Sign up to get free protection for your applications and to get access to all the features.
- package/example/src/__snapshots__/App.test.js.snap +1 -1
- package/jest-puppeteer.config.js +1 -0
- package/lib/components/Avatar/Avatar.d.ts +12 -0
- package/lib/components/Avatar/Avatar.js +89 -0
- package/lib/components/Avatar/Avatar.js.map +1 -0
- package/lib/components/Avatar/Avatars.d.ts +8 -0
- package/lib/components/Avatar/Avatars.js +71 -0
- package/lib/components/Avatar/Avatars.js.map +1 -0
- package/lib/components/Dropdown/Dropdown.d.ts +1 -1
- package/lib/components/Input/BooleanInput/BooleanInput.d.ts +1 -1
- package/lib/components/Input/BooleanInput/BooleanInput.js +1 -1
- package/lib/components/Input/BooleanInput/BooleanInput.js.map +1 -1
- package/lib/components/Input/MultiSelectInput/MultiSelectInput.d.ts +8 -2
- package/lib/components/Input/MultiSelectInput/MultiSelectInput.js +11 -9
- package/lib/components/Input/MultiSelectInput/MultiSelectInput.js.map +1 -1
- package/lib/components/Input/SelectInput/SelectInput.d.ts +8 -2
- package/lib/components/Input/SelectInput/SelectInput.js +13 -11
- package/lib/components/Input/SelectInput/SelectInput.js.map +1 -1
- package/lib/components/Input/TableInput/TableInput.d.ts +1 -1
- package/lib/components/Input/TableInput/TableInputRow/TableInputRow.js +1 -1
- package/lib/components/Input/TableInput/TableInputRow/TableInputRow.js.map +1 -1
- package/lib/components/Input/TagInput/TagInput.js +5 -2
- package/lib/components/Input/TagInput/TagInput.js.map +1 -1
- package/lib/components/ProgressIndicator/ProgressIndicator.d.ts +1 -1
- package/lib/components/index.d.ts +2 -0
- package/lib/components/index.js +2 -0
- package/lib/components/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__image_snapshots__/all-visual-tsx-visual-tests-renders-components-avatar-avatar-list-correctly-1-snap.png +0 -0
- package/src/__image_snapshots__/all-visual-tsx-visual-tests-renders-components-avatar-avatar-size-correctly-1-snap.png +0 -0
- package/src/__image_snapshots__/all-visual-tsx-visual-tests-renders-components-avatar-background-colors-correctly-1-snap.png +0 -0
- package/src/__image_snapshots__/all-visual-tsx-visual-tests-renders-components-avatar-standard-correctly-1-snap.png +0 -0
- package/src/__image_snapshots__/all-visual-tsx-visual-tests-renders-components-avatar-with-image-correctly-1-snap.png +0 -0
- package/src/components/Avatar/Avatar.stories.mdx +147 -0
- package/src/components/Avatar/Avatar.tsx +102 -0
- package/src/components/Avatar/Avatar.unit.tsx +79 -0
- package/src/components/Avatar/Avatars.tsx +50 -0
- package/src/components/Avatar/Avatars.unit.tsx +41 -0
- package/src/components/Input/BooleanInput/BooleanInput.tsx +2 -2
- package/src/components/Input/MultiSelectInput/MultiSelectInput.tsx +26 -16
- package/src/components/Input/MultiSelectInput/MultiSelectInput.unit.tsx +45 -0
- package/src/components/Input/SelectInput/SelectInput.tsx +28 -18
- package/src/components/Input/SelectInput/SelectInput.unit.tsx +52 -0
- package/src/components/Input/TableInput/TableInputRow/TableInputRow.tsx +2 -7
- package/src/components/Input/TagInput/TagInput.tsx +3 -3
- package/src/components/index.ts +2 -0
package/lib/components/index.js
CHANGED
@@ -10,6 +10,8 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
10
10
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
11
11
|
};
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
13
|
+
__exportStar(require("./Avatar/Avatar"), exports);
|
14
|
+
__exportStar(require("./Avatar/Avatars"), exports);
|
13
15
|
__exportStar(require("./Badge/Badge"), exports);
|
14
16
|
__exportStar(require("./Block/Block"), exports);
|
15
17
|
__exportStar(require("./BlockButton/BlockButton"), exports);
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,gDAA8B;AAC9B,gDAA8B;AAC9B,4DAA0C;AAC1C,0DAAwC;AACxC,kDAAgC;AAChC,8CAA4B;AAC5B,sDAAoC;AACpC,sDAAoC;AACpC,sDAAoC;AACpC,gDAA8B;AAC9B,kDAAgC;AAChC,0DAAwC;AACxC,sDAAoC;AACpC,gDAA8B;AAC9B,4DAA0C;AAC1C,0CAAwB;AACxB,wDAAsC;AACtC,8CAA4B;AAC5B,8CAA4B;AAC5B,kDAAgC;AAChC,0DAAwC;AACxC,gDAA8B;AAC9B,+CAA6B;AAC7B,0DAAwC;AACxC,8CAA4B;AAC5B,4DAA0C;AAC1C,oDAAkC;AAClC,4DAA0C;AAC1C,wEAAsD;AACtD,kDAAgC;AAChC,8DAA4C;AAC5C,kEAAgD;AAChD,kDAAgC;AAChC,gDAA8B;AAC9B,8CAA4B;AAC5B,gDAA8B;AAC9B,oDAAkC;AAClC,8CAA4B;AAC5B,oDAAkC"}
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,kDAAgC;AAChC,mDAAiC;AACjC,gDAA8B;AAC9B,gDAA8B;AAC9B,4DAA0C;AAC1C,0DAAwC;AACxC,kDAAgC;AAChC,8CAA4B;AAC5B,sDAAoC;AACpC,sDAAoC;AACpC,sDAAoC;AACpC,gDAA8B;AAC9B,kDAAgC;AAChC,0DAAwC;AACxC,sDAAoC;AACpC,gDAA8B;AAC9B,4DAA0C;AAC1C,0CAAwB;AACxB,wDAAsC;AACtC,8CAA4B;AAC5B,8CAA4B;AAC5B,kDAAgC;AAChC,0DAAwC;AACxC,gDAA8B;AAC9B,+CAA6B;AAC7B,0DAAwC;AACxC,8CAA4B;AAC5B,4DAA0C;AAC1C,oDAAkC;AAClC,4DAA0C;AAC1C,wEAAsD;AACtD,kDAAgC;AAChC,8DAA4C;AAC5C,kEAAgD;AAChD,kDAAgC;AAChC,gDAA8B;AAC9B,8CAA4B;AAC5B,gDAA8B;AAC9B,oDAAkC;AAClC,8CAA4B;AAC5B,oDAAkC"}
|
package/package.json
CHANGED
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,147 @@
|
|
1
|
+
import {Meta, Story, ArgsTable, Canvas} from '@storybook/addon-docs';
|
2
|
+
import {Avatar} from './Avatar.tsx';
|
3
|
+
import {Avatars} from './Avatars.tsx';
|
4
|
+
|
5
|
+
<Meta
|
6
|
+
title="Components/Avatar"
|
7
|
+
component={Avatar}
|
8
|
+
argTypes={{
|
9
|
+
firstName: {control: {type: 'text'}},
|
10
|
+
lastName: {control: {type: 'text'}},
|
11
|
+
username: {control: {type: 'text'}},
|
12
|
+
avatarUrl: {control: {type: 'text'}},
|
13
|
+
size: {control: {type: 'select', options: ['default', 'big']}},
|
14
|
+
}}
|
15
|
+
args={{
|
16
|
+
firstName: 'John',
|
17
|
+
lastName: 'Doe',
|
18
|
+
username: 'admin',
|
19
|
+
avatarUrl: undefined,
|
20
|
+
size: 'default',
|
21
|
+
}}
|
22
|
+
/>
|
23
|
+
|
24
|
+
# Avatar
|
25
|
+
|
26
|
+
## Usage
|
27
|
+
|
28
|
+
This component is used to display users avatars. If no avatar is available, first letters of the first and last name are
|
29
|
+
displayed with a dedicated color.
|
30
|
+
|
31
|
+
## Playground
|
32
|
+
|
33
|
+
<Canvas>
|
34
|
+
<Story name="Standard">
|
35
|
+
{args => {
|
36
|
+
return <Avatar firstName={'John'} lastName={'Doe'} username={'admin'} {...args} />;
|
37
|
+
}}
|
38
|
+
</Story>
|
39
|
+
</Canvas>
|
40
|
+
|
41
|
+
<ArgsTable story="Standard" />
|
42
|
+
|
43
|
+
## Variation on background colors
|
44
|
+
|
45
|
+
The background color is based from the username.
|
46
|
+
|
47
|
+
<Canvas>
|
48
|
+
<Story name="Background colors">
|
49
|
+
{args => {
|
50
|
+
return (
|
51
|
+
<>
|
52
|
+
<Avatar {...args} firstName={'Albert'} lastName={'Doe'} username={'a'} />
|
53
|
+
<Avatar {...args} firstName={'Bertrand'} lastName={'Doe'} username={'b'} />
|
54
|
+
<Avatar {...args} firstName={'Chris'} lastName={'Doe'} username={'c'} />
|
55
|
+
<Avatar {...args} firstName={'Danny'} lastName={'Doe'} username={'d'} />
|
56
|
+
<Avatar {...args} firstName={'Elon'} lastName={'Doe'} username={'e'} />
|
57
|
+
<Avatar {...args} firstName={'Fred'} lastName={'Doe'} username={'f'} />
|
58
|
+
<Avatar {...args} firstName={'Gus'} lastName={'Doe'} username={'g'} />
|
59
|
+
<Avatar {...args} firstName={'Helen'} lastName={'Doe'} username={'h'} />
|
60
|
+
<Avatar {...args} firstName={'Isabel'} lastName={'Doe'} username={'i'} />
|
61
|
+
<Avatar {...args} firstName={'John'} lastName={'Doe'} username={'j'} />
|
62
|
+
<Avatar {...args} firstName={'Kurt'} lastName={'Doe'} username={'k'} />
|
63
|
+
<Avatar {...args} firstName={'Leonard'} lastName={'Doe'} username={'l'} />
|
64
|
+
</>
|
65
|
+
);
|
66
|
+
}}
|
67
|
+
</Story>
|
68
|
+
</Canvas>
|
69
|
+
|
70
|
+
## Variation with image
|
71
|
+
|
72
|
+
<Canvas>
|
73
|
+
<Story name="With image">
|
74
|
+
{args => {
|
75
|
+
return (
|
76
|
+
<>
|
77
|
+
<Avatar {...args} avatarUrl={'https://picsum.photos/seed/akeneo/32/32'} />
|
78
|
+
</>
|
79
|
+
);
|
80
|
+
}}
|
81
|
+
</Story>
|
82
|
+
</Canvas>
|
83
|
+
|
84
|
+
## Variation on List
|
85
|
+
|
86
|
+
You can use a dedicated component to display avatar list. After a defined maximum, other avatars are not displayed.
|
87
|
+
|
88
|
+
<Canvas>
|
89
|
+
<Story name="Avatar list">
|
90
|
+
{args => {
|
91
|
+
return (
|
92
|
+
<>
|
93
|
+
<Avatars
|
94
|
+
max={5}
|
95
|
+
title="Helen Doe Isabel Doe John Doe Kurt Doe Leonard Doe"
|
96
|
+
>
|
97
|
+
<Avatar
|
98
|
+
{...args}
|
99
|
+
firstName={'Albert'}
|
100
|
+
lastName={'Doe'}
|
101
|
+
username={'a'}
|
102
|
+
avatarUrl={'https://picsum.photos/seed/akeneo/32/32'}
|
103
|
+
/>
|
104
|
+
<Avatar {...args} firstName={'Bertrand'} lastName={'Doe'} username={'b'} />
|
105
|
+
<Avatar
|
106
|
+
{...args}
|
107
|
+
firstName={'Chris'}
|
108
|
+
lastName={'Doe'}
|
109
|
+
username={'c'}
|
110
|
+
avatarUrl={'https://picsum.photos/seed/bkeneo/32/32'}
|
111
|
+
/>
|
112
|
+
<Avatar {...args} firstName={'Danny'} lastName={'Doe'} username={'d'} />
|
113
|
+
<Avatar
|
114
|
+
{...args}
|
115
|
+
firstName={'Elon'}
|
116
|
+
lastName={'Doe'}
|
117
|
+
username={'e'}
|
118
|
+
avatarUrl={'https://picsum.photos/seed/ckeneo/32/32'}
|
119
|
+
/>
|
120
|
+
<Avatar {...args} firstName={'Fred'} lastName={'Doe'} username={'f'} />
|
121
|
+
<Avatar {...args} firstName={'Gus'} lastName={'Doe'} username={'g'} />
|
122
|
+
<Avatar {...args} firstName={'Helen'} lastName={'Doe'} username={'h'} />
|
123
|
+
<Avatar {...args} firstName={'Isabel'} lastName={'Doe'} username={'i'} />
|
124
|
+
<Avatar {...args} firstName={'John'} lastName={'Doe'} username={'j'} />
|
125
|
+
<Avatar {...args} firstName={'Kurt'} lastName={'Doe'} username={'k'} />
|
126
|
+
<Avatar {...args} firstName={'Leonard'} lastName={'Doe'} username={'l'} />
|
127
|
+
</Avatars>
|
128
|
+
</>
|
129
|
+
);
|
130
|
+
}}
|
131
|
+
</Story>
|
132
|
+
</Canvas>
|
133
|
+
|
134
|
+
## Variation on Size
|
135
|
+
|
136
|
+
<Canvas>
|
137
|
+
<Story name="Avatar size">
|
138
|
+
{args => {
|
139
|
+
return (
|
140
|
+
<>
|
141
|
+
<Avatar {...args} size={'default'} />
|
142
|
+
<Avatar {...args} size={'big'} />
|
143
|
+
</>
|
144
|
+
);
|
145
|
+
}}
|
146
|
+
</Story>
|
147
|
+
</Canvas>
|
@@ -0,0 +1,102 @@
|
|
1
|
+
import React, {useMemo} from 'react';
|
2
|
+
import styled, {css} from 'styled-components';
|
3
|
+
import {useTheme} from '../../hooks';
|
4
|
+
import {Override} from '../../shared';
|
5
|
+
import {AkeneoThemedProps, getColor} from '../../theme';
|
6
|
+
|
7
|
+
const AvatarContainer = styled.span<AvatarProps & AkeneoThemedProps>`
|
8
|
+
${({size}) =>
|
9
|
+
size === 'default'
|
10
|
+
? css`
|
11
|
+
height: 32px;
|
12
|
+
width: 32px;
|
13
|
+
line-height: 32px;
|
14
|
+
font-size: 15px;
|
15
|
+
border-radius: 32px;
|
16
|
+
`
|
17
|
+
: css`
|
18
|
+
height: 140px;
|
19
|
+
width: 140px;
|
20
|
+
line-height: 140px;
|
21
|
+
font-size: 66px;
|
22
|
+
border-radius: 140px;
|
23
|
+
`}
|
24
|
+
display: inline-block;
|
25
|
+
color: ${getColor('white')};
|
26
|
+
text-align: center;
|
27
|
+
background-position: center;
|
28
|
+
background-repeat: no-repeat;
|
29
|
+
background-size: cover;
|
30
|
+
text-transform: uppercase;
|
31
|
+
cursor: ${({onClick}) => (onClick ? 'pointer' : 'default')};
|
32
|
+
`;
|
33
|
+
|
34
|
+
type AvatarProps = Override<
|
35
|
+
React.HTMLAttributes<HTMLSpanElement>,
|
36
|
+
{
|
37
|
+
/**
|
38
|
+
* Username to use as fallback if the avatar is not provided and the Firstname and Lastname are empty.
|
39
|
+
*/
|
40
|
+
username: string;
|
41
|
+
|
42
|
+
/**
|
43
|
+
* Firstname to use as fallback with the Lastname if the avatar is not provided.
|
44
|
+
*/
|
45
|
+
firstName: string;
|
46
|
+
|
47
|
+
/**
|
48
|
+
* Lastname to use as fallback with the Firstname if the avatar is not provided.
|
49
|
+
*/
|
50
|
+
lastName: string;
|
51
|
+
|
52
|
+
/**
|
53
|
+
* Url of the avatar image.
|
54
|
+
*/
|
55
|
+
avatarUrl?: string;
|
56
|
+
|
57
|
+
/**
|
58
|
+
* Size of the avatar.
|
59
|
+
*/
|
60
|
+
size?: 'default' | 'big';
|
61
|
+
}
|
62
|
+
>;
|
63
|
+
|
64
|
+
const Avatar = ({username, firstName, lastName, avatarUrl, size = 'default', ...rest}: AvatarProps) => {
|
65
|
+
const theme = useTheme();
|
66
|
+
|
67
|
+
const fallback = (
|
68
|
+
firstName.trim().charAt(0) + lastName.trim().charAt(0) || username.substring(0, 2)
|
69
|
+
).toLocaleUpperCase();
|
70
|
+
const title = `${firstName} ${lastName}`.trim() || username;
|
71
|
+
|
72
|
+
const backgroundColor = useMemo(() => {
|
73
|
+
const colorId = username.split('').reduce<number>((s, l) => s + l.charCodeAt(0), 0);
|
74
|
+
const colors = [
|
75
|
+
theme.colorAlternative.green120,
|
76
|
+
theme.colorAlternative.darkCyan120,
|
77
|
+
theme.colorAlternative.forestGreen120,
|
78
|
+
theme.colorAlternative.oliveGreen120,
|
79
|
+
theme.colorAlternative.blue120,
|
80
|
+
theme.colorAlternative.darkBlue120,
|
81
|
+
theme.colorAlternative.hotPink120,
|
82
|
+
theme.colorAlternative.red120,
|
83
|
+
theme.colorAlternative.coralRed120,
|
84
|
+
theme.colorAlternative.yellow120,
|
85
|
+
theme.colorAlternative.orange120,
|
86
|
+
theme.colorAlternative.chocolate120,
|
87
|
+
];
|
88
|
+
|
89
|
+
return colors[colorId % colors.length];
|
90
|
+
}, [theme, username]);
|
91
|
+
|
92
|
+
const style = avatarUrl ? {backgroundImage: `url(${avatarUrl})`} : {backgroundColor};
|
93
|
+
|
94
|
+
return (
|
95
|
+
<AvatarContainer size={size} {...rest} style={style} title={title}>
|
96
|
+
{avatarUrl ? '' : fallback}
|
97
|
+
</AvatarContainer>
|
98
|
+
);
|
99
|
+
};
|
100
|
+
|
101
|
+
export {Avatar};
|
102
|
+
export type {AvatarProps};
|
@@ -0,0 +1,79 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import {render, screen} from '../../storybook/test-util';
|
3
|
+
import {Avatar} from './Avatar';
|
4
|
+
|
5
|
+
test('renders', () => {
|
6
|
+
render(<Avatar username="john" firstName="John" lastName="Doe" />);
|
7
|
+
|
8
|
+
const avatar = screen.getByTitle('John Doe');
|
9
|
+
expect(avatar).toBeInTheDocument();
|
10
|
+
});
|
11
|
+
|
12
|
+
test('avatar image', () => {
|
13
|
+
render(<Avatar username="john" firstName="John" lastName="Doe" avatarUrl="path/to/image" />);
|
14
|
+
|
15
|
+
const avatar = screen.getByTitle('John Doe');
|
16
|
+
expect(avatar).toHaveStyle('background-image: url(path/to/image)');
|
17
|
+
});
|
18
|
+
|
19
|
+
test('deterministic fallback color', () => {
|
20
|
+
render(<Avatar username="john" firstName="John" lastName="Doe" />);
|
21
|
+
|
22
|
+
const avatar = screen.getByTitle('John Doe');
|
23
|
+
expect(avatar).toHaveStyle('background-color: rgb(68, 31, 0)');
|
24
|
+
});
|
25
|
+
|
26
|
+
test('fallback to firstname + lastname', () => {
|
27
|
+
render(<Avatar username="" firstName="John" lastName="Doe" />);
|
28
|
+
|
29
|
+
const avatar = screen.getByTitle('John Doe');
|
30
|
+
expect(avatar).toHaveTextContent('JD');
|
31
|
+
});
|
32
|
+
|
33
|
+
test('fallback to firstname only', () => {
|
34
|
+
render(<Avatar username="" firstName="John" lastName="" />);
|
35
|
+
|
36
|
+
const avatar = screen.getByTitle('John');
|
37
|
+
expect(avatar).toHaveTextContent('J');
|
38
|
+
});
|
39
|
+
|
40
|
+
test('fallback to lastname only', () => {
|
41
|
+
render(<Avatar username="" firstName="" lastName="Doe" />);
|
42
|
+
|
43
|
+
const avatar = screen.getByTitle('Doe');
|
44
|
+
expect(avatar).toHaveTextContent('D');
|
45
|
+
});
|
46
|
+
|
47
|
+
test('fallback to username', () => {
|
48
|
+
render(<Avatar username="john" firstName="" lastName="" />);
|
49
|
+
|
50
|
+
const avatar = screen.getByTitle('john');
|
51
|
+
expect(avatar).toHaveTextContent('JO');
|
52
|
+
});
|
53
|
+
|
54
|
+
test('initial are converted to uppercase', () => {
|
55
|
+
render(<Avatar username="" firstName="john" lastName="doe" />);
|
56
|
+
|
57
|
+
const avatar = screen.getByTitle('john doe');
|
58
|
+
expect(avatar).toHaveTextContent('JD');
|
59
|
+
});
|
60
|
+
|
61
|
+
test('size default', () => {
|
62
|
+
render(<Avatar username="john" firstName="John" lastName="Doe" />);
|
63
|
+
|
64
|
+
const avatar = screen.getByTitle('John Doe');
|
65
|
+
expect(avatar).toHaveStyle('width: 32px');
|
66
|
+
});
|
67
|
+
|
68
|
+
test('size big', () => {
|
69
|
+
render(<Avatar username="john" firstName="John" lastName="Doe" size="big" />);
|
70
|
+
|
71
|
+
const avatar = screen.getByTitle('John Doe');
|
72
|
+
expect(avatar).toHaveStyle('width: 140px');
|
73
|
+
});
|
74
|
+
|
75
|
+
test('supports ...rest props', () => {
|
76
|
+
render(<Avatar username="john" firstName="John" lastName="Doe" data-testid="my_value" />);
|
77
|
+
|
78
|
+
expect(screen.getByTestId('my_value')).toBeInTheDocument();
|
79
|
+
});
|
@@ -0,0 +1,50 @@
|
|
1
|
+
import React, {Children} from 'react';
|
2
|
+
import styled from 'styled-components';
|
3
|
+
import {Override} from '../../shared';
|
4
|
+
import {AkeneoThemedProps, getColor} from '../../theme';
|
5
|
+
|
6
|
+
const AvatarListContainer = styled.div<AvatarsProps & AkeneoThemedProps>`
|
7
|
+
display: flex;
|
8
|
+
flex-direction: row-reverse;
|
9
|
+
justify-content: flex-end;
|
10
|
+
& > * {
|
11
|
+
margin-right: -4px;
|
12
|
+
position: relative;
|
13
|
+
}
|
14
|
+
`;
|
15
|
+
|
16
|
+
const RemainingAvatar = styled.span`
|
17
|
+
height: 32px;
|
18
|
+
width: 32px;
|
19
|
+
display: inline-block;
|
20
|
+
border: 1px solid ${getColor('grey', 10)};
|
21
|
+
line-height: 32px;
|
22
|
+
text-align: center;
|
23
|
+
font-size: 15px;
|
24
|
+
border-radius: 32px;
|
25
|
+
background-color: ${getColor('white')};
|
26
|
+
`;
|
27
|
+
|
28
|
+
type AvatarsProps = Override<
|
29
|
+
React.HTMLAttributes<HTMLDivElement>,
|
30
|
+
{
|
31
|
+
max: number;
|
32
|
+
}
|
33
|
+
>;
|
34
|
+
|
35
|
+
const Avatars = ({max, children, ...rest}: AvatarsProps) => {
|
36
|
+
const childrenArray = Children.toArray(children);
|
37
|
+
const displayedChildren = childrenArray.slice(0, max);
|
38
|
+
const remainingChildrenCount = childrenArray.length - max;
|
39
|
+
const reverseChildren = displayedChildren.reverse();
|
40
|
+
|
41
|
+
return (
|
42
|
+
<AvatarListContainer {...rest}>
|
43
|
+
{remainingChildrenCount > 0 && <RemainingAvatar>+{remainingChildrenCount}</RemainingAvatar>}
|
44
|
+
{reverseChildren}
|
45
|
+
</AvatarListContainer>
|
46
|
+
);
|
47
|
+
};
|
48
|
+
|
49
|
+
export {Avatars};
|
50
|
+
export type {AvatarsProps};
|
@@ -0,0 +1,41 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import {render, screen} from '../../storybook/test-util';
|
3
|
+
import {Avatar} from './Avatar';
|
4
|
+
import {Avatars} from './Avatars';
|
5
|
+
|
6
|
+
test('renders multiple avatars', () => {
|
7
|
+
render(
|
8
|
+
<Avatars max={2}>
|
9
|
+
<Avatar username="john" firstName="John" lastName="Doe" />
|
10
|
+
<Avatar username="leonard" firstName="Leonard" lastName="Doe" />
|
11
|
+
</Avatars>
|
12
|
+
);
|
13
|
+
|
14
|
+
expect(screen.getByTitle('John Doe')).toBeInTheDocument();
|
15
|
+
expect(screen.getByTitle('Leonard Doe')).toBeInTheDocument();
|
16
|
+
});
|
17
|
+
|
18
|
+
test('renders a maximum number of avatars', () => {
|
19
|
+
render(
|
20
|
+
<Avatars max={1}>
|
21
|
+
<Avatar username="john" firstName="John" lastName="Doe" />
|
22
|
+
<Avatar username="leonard" firstName="Leonard" lastName="Doe" />
|
23
|
+
</Avatars>
|
24
|
+
);
|
25
|
+
|
26
|
+
expect(screen.getByTitle('John Doe')).toBeInTheDocument();
|
27
|
+
expect(screen.queryByTitle('Leonard Doe')).not.toBeInTheDocument();
|
28
|
+
expect(screen.getByText('+1')).toBeInTheDocument();
|
29
|
+
});
|
30
|
+
|
31
|
+
test('renders no avatar', () => {
|
32
|
+
render(<Avatars max={1}></Avatars>);
|
33
|
+
|
34
|
+
expect(screen.queryByTitle('+1')).not.toBeInTheDocument();
|
35
|
+
});
|
36
|
+
|
37
|
+
test('supports ...rest props', () => {
|
38
|
+
render(<Avatars max={1} data-testid="my_value" />);
|
39
|
+
|
40
|
+
expect(screen.getByTestId('my_value')).toBeInTheDocument();
|
41
|
+
});
|
@@ -149,7 +149,7 @@ type BooleanInputProps = Override<
|
|
149
149
|
clearLabel?: string;
|
150
150
|
}
|
151
151
|
) & {
|
152
|
-
readOnly
|
152
|
+
readOnly?: boolean;
|
153
153
|
yesLabel: string;
|
154
154
|
noLabel: string;
|
155
155
|
invalid?: boolean;
|
@@ -165,7 +165,7 @@ const BooleanInput = React.forwardRef<HTMLDivElement, BooleanInputProps>(
|
|
165
165
|
(
|
166
166
|
{
|
167
167
|
value,
|
168
|
-
readOnly,
|
168
|
+
readOnly = false,
|
169
169
|
onChange,
|
170
170
|
clearable = false,
|
171
171
|
yesLabel,
|
@@ -150,17 +150,24 @@ type MultiMultiSelectInputProps = Override<
|
|
150
150
|
* Callback called when the user hit enter on the field.
|
151
151
|
*/
|
152
152
|
onSubmit?: () => void;
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
153
|
+
} & (
|
154
|
+
| {
|
155
|
+
/**
|
156
|
+
* Handler called when the next page is almost reached.
|
157
|
+
*/
|
158
|
+
onNextPage?: () => void;
|
159
|
+
/**
|
160
|
+
* Handler called when the search value changed
|
161
|
+
*/
|
162
|
+
onSearchChange?: (searchValue: string) => void;
|
163
|
+
disableInternalSearch?: false;
|
164
|
+
}
|
165
|
+
| {
|
166
|
+
onNextPage: () => void;
|
167
|
+
onSearchChange: (searchValue: string) => void;
|
168
|
+
disableInternalSearch: true;
|
169
|
+
}
|
170
|
+
)
|
164
171
|
>;
|
165
172
|
|
166
173
|
/**
|
@@ -183,6 +190,7 @@ const MultiSelectInput = ({
|
|
183
190
|
verticalPosition,
|
184
191
|
onNextPage,
|
185
192
|
onSearchChange,
|
193
|
+
disableInternalSearch = false,
|
186
194
|
'aria-labelledby': ariaLabelledby,
|
187
195
|
...rest
|
188
196
|
}: MultiMultiSelectInputProps) => {
|
@@ -211,12 +219,14 @@ const MultiSelectInput = ({
|
|
211
219
|
return indexedChips;
|
212
220
|
}, {});
|
213
221
|
|
214
|
-
const filteredChildren =
|
215
|
-
|
216
|
-
|
222
|
+
const filteredChildren = disableInternalSearch
|
223
|
+
? validChildren
|
224
|
+
: validChildren.filter(({props}) => {
|
225
|
+
const childValue = props.value;
|
226
|
+
const optionValue = childValue + props.children;
|
217
227
|
|
218
|
-
|
219
|
-
|
228
|
+
return !value.includes(childValue) && optionValue.toLowerCase().includes(searchValue.toLowerCase());
|
229
|
+
});
|
220
230
|
|
221
231
|
const handleEnter = () => {
|
222
232
|
if (filteredChildren.length > 0 && dropdownIsOpen) {
|
@@ -77,6 +77,51 @@ test('it handles search', () => {
|
|
77
77
|
expect(onChange).toHaveBeenCalledTimes(2);
|
78
78
|
});
|
79
79
|
|
80
|
+
test('it handles external search', () => {
|
81
|
+
const onChange = jest.fn();
|
82
|
+
const onNextPage = jest.fn();
|
83
|
+
const onSearchChange = jest.fn();
|
84
|
+
|
85
|
+
const observe = jest.fn();
|
86
|
+
const unobserve = jest.fn();
|
87
|
+
window.IntersectionObserver = jest.fn(() => ({
|
88
|
+
observe,
|
89
|
+
unobserve,
|
90
|
+
})) as unknown as jest.Mock<IntersectionObserver>;
|
91
|
+
|
92
|
+
render(
|
93
|
+
<MultiSelectInput
|
94
|
+
openLabel="Open"
|
95
|
+
value={[]}
|
96
|
+
onChange={onChange}
|
97
|
+
placeholder="Placeholder"
|
98
|
+
emptyResultLabel="Empty result"
|
99
|
+
onNextPage={onNextPage}
|
100
|
+
onSearchChange={onSearchChange}
|
101
|
+
disableInternalSearch={true}
|
102
|
+
removeLabel="Remove"
|
103
|
+
>
|
104
|
+
<MultiSelectInput.Option value="en_US">English</MultiSelectInput.Option>
|
105
|
+
<MultiSelectInput.Option value="fr_FR">French</MultiSelectInput.Option>
|
106
|
+
<MultiSelectInput.Option value="de_DE">German</MultiSelectInput.Option>
|
107
|
+
<MultiSelectInput.Option value="es_ES">Spanish</MultiSelectInput.Option>
|
108
|
+
</MultiSelectInput>
|
109
|
+
);
|
110
|
+
|
111
|
+
const input = screen.getByRole('textbox');
|
112
|
+
fireEvent.click(input);
|
113
|
+
fireEvent.change(input, {target: {value: 'Fr'}});
|
114
|
+
|
115
|
+
const germanOption = screen.queryByText('German');
|
116
|
+
expect(germanOption).toBeInTheDocument();
|
117
|
+
const usOption = screen.queryByText('English');
|
118
|
+
expect(usOption).toBeInTheDocument();
|
119
|
+
const spanishOption = screen.queryByText('Spanish');
|
120
|
+
expect(spanishOption).toBeInTheDocument();
|
121
|
+
const frenchOption = screen.getByText('French');
|
122
|
+
expect(frenchOption).toBeInTheDocument();
|
123
|
+
});
|
124
|
+
|
80
125
|
test('it handles empty cases', () => {
|
81
126
|
const onChange = jest.fn();
|
82
127
|
render(
|
@@ -178,17 +178,24 @@ type SelectInputProps = Override<
|
|
178
178
|
* Force the vertical position of the overlay.
|
179
179
|
*/
|
180
180
|
verticalPosition?: VerticalPosition;
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
181
|
+
} & (
|
182
|
+
| {
|
183
|
+
/**
|
184
|
+
* Handler called when the next page is almost reached.
|
185
|
+
*/
|
186
|
+
onNextPage?: () => void;
|
187
|
+
/**
|
188
|
+
* Handler called when the search value changed
|
189
|
+
*/
|
190
|
+
onSearchChange?: (searchValue: string) => void;
|
191
|
+
disableInternalSearch?: false;
|
192
|
+
}
|
193
|
+
| {
|
194
|
+
onNextPage: () => void;
|
195
|
+
onSearchChange: (searchValue: string) => void;
|
196
|
+
disableInternalSearch: true;
|
197
|
+
}
|
198
|
+
)
|
192
199
|
>;
|
193
200
|
|
194
201
|
/**
|
@@ -209,6 +216,7 @@ const SelectInput = ({
|
|
209
216
|
verticalPosition,
|
210
217
|
onNextPage,
|
211
218
|
onSearchChange,
|
219
|
+
disableInternalSearch = false,
|
212
220
|
'aria-labelledby': ariaLabelledby,
|
213
221
|
...rest
|
214
222
|
}: SelectInputProps) => {
|
@@ -235,14 +243,16 @@ const SelectInput = ({
|
|
235
243
|
return optionCodes;
|
236
244
|
}, []);
|
237
245
|
|
238
|
-
const filteredChildren =
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
246
|
+
const filteredChildren = disableInternalSearch
|
247
|
+
? validChildren
|
248
|
+
: validChildren.filter(child => {
|
249
|
+
const content = typeof child.props.children === 'string' ? child.props.children : '';
|
250
|
+
const title = child.props.title ?? '';
|
251
|
+
const value = child.props.value;
|
252
|
+
const optionValue = value + content + title;
|
243
253
|
|
244
|
-
|
245
|
-
|
254
|
+
return optionValue.toLowerCase().includes(searchValue.toLowerCase());
|
255
|
+
});
|
246
256
|
|
247
257
|
const currentValueElement =
|
248
258
|
validChildren.find(child => {
|