Skip to content

feat(avatars): add status indicator component #1410

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Sep 22, 2022
Merged
16 changes: 8 additions & 8 deletions packages/avatars/.size-snapshot.json
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
{
"index.cjs.js": {
"bundled": 22094,
"minified": 15408,
"gzipped": 4317
"bundled": 28850,
"minified": 20381,
"gzipped": 5181
},
"index.esm.js": {
"bundled": 20471,
"minified": 14094,
"gzipped": 4094,
"bundled": 26890,
"minified": 18762,
"gzipped": 4979,
"treeshaked": {
"rollup": {
"code": 11861,
"code": 15337,
"import_statements": 322
},
"webpack": {
"code": 13425
"code": 17206
}
}
}
Expand Down
6 changes: 5 additions & 1 deletion packages/avatars/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ npm install react react-dom styled-components @zendeskgarden/react-theming

```jsx
import { ThemeProvider } from '@zendeskgarden/react-theming';
import { Avatar } from '@zendeskgarden/react-avatars';
import { Avatar, StatusIndicator } from '@zendeskgarden/react-avatars';

/**
* Place a `ThemeProvider` at the root of your React application
Expand All @@ -25,5 +25,9 @@ import { Avatar } from '@zendeskgarden/react-avatars';
<Avatar>
<img src="images/user.png" alt="Example Avatar" />
</Avatar>

<StatusIndicator type="available" aria-label="status: online">
Available
</StatusIndicator>
</ThemeProvider>;
```
26 changes: 26 additions & 0 deletions packages/avatars/demo/statusindicator.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Meta, ArgsTable, Canvas, Story } from '@storybook/addon-docs';
import { StatusIndicator } from '@zendeskgarden/react-avatars';

import { StatusIndicatorStory } from './stories/StatusIndicatorStory';

<Meta title="Packages/Avatars/StatusIndicator" component={StatusIndicator} />

# API

<ArgsTable />

# Demo

<Canvas>
<Story
name="StatusIndicator"
argTypes={{
children: { control: 'text' }
}}
args={{
'aria-label': 'Label'
}}
>
{args => <StatusIndicatorStory {...args} />}
</Story>
</Canvas>
14 changes: 14 additions & 0 deletions packages/avatars/demo/stories/StatusIndicatorStory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Copyright Zendesk, Inc.
*
* Use of this source code is governed under the Apache License, Version 2.0
* found at http://www.apache.org/licenses/LICENSE-2.0.
*/

import React from 'react';
import { Story } from '@storybook/react';
import { StatusIndicator, IStatusIndicatorProps } from '@zendeskgarden/react-avatars';

export const StatusIndicatorStory: Story<IStatusIndicatorProps> = ({ ...args }) => {
return <StatusIndicator {...args} />;
};
16 changes: 16 additions & 0 deletions packages/avatars/demo/~patterns/patterns.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Meta, Canvas, Story } from '@storybook/addon-docs';
import { Avatar } from '@zendeskgarden/react-avatars';
import { MenuStory } from './stories/MenuStory';
import { ChromeStory } from './stories/ChromeStory';
import { StatusMenuStory } from './stories/StatusMenuStory';

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

Expand Down Expand Up @@ -41,3 +42,18 @@ details.
{args => <MenuStory {...args} />}
</Story>
</Canvas>

## Status Menu

The following example demonstrates StatusIndicator being used in a menu.

<Canvas>
<Story
name="StatusMenu"
parameters={{ controls: { include: ['isCompact'] } }}
args={{ isCompact: false, type: 'available' }}
argTypes={{ isCompact: { control: 'boolean' } }}
>
{args => <StatusMenuStory {...args} />}
</Story>
</Canvas>
57 changes: 57 additions & 0 deletions packages/avatars/demo/~patterns/stories/StatusMenuStory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* Copyright Zendesk, Inc.
*
* Use of this source code is governed under the Apache License, Version 2.0
* found at http://www.apache.org/licenses/LICENSE-2.0.
*/

import React, { useState } from 'react';
import { Story } from '@storybook/react';
import { Col, Grid, Row } from '@zendeskgarden/react-grid';
import { Dropdown, Trigger, Menu, Item } from '@zendeskgarden/react-dropdowns';
import { Avatar, IStatusIndicatorProps, StatusIndicator } from '@zendeskgarden/react-avatars';

