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'; Example Avatar + + + 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 + + + + {args => } + + 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. + + + + {args => } + + 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)}> + + + Example User + + + + + + Online + + + + + + Transfers only + + + + + + Away + + + + + + Offline + + + + + + + + ); +}; 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' ? + {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; +}