Skip to content

feat(avatars): allow null aria-label on StatusIndicator to mark as decorative #2031

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 4 commits into from
Jul 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 110 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions packages/avatars/demo/~patterns/stories/StatusMenuStory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,22 +43,22 @@ export const StatusMenuStory: Story = ({ isCompact }) => {
isCompact={isCompact}
>
<Item value="offline">
<StatusIndicator isCompact={isCompact} type="offline">
<StatusIndicator aria-label={null} isCompact={isCompact} type="offline">
Offline
</StatusIndicator>
</Item>
<Item value="available">
<StatusIndicator isCompact={isCompact} type="available">
<StatusIndicator aria-label={null} isCompact={isCompact} type="available">
Online
</StatusIndicator>
</Item>
<Item value="transfers">
<StatusIndicator isCompact={isCompact} type="transfers">
<StatusIndicator aria-label={null} isCompact={isCompact} type="transfers">
Transfers only
</StatusIndicator>
</Item>
<Item value="away">
<StatusIndicator isCompact={isCompact} type="away">
<StatusIndicator aria-label={null} isCompact={isCompact} type="away">
Away
</StatusIndicator>
</Item>
Expand Down
8 changes: 8 additions & 0 deletions packages/avatars/src/elements/StatusIndicator.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,13 @@ describe('StatusIndicator', () => {

expect(getByRole('img')).toHaveAttribute('aria-label', `status: ${type}`);
});

it.each(STATUS)('renders "$1" status type, and with aria label removed', type => {
const { container } = render(<StatusIndicator aria-label={null} type={type} />);
const imgElement = container.firstChild?.firstChild;

expect(imgElement).not.toHaveAttribute('aria-label');
expect(imgElement).toHaveAttribute('aria-hidden');
});
});
});
12 changes: 10 additions & 2 deletions packages/avatars/src/elements/StatusIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,13 @@ export const StatusIndicator = forwardRef<HTMLElement, IStatusIndicatorProps>(
}

const defaultLabel = useMemo(() => ['status'].concat(type || []).join(': '), [type]);
const ariaLabel = useText(StatusIndicator, { 'aria-label': label }, 'aria-label', defaultLabel);
const ariaLabel = useText(
StatusIndicator,
{ 'aria-label': label },
'aria-label',
defaultLabel,
label !== null
);

return (
// [1]
Expand All @@ -50,10 +56,11 @@ export const StatusIndicator = forwardRef<HTMLElement, IStatusIndicatorProps>(
{/* [2] */}
{/* eslint-disable-next-line jsx-a11y/prefer-tag-over-role */}
<StyledStandaloneStatusIndicator
aria-hidden={label === null ? true : undefined}
aria-label={ariaLabel}
role="img"
$type={type}
$size={isCompact ? 'small' : 'medium'}
aria-label={ariaLabel}
>
{type === 'away' ? <ClockIcon data-icon-status={type} aria-hidden="true" /> : null}
{type === 'transfers' ? (
Expand All @@ -69,6 +76,7 @@ export const StatusIndicator = forwardRef<HTMLElement, IStatusIndicatorProps>(
StatusIndicator.displayName = 'StatusIndicator';

StatusIndicator.propTypes = {
'aria-label': PropTypes.string,
type: PropTypes.oneOf(STATUS),
isCompact: PropTypes.bool
};
Expand Down
5 changes: 4 additions & 1 deletion packages/avatars/src/styled/StyledStatusIndicator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ const COMPONENT_ID = 'avatars.status_indicator';
const [xxs, xs, s, m, l] = SIZE;

const sizeStyles = (props: IStatusIndicatorProps & ThemeProps<DefaultTheme>) => {
const isVisible = !includes([xxs, xs], props.$size);
const isVisible = props.$size !== xxs;
const iconSize = props.$size === xs ? `${props.theme.space.base * 2}px` : undefined;
const borderWidth = getStatusBorderOffset(props);

let padding = '0';
Expand Down Expand Up @@ -58,6 +59,8 @@ const sizeStyles = (props: IStatusIndicatorProps & ThemeProps<DefaultTheme>) =>

& > svg {
${!isVisible && 'display: none;'}
width: ${iconSize};
height: ${iconSize};
}
`;
};
Expand Down
4 changes: 3 additions & 1 deletion packages/avatars/src/styled/StyledStatusIndicatorBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const sizeStyles = (props: IStyledStatusIndicatorProps) => {
* 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
* 3. prevent arrowhead cutting off in the transfers icon
*/
return css`
border: ${offset} ${props.theme.borderStyles.solid};
Expand All @@ -48,14 +49,15 @@ const sizeStyles = (props: IStyledStatusIndicatorProps) => {
& > svg {
position: absolute;
top: -${offset};
left: -${offset};
inset-inline-start: -${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);
inset-inline-start: ${props.$size === 'extrasmall' ? '-1px' : undefined}; /* [3] */
}

/* stylelint-disable-next-line selector-no-qualifying-type */
Expand Down
4 changes: 3 additions & 1 deletion packages/avatars/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ export interface IAvatarProps extends HTMLAttributes<HTMLElement> {
badge?: string | number;
}

export interface IStatusIndicatorProps extends HTMLAttributes<HTMLElement> {
export interface IStatusIndicatorProps extends Omit<HTMLAttributes<HTMLElement>, 'aria-label'> {
/** Overrides the label for the status indicator. Use `null` to mark the indicator as decorative. */
'aria-label'?: string | null;
/** Applies status type for styling and default aria-label */
type?: (typeof STATUS)[number];
/** Applies compact styling */
Expand Down