export const StatusMenuStory: Story = ({ isCompact }) => {
const [selectedType, setSelectedType] = useState<IStatusIndicatorProps['type']>('available');

return (
<Grid>
<Row style={{ height: 'calc(100vh - 80px)' }}>
<Col textAlign="center" alignSelf="center">
<Dropdown selectedItem={selectedType} onSelect={value => setSelectedType(value)}>
<Trigger>
<Avatar status={selectedType} size={isCompact ? 'small' : 'large'}>
<img alt="Example User" src="images/avatars/chrome.png" />
</Avatar>
</Trigger>
<Menu isCompact={isCompact}>
<Item value="available">
<StatusIndicator isCompact={isCompact} type="available">
Online
</StatusIndicator>
</Item>

<Item value="transfers">
<StatusIndicator isCompact={isCompact} type="transfers">
Transfers only
</StatusIndicator>
</Item>

<Item value="away">
<StatusIndicator isCompact={isCompact} type="away">
Away
</StatusIndicator>
</Item>

<Item value="offline">
<StatusIndicator isCompact={isCompact} type="offline">
Offline
</StatusIndicator>
</Item>
</Menu>
</Dropdown>
</Col>
</Row>
</Grid>
);
};
59 changes: 59 additions & 0 deletions packages/avatars/src/elements/StatusIndicator.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* Copyright Zendesk, Inc.
*
* Use of this source code is governed under the Apache License, Version 2.0
* found at http://www.apache.org/licenses/LICENSE-2.0.
*/

import React from 'react';
import { render, renderRtl, cleanup } from 'garden-test-utils';

import { StatusIndicator } from './StatusIndicator';
import { STATUS } from '../types';

describe('StatusIndicator', () => {
afterEach(cleanup);

it('passes ref to underlying DOM element', () => {
const ref = React.createRef<HTMLElement>();
const { container } = render(<StatusIndicator type="available" ref={ref} />);

expect(container.firstChild).toBe(ref.current);
});

it('has the correct roles', () => {
const { getByRole, container } = render(<StatusIndicator type="available" />);

expect(getByRole('status')).toBe(container.firstChild);
expect(getByRole('img')).toBe(container.firstChild?.firstChild);
});

it('renders with a caption', () => {
const text = 'caption';
const { getByText } = render(<StatusIndicator type="available">{text}</StatusIndicator>);

expect(getByText(text).nodeName).toBe('FIGCAPTION');
});

it('renders in compact mode', () => {
const { getByRole } = render(<StatusIndicator type="available" isCompact />);

expect(getByRole('img')).toHaveStyleRule('height', '8px');
});

it('renders in RTL mode', () => {
const { getByRole } = renderRtl(<StatusIndicator type="transfers" />);

expect(getByRole('img')).toHaveStyleRule('transform', 'scale(-1,1)', {
modifier: "& > svg[data-icon-status='transfers']"
});
});

describe('types', () => {
it.each(STATUS)('renders "$1" status type, and with aria label', type => {
const { getByRole } = render(<StatusIndicator type={type} />);

expect(getByRole('img')).toHaveAttribute('aria-label', `status: ${type}`);
});
});
});
70 changes: 70 additions & 0 deletions packages/avatars/src/elements/StatusIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* Copyright Zendesk, Inc.
*
* Use of this source code is governed under the Apache License, Version 2.0
* found at http://www.apache.org/licenses/LICENSE-2.0.
*/

import React, { forwardRef, useMemo } from 'react';
import PropTypes from 'prop-types';
import { useText } from '@zendeskgarden/react-theming';
import ClockIcon12 from '@zendeskgarden/svg-icons/src/12/clock-stroke.svg';
import ClockIcon16 from '@zendeskgarden/svg-icons/src/16/clock-stroke.svg';
import ArrowLeftIcon12 from '@zendeskgarden/svg-icons/src/12/arrow-left-sm-stroke.svg';
import ArrowLeftIcon16 from '@zendeskgarden/svg-icons/src/16/arrow-left-sm-stroke.svg';

import { IStatusIndicatorProps, STATUS } from '../types';
import {
StyledStandaloneStatusIndicator,
StyledStandaloneStatusCaption,
StyledStandaloneStatus
} from '../styled';

