Skip to content

Commit

Permalink
feat(ActionCard): add loading state with optional animation (#1507)
Browse files Browse the repository at this point in the history
  • Loading branch information
JoannaSikora authored Feb 3, 2025
1 parent be88a3e commit 6c90ff8
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 58 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@import '../../styles/animations';

$base-class: 'action-card';

@mixin verticalStyles {
Expand All @@ -11,8 +13,37 @@ $base-class: 'action-card';
}
}

.visually-hidden {
position: absolute;
margin: -1px;
border: 0;
padding: 0;
width: 1px;
height: 1px;
overflow: hidden;
white-space: nowrap;
clip: rect(0, 0, 0, 0);
}

.main-wrapper {
container-type: inline-size;

&.#{$base-class}--loading {
border: 0;
border-radius: var(--radius-4);
background-color: var(--surface-secondary-disabled);
cursor: default;
width: 100%;
min-height: 280px;

.#{$base-class} {
display: none;
}
}

&.#{$base-class}--loading--animated {
@include skeleton-loading;
}
}

.#{$base-class} {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,13 @@ describe('<ActionCard> component', () => {
expect(onClick).not.toHaveBeenCalled();
expect(onButtonClick).toHaveBeenCalledTimes(1);
});

it('should display loading state when isLoading is true', () => {
const { queryByText } = renderComponent({
isLoading: true,
});

expect(queryByText('Example content')).toBeNull();
expect(queryByText('Example button')).toBeNull();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as React from 'react';
import { Meta } from '@storybook/react';

import image from '../../stories/assets/chat-window.png';
import { StoryDescriptor } from '../../stories/components/StoryDescriptor';
import { Button } from '../Button';
import { Heading, Text } from '../Typography';

Expand Down Expand Up @@ -77,3 +78,20 @@ export const TwoColumns = (): React.ReactElement => {
</ActionCard>
);
};

export const Loading = (): React.ReactElement => {
return (
<>
<StoryDescriptor title="Loading">
<ActionCard isLoading>
<PrimaryColumnComponent />
</ActionCard>
</StoryDescriptor>
<StoryDescriptor title="Loading with animation">
<ActionCard isLoading isLoadingAnimated>
<PrimaryColumnComponent />
</ActionCard>
</StoryDescriptor>
</>
);
};
64 changes: 40 additions & 24 deletions packages/react-components/src/components/ActionCard/ActionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,18 @@ export const ActionCard: FC<PropsWithChildren<ActionCardProps>> = ({
firstColumnClassName,
secondColumnClassName,
onClick,
isLoading = false,
isLoadingAnimated = false,
}) => {
const mergedClassNames = cx(styles[baseClass], className);

const wrapperClassNames = cx(styles[`main-wrapper`], {
[styles[`${baseClass}--loading`]]: isLoading,
[styles[`${baseClass}--loading--animated`]]: isLoadingAnimated,
});

const handleOnClick = (e: MouseEvent<HTMLDivElement>) => {
if (isLoading) return;
if (e.currentTarget !== document.activeElement) {
return;
}
Expand All @@ -27,6 +35,7 @@ export const ActionCard: FC<PropsWithChildren<ActionCardProps>> = ({
};

const handleOnKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
if (isLoading) return;
if (e.currentTarget !== document.activeElement) {
return;
}
Expand All @@ -38,42 +47,49 @@ export const ActionCard: FC<PropsWithChildren<ActionCardProps>> = ({
e.key === 'Space'
) {
e.preventDefault();

onClick?.();
}
};

return (
<div className={styles[`main-wrapper`]}>
<div className={wrapperClassNames}>
<div aria-live="polite" className={styles['visually-hidden']}>
{isLoading ? 'Loading content' : null}
</div>
<div
role="button"
role={isLoading ? 'presentation' : 'button'}
aria-label="Action Card"
tabIndex={0}
aria-busy={isLoading}
tabIndex={isLoading ? -1 : 0}
className={mergedClassNames}
onClick={handleOnClick}
onKeyDown={handleOnKeyDown}
>
<div
data-testid={`${baseClass}-first-column`}
className={cx(
styles[`${baseClass}__column`],
styles[`${baseClass}__column--first`],
firstColumnClassName
)}
>
{children}
</div>
{secondColumn && (
<div
data-testid={`${baseClass}-second-column`}
className={cx(
styles[`${baseClass}__column`],
styles[`${baseClass}__column--second`],
secondColumnClassName
{!isLoading && (
<>
<div
data-testid={`${baseClass}-first-column`}
className={cx(
styles[`${baseClass}__column`],
styles[`${baseClass}__column--first`],
firstColumnClassName
)}
>
{children}
</div>
{secondColumn && (
<div
data-testid={`${baseClass}-second-column`}
className={cx(
styles[`${baseClass}__column`],
styles[`${baseClass}__column--second`],
secondColumnClassName
)}
>
{secondColumn}
</div>
)}
>
{secondColumn}
</div>
</>
)}
</div>
</div>
Expand Down
25 changes: 23 additions & 2 deletions packages/react-components/src/components/ActionCard/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
import * as React from 'react';

export interface ActionCardProps {
export type ActionCardProps = (
| {
/**
* Specify if the card is in loading state
*/
isLoading: true;
/**
* Specify if the card is in animated loading state
*/
isLoadingAnimated?: boolean;
}
| {
/**
* Specify if the card is in loading state
*/
isLoading?: false;
/**
* Loading animation is not available when not loading
*/
isLoadingAnimated?: never;
}
) & {
/**
* The CSS class for main container
*/
Expand All @@ -21,4 +42,4 @@ export interface ActionCardProps {
* Optional handler called on card click
*/
onClick?: () => void;
}
};
Original file line number Diff line number Diff line change
@@ -1,39 +1,11 @@
@import '../../styles/animations';

$base-class: 'skeleton';

@mixin background() {
background-color: var(--surface-secondary-disabled);
}

@keyframes loading {
0% {
left: -100%;
}

100% {
left: 100%;
}
}

@mixin animation() {
position: relative;
overflow: hidden;

&::before {
position: absolute;
left: 0;
background: linear-gradient(
90deg,
var(--animated-gradient-value-1),
var(--animated-gradient-value-2),
var(--animated-gradient-value-3)
);
width: 100%;
height: 100%;
animation: loading 2s forwards infinite;
content: '';
}
}

.skeleton-wrapper {
display: flex;
gap: var(--spacing-2);
Expand All @@ -55,7 +27,7 @@ $base-class: 'skeleton';
}

&--animated {
@include animation;
@include skeleton-loading;
}
}

Expand All @@ -66,6 +38,6 @@ $base-class: 'skeleton';
width: 100%;

&--animated {
@include animation;
@include skeleton-loading;
}
}
29 changes: 29 additions & 0 deletions packages/react-components/src/styles/_animations.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
@keyframes loading {
0% {
left: -100%;
}

100% {
left: 100%;
}
}

@mixin skeleton-loading {
position: relative;
overflow: hidden;

&::before {
position: absolute;
left: 0;
background: linear-gradient(
90deg,
var(--animated-gradient-value-1),
var(--animated-gradient-value-2),
var(--animated-gradient-value-3)
);
width: 100%;
height: 100%;
animation: loading 2s forwards infinite;
content: '';
}
}

0 comments on commit 6c90ff8

Please sign in to comment.