Skip to content

Commit

Permalink
feat(components): show ProgressBar for pending buttons (#1426)
Browse files Browse the repository at this point in the history
* feat(components): show `ProgressBar` for pending buttons

* chore: add test

* fix: keep text

* fix: progress first

* fix: alignment
  • Loading branch information
Niznikr authored Oct 2, 2024
1 parent 5fff6f6 commit 04723e9
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 12 deletions.
5 changes: 5 additions & 0 deletions .changeset/sixty-dolls-change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@launchpad-ui/components": patch
---

Show `ProgressBar` for pending buttons
5 changes: 5 additions & 0 deletions packages/components/__tests__/Button.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,9 @@ describe('Button', () => {
render(<Button>Button</Button>);
expect(screen.getByRole('button')).toBeVisible();
});

it('renders progressbar when pending', () => {
render(<Button isPending>Button</Button>);
expect(screen.getByRole('progressbar')).toBeVisible();
});
});
12 changes: 11 additions & 1 deletion packages/components/src/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import { cva, cx } from 'class-variance-authority';
import { forwardRef } from 'react';
import {
Button as AriaButton,
Provider,
SelectContext,
TextContext,
composeRenderProps,
useSlottedContext,
} from 'react-aria-components';

import { input } from './Input';
import { ProgressBar } from './ProgressBar';
import styles from './styles/Button.module.css';

const button = cva(styles.base, {
Expand Down Expand Up @@ -54,7 +57,14 @@ const _Button = (
? cx(input(), styles.select, selectContext.isInvalid && styles.invalid, className)
: button({ ...renderProps, size, variant, className }),
)}
/>
>
{composeRenderProps(props.children, (children, { isPending }) => (
<Provider values={[[TextContext, { className: isPending ? styles.pending : undefined }]]}>
{isPending && <ProgressBar isIndeterminate aria-label="loading" />}
{children}
</Provider>
))}
</AriaButton>
);
};

Expand Down
10 changes: 10 additions & 0 deletions packages/components/src/styles/Button.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,13 @@
color: var(--lp-color-text-interactive-base);
z-index: 1;
}

.base[data-pending] {
& [data-icon] {
opacity: 50%;
}
}

.pending {
opacity: 50%;
}
59 changes: 48 additions & 11 deletions packages/components/stories/Button.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import type { PlayFunction } from '@storybook/types';
import { Icon } from '@launchpad-ui/icons';
import { vars } from '@launchpad-ui/vars';
import { fireEvent, userEvent, within } from '@storybook/test';
import { useEffect, useRef, useState } from 'react';

import { Button } from '../src';
import { Button, Text } from '../src';

const meta: Meta<typeof Button> = {
component: Button,
Expand Down Expand Up @@ -80,56 +81,92 @@ const play: PlayFunction<ReactRenderer> = async ({
};

export const Default: Story = {
render: (args) => renderStates({ children: 'Default', ...args }),
render: (args) => renderStates({ children: <Text>Default</Text>, ...args }),
play,
};

export const Primary: Story = {
render: (args) => renderStates({ children: 'Primary', variant: 'primary', ...args }),
render: (args) => renderStates({ children: <Text>Primary</Text>, variant: 'primary', ...args }),
play,
};

export const Minimal: Story = {
render: (args) => renderStates({ children: 'Minimal', variant: 'minimal', ...args }),
render: (args) => renderStates({ children: <Text>Minimal</Text>, variant: 'minimal', ...args }),
play,
};

export const Destructive: Story = {
render: (args) => renderStates({ children: 'Destructive', variant: 'destructive', ...args }),
render: (args) =>
renderStates({ children: <Text>Destructive</Text>, variant: 'destructive', ...args }),
play,
};

export const PrimaryFlair: Story = {
render: (args) => renderStates({ children: 'Primary flair', variant: 'primaryFlair', ...args }),
render: (args) =>
renderStates({ children: <Text>Primary flair</Text>, variant: 'primaryFlair', ...args }),
play,
};

export const DefaultFlair: Story = {
render: (args) => renderStates({ children: 'Default flair', variant: 'defaultFlair', ...args }),
render: (args) =>
renderStates({ children: <Text>Default flair</Text>, variant: 'defaultFlair', ...args }),
play,
};

export const MinimalFlair: Story = {
render: (args) => renderStates({ children: 'Minimal flair', variant: 'minimalFlair', ...args }),
render: (args) =>
renderStates({ children: <Text>Minimal flair</Text>, variant: 'minimalFlair', ...args }),
play,
};

export const WithIcon: Story = {
args: {
children: (
<>
With icon <Icon name="add" size="small" />
<Text>With icon </Text>
<Icon name="add" size="small" />
</>
),
},
};

export const Small: Story = {
render: (args) => renderStates({ children: 'Default', size: 'small', ...args }),
render: (args) => renderStates({ children: <Text>Default</Text>, size: 'small', ...args }),
play,
};

export const Large: Story = {
render: (args) => renderStates({ children: 'Default', size: 'large', ...args }),
render: (args) => renderStates({ children: <Text>Default</Text>, size: 'large', ...args }),
play,
};

export const Pending: Story = {
render: (args) => {
const [isPending, setPending] = useState(false);

const timeout = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
const handlePress = () => {
setPending(true);
timeout.current = setTimeout(() => {
setPending(false);
timeout.current = undefined;
}, 2000);
};

useEffect(() => {
return () => {
clearTimeout(timeout.current);
};
}, []);

return <Button isPending={isPending} onPress={handlePress} {...args} />;
},
args: {
children: <Text>Pending</Text>,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);

await userEvent.click(canvas.getByRole('button'));
},
};

0 comments on commit 04723e9

Please sign in to comment.