/**
* @extends HTMLAttributes<HTMLElement>
*/
const StatusIndicatorComponent = forwardRef<HTMLElement, IStatusIndicatorProps>(
({ children, type, isCompact, ...props }, ref) => {
let ClockIcon = ClockIcon16;
let ArrowLeftIcon = ArrowLeftIcon16;

if (isCompact) {
ClockIcon = ClockIcon12;
ArrowLeftIcon = ArrowLeftIcon12;
}

const defaultLabel = useMemo(() => ['status'].concat(type || []).join(': '), [type]);
const ariaLabel = useText(StatusIndicatorComponent, props, 'aria-label', defaultLabel);

return (
<StyledStandaloneStatus role="status" ref={ref} {...props}>
<StyledStandaloneStatusIndicator
role="img"
type={type}
isCompact={isCompact}
aria-label={ariaLabel}
>
{type === 'away' ? <ClockIcon data-icon-status={type} aria-hidden="true" /> : null}
{type === 'transfers' ? (
<ArrowLeftIcon data-icon-status={type} aria-hidden="true" />
) : null}
</StyledStandaloneStatusIndicator>
{children && <StyledStandaloneStatusCaption>{children}</StyledStandaloneStatusCaption>}
</StyledStandaloneStatus>
);
}
);

StatusIndicatorComponent.displayName = 'StatusIndicator';

StatusIndicatorComponent.propTypes = {
type: PropTypes.oneOf(STATUS),
isCompact: PropTypes.bool
};

StatusIndicatorComponent.defaultProps = {
type: 'offline',
isCompact: false
};

export const StatusIndicator = StatusIndicatorComponent;
3 changes: 2 additions & 1 deletion packages/avatars/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
*/

export { Avatar } from './elements/Avatar';
export type { IAvatarProps } from './types';
export { StatusIndicator } from './elements/StatusIndicator';
export type { IAvatarProps, IStatusIndicatorProps } from './types';
4 changes: 1 addition & 3 deletions packages/avatars/src/styled/StyledAvatar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,10 @@ import { math } from 'polished';
import { IAvatarProps, SIZE } from '../types';
import { StyledText } from './StyledText';
import { StyledStatusIndicator } from './StyledStatusIndicator';
import { getStatusColor } from './utility';
import { getStatusColor, TRANSITION_DURATION } from './utility';

const COMPONENT_ID = 'avatars.avatar';

const TRANSITION_DURATION = 0.25;

const badgeStyles = (props: IStyledAvatarProps & ThemeProps<DefaultTheme>) => {
const [xxs, xs, s, m, l] = SIZE;

Expand Down
26 changes: 26 additions & 0 deletions packages/avatars/src/styled/StyledStandaloneStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Copyright Zendesk, Inc.
*
* Use of this source code is governed under the Apache License, Version 2.0
* found at http://www.apache.org/licenses/LICENSE-2.0.
*/

import styled, { ThemeProps, DefaultTheme } from 'styled-components';
import { retrieveComponentStyles, DEFAULT_THEME } from '@zendeskgarden/react-theming';

const COMPONENT_ID = 'avatars.status-indicator.status';

export const StyledStandaloneStatus = styled.figure.attrs({
'data-garden-id': COMPONENT_ID,
'data-garden-version': PACKAGE_VERSION
})<ThemeProps<DefaultTheme>>`
display: inline-flex;
flex-flow: row nowrap;
margin: 0;

${props => retrieveComponentStyles(COMPONENT_ID, props)};
`;

StyledStandaloneStatus.defaultProps = {
theme: DEFAULT_THEME
};
33 changes: 33 additions & 0 deletions packages/avatars/src/styled/StyledStandaloneStatusCaption.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Copyright Zendesk, Inc.
*
* Use of this source code is governed under the Apache License, Version 2.0
* found at http://www.apache.org/licenses/LICENSE-2.0.
*/

import styled, { ThemeProps, DefaultTheme, css } from 'styled-components';
import { retrieveComponentStyles, DEFAULT_THEME } from '@zendeskgarden/react-theming';

const COMPONENT_ID = 'avatars.status-indicator.caption';

function sizeStyles(props: ThemeProps<DefaultTheme>) {
return css`
padding: 0 ${props.theme.space.base}px;
line-height: ${props.theme.space.base * 6}px;
font-size: ${props.theme.fontSizes.md};
font-weight: ${props.theme.fontWeights.regular};
`;
}

export const StyledStandaloneStatusCaption = styled.figcaption.attrs({
'data-garden-id': COMPONENT_ID,
'data-garden-version': PACKAGE_VERSION
})<ThemeProps<DefaultTheme>>`
${sizeStyles}

${props => retrieveComponentStyles(COMPONENT_ID, props)};
`;

StyledStandaloneStatusCaption.defaultProps = {
theme: DEFAULT_THEME
};
Loading