Skip to content

Commit 1c21775

Browse files
feat(avatars): add status indicator component (#1410)
1 parent 4f321b9 commit 1c21775

20 files changed

+566
-113
lines changed

packages/avatars/.size-snapshot.json

+9-9
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
{
22
"index.cjs.js": {
3-
"bundled": 22094,
4-
"minified": 15408,
5-
"gzipped": 4317
3+
"bundled": 26216,
4+
"minified": 18795,
5+
"gzipped": 4878
66
},
77
"index.esm.js": {
8-
"bundled": 20471,
9-
"minified": 14094,
10-
"gzipped": 4094,
8+
"bundled": 24250,
9+
"minified": 17172,
10+
"gzipped": 4660,
1111
"treeshaked": {
1212
"rollup": {
13-
"code": 11861,
14-
"import_statements": 322
13+
"code": 14087,
14+
"import_statements": 341
1515
},
1616
"webpack": {
17-
"code": 13425
17+
"code": 15949
1818
}
1919
}
2020
}

packages/avatars/README.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ npm install react react-dom styled-components @zendeskgarden/react-theming
1616

1717
```jsx
1818
import { ThemeProvider } from '@zendeskgarden/react-theming';
19-
import { Avatar } from '@zendeskgarden/react-avatars';
19+
import { Avatar, StatusIndicator } from '@zendeskgarden/react-avatars';
2020

2121
/**
2222
* Place a `ThemeProvider` at the root of your React application
@@ -25,5 +25,9 @@ import { Avatar } from '@zendeskgarden/react-avatars';
2525
<Avatar>
2626
<img src="images/user.png" alt="Example Avatar" />
2727
</Avatar>
28+
29+
<StatusIndicator type="available" aria-label="status: online">
30+
Available
31+
</StatusIndicator>
2832
</ThemeProvider>;
2933
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Meta, ArgsTable, Canvas, Story } from '@storybook/addon-docs';
2+
import { StatusIndicator } from '@zendeskgarden/react-avatars';
3+
4+
import { StatusIndicatorStory } from './stories/StatusIndicatorStory';
5+
6+
<Meta title="Packages/Avatars/StatusIndicator" component={StatusIndicator} />
7+
8+
# API
9+
10+
<ArgsTable />
11+
12+
# Demo
13+
14+
<Canvas>
15+
<Story
16+
name="StatusIndicator"
17+
argTypes={{
18+
children: { control: 'text' }
19+
}}
20+
args={{
21+
'aria-label': 'Label',
22+
children: 'Status'
23+
}}
24+
parameters={{
25+
design: {
26+
allowFullscreen: true,
27+
type: 'figma',
28+
url: 'https://www.figma.com/file/6g87L4FdKZTA3knt3Rsfdx/Garden?node-id=10927%3A45275'
29+
}
30+
}}
31+
>
32+
{args => <StatusIndicatorStory {...args} />}
33+
</Story>
34+
</Canvas>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* Copyright Zendesk, Inc.
3+
*
4+
* Use of this source code is governed under the Apache License, Version 2.0
5+
* found at http://www.apache.org/licenses/LICENSE-2.0.
6+
*/
7+
8+
import React from 'react';
9+
import { Story } from '@storybook/react';
10+
import { StatusIndicator, IStatusIndicatorProps } from '@zendeskgarden/react-avatars';
11+
12+
export const StatusIndicatorStory: Story<IStatusIndicatorProps> = ({ ...args }) => {
13+
return <StatusIndicator {...args} />;
14+
};

packages/avatars/demo/~patterns/patterns.stories.mdx

+16
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Meta, Canvas, Story } from '@storybook/addon-docs';
22
import { Avatar } from '@zendeskgarden/react-avatars';
33
import { MenuStory } from './stories/MenuStory';
44
import { ChromeStory } from './stories/ChromeStory';
5+
import { StatusMenuStory } from './stories/StatusMenuStory';
56

67
<Meta title="Packages/Avatars/[patterns]" component={Avatar} />
78

@@ -41,3 +42,18 @@ details.
4142
{args => <MenuStory {...args} />}
4243
</Story>
4344
</Canvas>
45+
46+
## Status Menu
47+
48+
The following example demonstrates StatusIndicator being used in a menu.
49+
50+
<Canvas>
51+
<Story
52+
name="StatusMenu"
53+
parameters={{ controls: { include: ['isCompact'] } }}
54+
args={{ isCompact: false, type: 'available' }}
55+
argTypes={{ isCompact: { control: 'boolean' } }}
56+
>
57+
{args => <StatusMenuStory {...args} />}
58+
</Story>
59+
</Canvas>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* Copyright Zendesk, Inc.
3+
*
4+
* Use of this source code is governed under the Apache License, Version 2.0
5+
* found at http://www.apache.org/licenses/LICENSE-2.0.
6+
*/
7+
8+
import React, { useState } from 'react';
9+
import { Story } from '@storybook/react';
10+
import { Col, Grid, Row } from '@zendeskgarden/react-grid';
11+
import { Dropdown, Trigger, Menu, Item } from '@zendeskgarden/react-dropdowns';
12+
import { Avatar, IStatusIndicatorProps, StatusIndicator } from '@zendeskgarden/react-avatars';
13+
14+
export const StatusMenuStory: Story = ({ isCompact }) => {
15+
const [selectedType, setSelectedType] = useState<IStatusIndicatorProps['type']>('available');
16+
17+
return (
18+
<Grid>
19+
<Row style={{ height: 'calc(100vh - 80px)' }}>
20+
<Col textAlign="center" alignSelf="center">
21+
<Dropdown selectedItem={selectedType} onSelect={value => setSelectedType(value)}>
22+
<Trigger>
23+
<Avatar status={selectedType} size={isCompact ? 'small' : 'medium'}>
24+
<img alt="Example User" src="images/avatars/chrome.png" />
25+
</Avatar>
26+
</Trigger>
27+
<Menu isCompact={isCompact}>
28+
<Item value="available">
29+
<StatusIndicator isCompact={isCompact} type="available">
30+
Online
31+
</StatusIndicator>
32+
</Item>
33+
34+
<Item value="transfers">
35+
<StatusIndicator isCompact={isCompact} type="transfers">
36+
Transfers only
37+
</StatusIndicator>
38+
</Item>
39+
40+
<Item value="away">
41+
<StatusIndicator isCompact={isCompact} type="away">
42+
Away
43+
</StatusIndicator>
44+
</Item>
45+
46+
<Item value="offline">
47+
<StatusIndicator isCompact={isCompact} type="offline">
48+
Offline
49+
</StatusIndicator>
50+
</Item>
51+
</Menu>
52+
</Dropdown>
53+
</Col>
54+
</Row>
55+
</Grid>
56+
);
57+
};

packages/avatars/src/elements/Avatar.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ const AvatarComponent = forwardRef<HTMLElement, IAvatarProps>(
7676
{computedStatus && (
7777
<StyledStatusIndicator
7878
size={size}
79-
status={computedStatus}
79+
type={computedStatus}
8080
backgroundColor={backgroundColor}
8181
foregroundColor={foregroundColor}
8282
surfaceColor={surfaceColor}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* Copyright Zendesk, Inc.
3+
*
4+
* Use of this source code is governed under the Apache License, Version 2.0
5+
* found at http://www.apache.org/licenses/LICENSE-2.0.
6+
*/
7+
8+
import React from 'react';
9+
import { render, renderRtl, cleanup } from 'garden-test-utils';
10+
11+
import { StatusIndicator } from './StatusIndicator';
12+
import { STATUS } from '../types';
13+
14+
describe('StatusIndicator', () => {
15+
afterEach(cleanup);
16+
17+
it('passes ref to underlying DOM element', () => {
18+
const ref = React.createRef<HTMLElement>();
19+
const { container } = render(<StatusIndicator type="available" ref={ref} />);
20+
21+
expect(container.firstChild).toBe(ref.current);
22+
});
23+
24+
it('has the correct roles', () => {
25+
const { getByRole, container } = render(<StatusIndicator type="available" />);
26+
27+
expect(getByRole('status')).toBe(container.firstChild);
28+
expect(getByRole('img')).toBe(container.firstChild?.firstChild);
29+
});
30+
31+
it('renders with a caption', () => {
32+
const text = 'caption';
33+
const { getByText } = render(<StatusIndicator type="available">{text}</StatusIndicator>);
34+
35+
expect(getByText(text).nodeName).toBe('FIGCAPTION');
36+
});
37+
38+
it('renders in compact mode', () => {
39+
const { getByRole } = render(<StatusIndicator type="available" isCompact />);
40+
41+
expect(getByRole('img')).toHaveStyleRule('height', '8px');
42+
});
43+
44+
it('renders in RTL mode', () => {
45+
const { getByRole } = renderRtl(<StatusIndicator type="transfers">Caption</StatusIndicator>);
46+
47+
expect(getByRole('img')).toHaveStyleRule('transform', 'scale(-1,1)', {
48+
modifier: "& > svg[data-icon-status='transfers']"
49+
});
50+
});
51+
52+
describe('types', () => {
53+
it.each(STATUS)('renders "$1" status type, and with aria label', type => {
54+
const { getByRole } = render(<StatusIndicator type={type} />);
55+
56+
expect(getByRole('img')).toHaveAttribute('aria-label', `status: ${type}`);
57+
});
58+
});
59+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* Copyright Zendesk, Inc.
3+
*
4+
* Use of this source code is governed under the Apache License, Version 2.0
5+
* found at http://www.apache.org/licenses/LICENSE-2.0.
6+
*/
7+
8+
import React, { forwardRef, useMemo } from 'react';
9+
import PropTypes from 'prop-types';
10+
import { useText } from '@zendeskgarden/react-theming';
11+
import ClockIcon12 from '@zendeskgarden/svg-icons/src/12/clock-stroke.svg';
12+
import ClockIcon16 from '@zendeskgarden/svg-icons/src/16/clock-stroke.svg';
13+
import ArrowLeftIcon12 from '@zendeskgarden/svg-icons/src/12/arrow-left-sm-stroke.svg';
14+
import ArrowLeftIcon16 from '@zendeskgarden/svg-icons/src/16/arrow-left-sm-stroke.svg';
15+
16+
import { IStatusIndicatorProps, STATUS } from '../types';
17+
import {
18+
StyledStandaloneStatusIndicator,
19+
StyledStandaloneStatusCaption,
20+
StyledStandaloneStatus
21+
} from '../styled';
22+
23+
/**
24+
* @extends HTMLAttributes<HTMLElement>
25+
*/
26+
const StatusIndicatorComponent = forwardRef<HTMLElement, IStatusIndicatorProps>(
27+
({ children, type, isCompact, 'aria-label': label, ...props }, ref) => {
28+
let ClockIcon = ClockIcon16;
29+
let ArrowLeftIcon = ArrowLeftIcon16;
30+
31+
if (isCompact) {
32+
ClockIcon = ClockIcon12;
33+
ArrowLeftIcon = ArrowLeftIcon12;
34+
}
35+
36+
const defaultLabel = useMemo(() => ['status'].concat(type || []).join(': '), [type]);
37+
const ariaLabel = useText(
38+
StatusIndicatorComponent,
39+
{ 'aria-label': label },
40+
'aria-label',
41+
defaultLabel
42+
);
43+
44+
return (
45+
<StyledStandaloneStatus role="status" ref={ref} {...props}>
46+
<StyledStandaloneStatusIndicator
47+
role="img"
48+
type={type}
49+
size={isCompact ? 'small' : 'medium'}
50+
aria-label={ariaLabel}
51+
>
52+
{type === 'away' ? <ClockIcon data-icon-status={type} aria-hidden="true" /> : null}
53+
{type === 'transfers' ? (
54+
<ArrowLeftIcon data-icon-status={type} aria-hidden="true" />
55+
) : null}
56+
</StyledStandaloneStatusIndicator>
57+
{children && <StyledStandaloneStatusCaption>{children}</StyledStandaloneStatusCaption>}
58+
</StyledStandaloneStatus>
59+
);
60+
}
61+
);
62+
63+
StatusIndicatorComponent.displayName = 'StatusIndicator';
64+
65+
StatusIndicatorComponent.propTypes = {
66+
type: PropTypes.oneOf(STATUS),
67+
isCompact: PropTypes.bool
68+
};
69+
70+
StatusIndicatorComponent.defaultProps = {
71+
type: 'offline'
72+
};
73+
74+
export const StatusIndicator = StatusIndicatorComponent;

packages/avatars/src/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@
66
*/
77

88
export { Avatar } from './elements/Avatar';
9-
export type { IAvatarProps } from './types';
9+
export { StatusIndicator } from './elements/StatusIndicator';
10+
export type { IAvatarProps, IStatusIndicatorProps } from './types';

packages/avatars/src/styled/StyledAvatar.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,10 @@ import { math } from 'polished';
1212
import { IAvatarProps, SIZE } from '../types';
1313
import { StyledText } from './StyledText';
1414
import { StyledStatusIndicator } from './StyledStatusIndicator';
15-
import { getStatusColor } from './utility';
15+
import { getStatusColor, TRANSITION_DURATION } from './utility';
1616

1717
const COMPONENT_ID = 'avatars.avatar';
1818

19-
const TRANSITION_DURATION = 0.25;
20-
2119
const badgeStyles = (props: IStyledAvatarProps & ThemeProps<DefaultTheme>) => {
2220
const [xxs, xs, s, m, l] = SIZE;
2321

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Copyright Zendesk, Inc.
3+
*
4+
* Use of this source code is governed under the Apache License, Version 2.0
5+
* found at http://www.apache.org/licenses/LICENSE-2.0.
6+
*/
7+
8+
import styled, { ThemeProps, DefaultTheme } from 'styled-components';
9+
import { retrieveComponentStyles, DEFAULT_THEME } from '@zendeskgarden/react-theming';
10+
11+
import { TRANSITION_DURATION } from './utility';
12+
13+
const COMPONENT_ID = 'avatars.status-indicator.status';
14+
15+
export const StyledStandaloneStatus = styled.figure.attrs({
16+
'data-garden-id': COMPONENT_ID,
17+
'data-garden-version': PACKAGE_VERSION
18+
})<ThemeProps<DefaultTheme>>`
19+
display: inline-flex;
20+
flex-flow: row nowrap;
21+
transition: all ${TRANSITION_DURATION}s ease-in-out;
22+
margin: 0;
23+
box-sizing: content-box;
24+
25+
${props => retrieveComponentStyles(COMPONENT_ID, props)};
26+
`;
27+
28+
StyledStandaloneStatus.defaultProps = {
29+
theme: DEFAULT_THEME
30+
};

0 commit comments

Comments
 (0)