diff --git a/packages/avatars/.size-snapshot.json b/packages/avatars/.size-snapshot.json
index 1373447f095..3763dda6b77 100644
--- a/packages/avatars/.size-snapshot.json
+++ b/packages/avatars/.size-snapshot.json
@@ -1,20 +1,20 @@
{
"index.cjs.js": {
- "bundled": 22094,
- "minified": 15408,
- "gzipped": 4317
+ "bundled": 26216,
+ "minified": 18795,
+ "gzipped": 4878
},
"index.esm.js": {
- "bundled": 20471,
- "minified": 14094,
- "gzipped": 4094,
+ "bundled": 24250,
+ "minified": 17172,
+ "gzipped": 4660,
"treeshaked": {
"rollup": {
- "code": 11861,
- "import_statements": 322
+ "code": 14087,
+ "import_statements": 341
},
"webpack": {
- "code": 13425
+ "code": 15949
}
}
}
diff --git a/packages/avatars/README.md b/packages/avatars/README.md
index db644cd2607..0f681118c2d 100644
--- a/packages/avatars/README.md
+++ b/packages/avatars/README.md
@@ -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
@@ -25,5 +25,9 @@ import { Avatar } from '@zendeskgarden/react-avatars';
+
+
+ Available
+
;
```
diff --git a/packages/avatars/demo/statusindicator.stories.mdx b/packages/avatars/demo/statusindicator.stories.mdx
new file mode 100644
index 00000000000..50a1a93c82a
--- /dev/null
+++ b/packages/avatars/demo/statusindicator.stories.mdx
@@ -0,0 +1,34 @@
+import { Meta, ArgsTable, Canvas, Story } from '@storybook/addon-docs';
+import { StatusIndicator } from '@zendeskgarden/react-avatars';
+
+import { StatusIndicatorStory } from './stories/StatusIndicatorStory';
+
+
+
+# API
+
+
+
+# Demo
+
+
diff --git a/packages/avatars/demo/stories/StatusIndicatorStory.tsx b/packages/avatars/demo/stories/StatusIndicatorStory.tsx
new file mode 100644
index 00000000000..aedca55548e
--- /dev/null
+++ b/packages/avatars/demo/stories/StatusIndicatorStory.tsx
@@ -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 = ({ ...args }) => {
+ return ;
+};
diff --git a/packages/avatars/demo/~patterns/patterns.stories.mdx b/packages/avatars/demo/~patterns/patterns.stories.mdx
index fb1eb2294b3..2fc4687eac8 100644
--- a/packages/avatars/demo/~patterns/patterns.stories.mdx
+++ b/packages/avatars/demo/~patterns/patterns.stories.mdx
@@ -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';
@@ -41,3 +42,18 @@ details.
{args => }
+
+## Status Menu
+
+The following example demonstrates StatusIndicator being used in a menu.
+
+
diff --git a/packages/avatars/demo/~patterns/stories/StatusMenuStory.tsx b/packages/avatars/demo/~patterns/stories/StatusMenuStory.tsx
new file mode 100644
index 00000000000..a9e21e58e92
--- /dev/null
+++ b/packages/avatars/demo/~patterns/stories/StatusMenuStory.tsx
@@ -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('available');
+
+ return (
+
+
+
+ setSelectedType(value)}>
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/avatars/src/elements/Avatar.tsx b/packages/avatars/src/elements/Avatar.tsx
index c984bd99f66..eaab1805544 100644
--- a/packages/avatars/src/elements/Avatar.tsx
+++ b/packages/avatars/src/elements/Avatar.tsx
@@ -76,7 +76,7 @@ const AvatarComponent = forwardRef(
{computedStatus && (
{
+ afterEach(cleanup);
+
+ it('passes ref to underlying DOM element', () => {
+ const ref = React.createRef();
+ const { container } = render();
+
+ expect(container.firstChild).toBe(ref.current);
+ });
+
+ it('has the correct roles', () => {
+ const { getByRole, container } = render();
+
+ expect(getByRole('status')).toBe(container.firstChild);
+ expect(getByRole('img')).toBe(container.firstChild?.firstChild);
+ });
+
+ it('renders with a caption', () => {
+ const text = 'caption';
+ const { getByText } = render({text});
+
+ expect(getByText(text).nodeName).toBe('FIGCAPTION');
+ });
+
+ it('renders in compact mode', () => {
+ const { getByRole } = render();
+
+ expect(getByRole('img')).toHaveStyleRule('height', '8px');
+ });
+
+ it('renders in RTL mode', () => {
+ const { getByRole } = renderRtl(Caption);
+
+ 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();
+
+ expect(getByRole('img')).toHaveAttribute('aria-label', `status: ${type}`);
+ });
+ });
+});
diff --git a/packages/avatars/src/elements/StatusIndicator.tsx b/packages/avatars/src/elements/StatusIndicator.tsx
new file mode 100644
index 00000000000..c0c4ee58e96
--- /dev/null
+++ b/packages/avatars/src/elements/StatusIndicator.tsx
@@ -0,0 +1,74 @@
+/**
+ * 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
+ */
+const StatusIndicatorComponent = forwardRef(
+ ({ children, type, isCompact, 'aria-label': label, ...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,
+ { 'aria-label': label },
+ 'aria-label',
+ defaultLabel
+ );
+
+ return (
+
+
+ {type === 'away' ? : null}
+ {type === 'transfers' ? (
+
+ ) : null}
+
+ {children && {children}}
+
+ );
+ }
+);
+
+StatusIndicatorComponent.displayName = 'StatusIndicator';
+
+StatusIndicatorComponent.propTypes = {
+ type: PropTypes.oneOf(STATUS),
+ isCompact: PropTypes.bool
+};
+
+StatusIndicatorComponent.defaultProps = {
+ type: 'offline'
+};
+
+export const StatusIndicator = StatusIndicatorComponent;
diff --git a/packages/avatars/src/index.ts b/packages/avatars/src/index.ts
index af25f995fce..f412404d49d 100644
--- a/packages/avatars/src/index.ts
+++ b/packages/avatars/src/index.ts
@@ -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';
diff --git a/packages/avatars/src/styled/StyledAvatar.ts b/packages/avatars/src/styled/StyledAvatar.ts
index 2c36eb612b9..687decab5c1 100644
--- a/packages/avatars/src/styled/StyledAvatar.ts
+++ b/packages/avatars/src/styled/StyledAvatar.ts
@@ -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) => {
const [xxs, xs, s, m, l] = SIZE;
diff --git a/packages/avatars/src/styled/StyledStandaloneStatus.ts b/packages/avatars/src/styled/StyledStandaloneStatus.ts
new file mode 100644
index 00000000000..d6424d9ab5b
--- /dev/null
+++ b/packages/avatars/src/styled/StyledStandaloneStatus.ts
@@ -0,0 +1,30 @@
+/**
+ * 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';
+
+import { TRANSITION_DURATION } from './utility';
+
+const COMPONENT_ID = 'avatars.status-indicator.status';
+
+export const StyledStandaloneStatus = styled.figure.attrs({
+ 'data-garden-id': COMPONENT_ID,
+ 'data-garden-version': PACKAGE_VERSION
+})>`
+ display: inline-flex;
+ flex-flow: row nowrap;
+ transition: all ${TRANSITION_DURATION}s ease-in-out;
+ margin: 0;
+ box-sizing: content-box;
+
+ ${props => retrieveComponentStyles(COMPONENT_ID, props)};
+`;
+
+StyledStandaloneStatus.defaultProps = {
+ theme: DEFAULT_THEME
+};
diff --git a/packages/avatars/src/styled/StyledStandaloneStatusCaption.ts b/packages/avatars/src/styled/StyledStandaloneStatusCaption.ts
new file mode 100644
index 00000000000..88a113dac51
--- /dev/null
+++ b/packages/avatars/src/styled/StyledStandaloneStatusCaption.ts
@@ -0,0 +1,40 @@
+/**
+ * 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 {
+ DEFAULT_THEME,
+ getLineHeight,
+ retrieveComponentStyles
+} from '@zendeskgarden/react-theming';
+
+const COMPONENT_ID = 'avatars.status-indicator.caption';
+
+function sizeStyles(props: ThemeProps) {
+ const marginRule = `margin-${props.theme.rtl ? 'right' : 'left'}: ${
+ props.theme.space.base * 2
+ }px;`;
+
+ return css`
+ ${marginRule}
+ line-height: ${getLineHeight(props.theme.lineHeights.md, props.theme.fontSizes.md)};
+ font-size: ${props.theme.fontSizes.md};
+ `;
+}
+
+export const StyledStandaloneStatusCaption = styled.figcaption.attrs({
+ 'data-garden-id': COMPONENT_ID,
+ 'data-garden-version': PACKAGE_VERSION
+})>`
+ ${sizeStyles}
+
+ ${props => retrieveComponentStyles(COMPONENT_ID, props)};
+`;
+
+StyledStandaloneStatusCaption.defaultProps = {
+ theme: DEFAULT_THEME
+};
diff --git a/packages/avatars/src/styled/StyledStandaloneStatusIndicator.ts b/packages/avatars/src/styled/StyledStandaloneStatusIndicator.ts
new file mode 100644
index 00000000000..0916fefba59
--- /dev/null
+++ b/packages/avatars/src/styled/StyledStandaloneStatusIndicator.ts
@@ -0,0 +1,30 @@
+/**
+ * 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 from 'styled-components';
+import { retrieveComponentStyles, DEFAULT_THEME } from '@zendeskgarden/react-theming';
+
+import { getStatusSize, IStyledStatusIndicatorProps } from './utility';
+import { StyledStatusIndicatorBase } from './StyledStatusIndicatorBase';
+
+const COMPONENT_ID = 'avatars.status-indicator.indicator';
+
+export const StyledStandaloneStatusIndicator = styled(StyledStatusIndicatorBase).attrs({
+ 'data-garden-id': COMPONENT_ID,
+ 'data-garden-version': PACKAGE_VERSION
+})`
+ position: relative;
+ margin-top: ${props =>
+ `calc((${props.theme.lineHeights.md} - ${getStatusSize(props, '0')}) / 2)`};
+
+ ${props => retrieveComponentStyles(COMPONENT_ID, props)};
+`;
+
+StyledStandaloneStatusIndicator.defaultProps = {
+ type: 'offline',
+ theme: DEFAULT_THEME
+};
diff --git a/packages/avatars/src/styled/StyledStatusIndicator.spec.tsx b/packages/avatars/src/styled/StyledStatusIndicator.spec.tsx
index e4897f24d08..5f0b9ff0daa 100644
--- a/packages/avatars/src/styled/StyledStatusIndicator.spec.tsx
+++ b/packages/avatars/src/styled/StyledStatusIndicator.spec.tsx
@@ -96,7 +96,7 @@ describe('StyledStatusIndicator', () => {
});
});
- describe('status', () => {
+ describe('type', () => {
it('renders default', () => {
const { container } = render();
@@ -105,7 +105,7 @@ describe('StyledStatusIndicator', () => {
describe('away', () => {
it('renders away style', () => {
- const { container } = render();
+ const { container } = render();
const color = getColor('orange', 400);
expect(container.firstChild).toHaveStyleRule('background-color', color);
@@ -114,7 +114,7 @@ describe('StyledStatusIndicator', () => {
describe('transfers', () => {
it('renders transfers style', () => {
- const { container } = render();
+ const { container } = render();
const color = getColor('azure', 400);
expect(container.firstChild).toHaveStyleRule('background-color', color);
@@ -123,16 +123,25 @@ describe('StyledStatusIndicator', () => {
describe('active', () => {
it('renders active style', () => {
- const { container } = render();
+ const { container } = render();
const color = getColor('crimson', 400);
+ expect(container.firstChild).toHaveStyleRule('height', '16px');
+ expect(container.firstChild).toHaveStyleRule('background-color', color);
+ });
+
+ it('renders active style with small size', () => {
+ const { container } = render();
+ const color = getColor('crimson', 400);
+
+ expect(container.firstChild).toHaveStyleRule('height', '12px');
expect(container.firstChild).toHaveStyleRule('background-color', color);
});
});
describe('available', () => {
it('renders available style', () => {
- const { container } = render();
+ const { container } = render();
const color = getColor('mint', 400);
expect(container.firstChild).toHaveStyleRule('background-color', color);
@@ -141,10 +150,10 @@ describe('StyledStatusIndicator', () => {
describe('offline', () => {
it('renders offline style', () => {
- const { container } = render();
+ const { container } = render();
const color = getColor('grey', 500);
- expect(container.firstChild).toHaveStyleRule('border-color', color);
+ expect(container.firstChild).toHaveStyleRule('border-color', `${color}`);
});
});
});
diff --git a/packages/avatars/src/styled/StyledStatusIndicator.ts b/packages/avatars/src/styled/StyledStatusIndicator.ts
index bf2d6eb1f0c..f32fc0c5786 100644
--- a/packages/avatars/src/styled/StyledStatusIndicator.ts
+++ b/packages/avatars/src/styled/StyledStatusIndicator.ts
@@ -9,65 +9,37 @@ import styled, { css, ThemeProps, DefaultTheme } from 'styled-components';
import { retrieveComponentStyles, DEFAULT_THEME } from '@zendeskgarden/react-theming';
import { math } from 'polished';
-import { IAvatarProps, SIZE, STATUS } from '../types';
-import { getStatusColor } from './utility';
+import { IAvatarProps, SIZE } from '../types';
+import { StyledStatusIndicatorBase } from './StyledStatusIndicatorBase';
+import { getStatusBorderOffset, includes, IStyledStatusIndicatorProps } from './utility';
export interface IStatusIndicatorProps extends Omit {
- readonly status?: IAvatarProps['status'] | 'active';
+ readonly type?: IStyledStatusIndicatorProps['type'];
borderColor?: string;
}
const COMPONENT_ID = 'avatars.status_indicator';
const [xxs, xs, s, m, l] = SIZE;
-const [active, available, away, transfers, offline] = ['active', ...STATUS];
const sizeStyles = (props: IStatusIndicatorProps & ThemeProps) => {
- const isActive = props.status === 'active';
+ const isVisible = !includes([xxs, xs], props.size);
+ const borderWidth = getStatusBorderOffset(props);
- let isVisible = true;
- let height = '0';
let padding = '0';
- let borderWidth = props.theme.shadowWidths.sm;
-
- switch (props.size) {
- case xxs:
- isVisible = false;
- borderWidth = math(`${borderWidth} - 1`);
- height = math(`${props.theme.space.base}px - ${borderWidth}`);
- break;
- case xs:
- isVisible = false;
- height = math(`${props.theme.space.base * 2}px - (${borderWidth} * 2)`);
- break;
- case s:
- height = math(`${props.theme.space.base * 3}px ${isActive ? '' : `- (${borderWidth} * 2)`}`);
- padding = math(`${props.theme.space.base + 1}px - (${borderWidth} * 2)`);
- break;
- case m:
- case l:
- height = math(`${props.theme.space.base * 4}px ${isActive ? '' : `- (${borderWidth} * 2)`}`);
- padding = math(`${props.theme.space.base + 3}px - (${borderWidth} * 2)`);
- break;
+
+ if (props.size === s) {
+ padding = math(`${props.theme.space.base + 1}px - (${borderWidth} * 2)`);
+ } else if (includes([m, l], props.size)) {
+ padding = math(`${props.theme.space.base + 3}px - (${borderWidth} * 2)`);
}
- /**
- * 1. because we are using the stroke icon instead of fill due to artifacts in visual appearance,
- * we need to remove the circle
- * 2. when @zendeskgarden/css-bedrock is present, max-height needs to be unset due to icon being
- * resized incorrectly
- */
return css`
- border: ${borderWidth} ${props.theme.borderStyles.solid};
- border-radius: ${height};
- min-width: ${height};
max-width: calc(2em + (${borderWidth} * 3));
- height: ${height};
box-sizing: content-box;
overflow: hidden;
text-align: center;
text-overflow: ellipsis;
- line-height: ${height};
white-space: nowrap;
font-size: ${props.theme.fontSizes.xs};
font-weight: ${props.theme.fontWeights.semibold};
@@ -84,50 +56,19 @@ const sizeStyles = (props: IStatusIndicatorProps & ThemeProps) =>
& > svg {
${!isVisible && 'display: none;'}
- position: absolute;
- top: -${borderWidth};
- left: -${borderWidth};
- transform-origin: 50% 50%;
- max-height: unset; /* [2] */
-
- /* stylelint-disable-next-line selector-no-qualifying-type */
- &[data-icon-status='transfers'] {
- transform: scale(${props.theme.rtl ? -1 : 1}, 1);
- }
-
- /* stylelint-disable-next-line selector-no-qualifying-type */
- &[data-icon-status='away'] circle {
- display: none; /* [1] */
- }
}
`;
};
const colorStyles = (props: IStatusIndicatorProps & ThemeProps) => {
- const foregroundColor = props.foregroundColor || props.theme.palette.white;
- const surfaceColor =
- props.surfaceColor ||
- (props.status ? props.theme.colors.background : (props.theme.palette.white as string));
- let backgroundColor = props.backgroundColor || 'transparent';
- let borderColor = props.borderColor || backgroundColor;
- let boxShadow = props.theme.shadows.sm(surfaceColor);
-
- if (props.size === xxs) {
- boxShadow = boxShadow.replace(props.theme.shadowWidths.sm, '1px');
- }
+ const { theme, type, size, foregroundColor, backgroundColor, borderColor, surfaceColor } = props;
- switch (props.status) {
- case available:
- case active:
- case away:
- case transfers:
- backgroundColor = getStatusColor(props.status, props.theme);
- borderColor = backgroundColor;
- break;
- case offline:
- borderColor = getStatusColor(props.status, props.theme);
- backgroundColor = props.theme.palette.white as string;
- break;
+ let boxShadow = theme.shadows.sm(
+ surfaceColor || (type ? theme.colors.background : (theme.palette.white as string))
+ );
+
+ if (size === xxs) {
+ boxShadow = boxShadow.replace(theme.shadowWidths.sm, '1px');
}
return css`
@@ -138,15 +79,13 @@ const colorStyles = (props: IStatusIndicatorProps & ThemeProps) =>
`;
};
-export const StyledStatusIndicator = styled.div.attrs({
+export const StyledStatusIndicator = styled(StyledStatusIndicatorBase).attrs({
'data-garden-id': COMPONENT_ID,
'data-garden-version': PACKAGE_VERSION
})`
${sizeStyles}
${colorStyles}
- transition: inherit;
-
${props => retrieveComponentStyles(COMPONENT_ID, props)};
`;
diff --git a/packages/avatars/src/styled/StyledStatusIndicatorBase.ts b/packages/avatars/src/styled/StyledStatusIndicatorBase.ts
new file mode 100644
index 00000000000..3a180f33327
--- /dev/null
+++ b/packages/avatars/src/styled/StyledStatusIndicatorBase.ts
@@ -0,0 +1,101 @@
+/**
+ * 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, { css, keyframes } from 'styled-components';
+import { retrieveComponentStyles, DEFAULT_THEME } from '@zendeskgarden/react-theming';
+
+import {
+ TRANSITION_DURATION,
+ getStatusColor,
+ getStatusBorderOffset,
+ getStatusSize,
+ IStyledStatusIndicatorProps
+} from './utility';
+
+const COMPONENT_ID = 'avatars.status-indicator.base';
+
+const iconFadeIn = keyframes`
+ 0% {
+ opacity: 0;
+ }
+
+ 100% {
+ opacity: 1;
+ }
+`;
+
+const sizeStyles = (props: IStyledStatusIndicatorProps) => {
+ const offset = getStatusBorderOffset(props);
+ const size = getStatusSize(props, offset);
+
+ /**
+ * 1. because we are using the stroke icon instead of fill due to artifacts in visual appearance,
+ * we need to remove the circle
+ * 2. when @zendeskgarden/css-bedrock is present, max-height needs to be unset due to icon being
+ * resized incorrectly
+ */
+ return css`
+ border: ${offset} ${props.theme.borderStyles.solid};
+ border-radius: ${size};
+ width: ${size};
+ min-width: ${size};
+ height: ${size};
+ line-height: ${size};
+
+ & > svg {
+ position: absolute;
+ top: -${offset};
+ left: -${offset};
+ transform-origin: 50% 50%;
+ animation: ${iconFadeIn} ${TRANSITION_DURATION}s;
+ max-height: unset; /* [2] */
+
+ /* stylelint-disable-next-line selector-no-qualifying-type */
+ &[data-icon-status='transfers'] {
+ transform: scale(${props.theme.rtl ? -1 : 1}, 1);
+ }
+
+ /* stylelint-disable-next-line selector-no-qualifying-type */
+ &[data-icon-status='away'] circle {
+ display: none; /* [1] */
+ }
+ }
+ `;
+};
+
+const colorStyles = (props: IStyledStatusIndicatorProps) => {
+ let backgroundColor = getStatusColor(props.type, props.theme);
+ let borderColor = backgroundColor;
+
+ if (props.type === 'offline') {
+ borderColor = getStatusColor(props.type, props.theme);
+ backgroundColor = props.theme.palette.white as string;
+ }
+
+ return css`
+ border-color: ${borderColor};
+ background-color: ${backgroundColor};
+ color: ${props.theme.palette.white};
+ `;
+};
+
+export const StyledStatusIndicatorBase = styled.div.attrs({
+ 'data-garden-id': COMPONENT_ID,
+ 'data-garden-version': PACKAGE_VERSION
+})`
+ transition: inherit;
+
+ ${sizeStyles}
+ ${colorStyles}
+
+ ${props => retrieveComponentStyles(COMPONENT_ID, props)};
+`;
+
+StyledStatusIndicatorBase.defaultProps = {
+ theme: DEFAULT_THEME,
+ size: 'small'
+};
diff --git a/packages/avatars/src/styled/index.ts b/packages/avatars/src/styled/index.ts
index b853ceed888..2681e9e38d0 100644
--- a/packages/avatars/src/styled/index.ts
+++ b/packages/avatars/src/styled/index.ts
@@ -6,5 +6,8 @@
*/
export * from './StyledAvatar';
+export * from './StyledStandaloneStatus';
+export * from './StyledStandaloneStatusCaption';
+export * from './StyledStandaloneStatusIndicator';
export * from './StyledStatusIndicator';
export * from './StyledText';
diff --git a/packages/avatars/src/styled/utility.ts b/packages/avatars/src/styled/utility.ts
index b7cea72ef05..01098ec2214 100644
--- a/packages/avatars/src/styled/utility.ts
+++ b/packages/avatars/src/styled/utility.ts
@@ -5,28 +5,65 @@
* found at http://www.apache.org/licenses/LICENSE-2.0.
*/
-import { getColor, DEFAULT_THEME } from '@zendeskgarden/react-theming';
+import { getColor } from '@zendeskgarden/react-theming';
+import { ThemeProps, DefaultTheme } from 'styled-components';
+import { math } from 'polished';
-import { STATUS } from '../types';
+import { SIZE, IAvatarProps } from '../types';
-const [active, available, away, transfers, offline] = ['active', ...STATUS];
+const [xxs, xs, s, m, l] = SIZE;
+
+export const TRANSITION_DURATION = 0.25;
+
+export interface IStyledStatusIndicatorProps extends ThemeProps {
+ readonly size?: IAvatarProps['size'];
+ readonly type?: IAvatarProps['status'] | 'active';
+}
export function getStatusColor(
- status: typeof STATUS[number] | 'active' | undefined,
- theme = DEFAULT_THEME
+ type?: IStyledStatusIndicatorProps['type'],
+ theme?: IStyledStatusIndicatorProps['theme']
): string {
- switch (status) {
- case active:
+ switch (type) {
+ case 'active':
return getColor('crimson', 400, theme)!;
- case available:
+ case 'available':
return getColor('mint', 400, theme)!;
- case away:
+ case 'away':
return getColor('orange', 400, theme)!;
- case transfers:
+ case 'transfers':
return getColor('azure', 400, theme)!;
- case offline:
+ case 'offline':
return getColor('grey', 500, theme)!;
default:
return 'transparent';
}
}
+
+export function getStatusBorderOffset(props: IStyledStatusIndicatorProps): string {
+ return props.size === xxs
+ ? math(`${props.theme.shadowWidths.sm} - 1`)
+ : props.theme.shadowWidths.sm;
+}
+
+export function getStatusSize(props: IStyledStatusIndicatorProps, offset: string): string {
+ const isActive = props.type === 'active';
+
+ switch (props.size) {
+ case xxs:
+ return math(`${props.theme.space.base}px - ${offset}`);
+ case xs:
+ return math(`${props.theme.space.base * 2}px - (${offset} * 2)`);
+ case s:
+ return math(`${props.theme.space.base * 3}px ${isActive ? '' : `- (${offset} * 2)`}`);
+ case m:
+ case l:
+ return math(`${props.theme.space.base * 4}px ${isActive ? '' : `- (${offset} * 2)`}`);
+ default:
+ return '0';
+ }
+}
+
+export function includes(array: readonly T[], element: U): element is T {
+ return array.includes(element as T);
+}
diff --git a/packages/avatars/src/types/index.ts b/packages/avatars/src/types/index.ts
index 7f80af3495f..3890da0fc0b 100644
--- a/packages/avatars/src/types/index.ts
+++ b/packages/avatars/src/types/index.ts
@@ -29,3 +29,10 @@ export interface IAvatarProps extends HTMLAttributes {
/** Sets the badge text and applies active styling */
badge?: string | number;
}
+
+export interface IStatusIndicatorProps extends HTMLAttributes {
+ /** Applies status type for styling and default aria-label */
+ type?: typeof STATUS[number];
+ /** Applies compact styling */
+ isCompact?: boolean;
+}