Skip to content

Commit

Permalink
Merge pull request #331 from riccardoperra/330-loading-indicator-when…
Browse files Browse the repository at this point in the history
…-exporting-as-image

fix(app): add loading indicator when exporting as image
  • Loading branch information
riccardoperra authored Aug 21, 2022
2 parents 6998146 + 1c079d0 commit 4f7b321
Show file tree
Hide file tree
Showing 10 changed files with 120 additions and 25 deletions.
5 changes: 5 additions & 0 deletions .changeset/famous-adults-attend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@codeimage/ui': minor
---

feat(ui): add LoadingCircle spinner icon and Button support for loading state and left icon
5 changes: 5 additions & 0 deletions .changeset/mean-baboons-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@codeimage/app': patch
---

fix(app): #330 loading indicator when exporting as image
9 changes: 3 additions & 6 deletions apps/codeimage/src/components/Toolbar/ExportButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,10 @@ export const ExportButton: Component<ExportButtonProps> = props => {
ref={openButtonRef}
variant={'solid'}
theme={'primary'}
disabled={data.loading}
loading={data.loading}
leftIcon={() => <DownloadIcon />}
>
<DownloadIcon />

<Box as={'span'} marginLeft={2}>
{label()}
</Box>
{label()}
</Button>

<Show when={overlayState.isOpen()}>
Expand Down
11 changes: 4 additions & 7 deletions apps/codeimage/src/components/Toolbar/ExportNewTabButton.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {useI18n} from '@codeimage/locale';
import {Box, Button, useSnackbarStore} from '@codeimage/ui';
import {Button, useSnackbarStore} from '@codeimage/ui';
import {Component, createEffect} from 'solid-js';
import {
ExportExtension,
Expand Down Expand Up @@ -60,14 +60,11 @@ export const ExportInNewTabButton: Component<ExportButtonProps> = props => {
<Button
variant={'solid'}
theme={'primaryAlt'}
disabled={data.loading}
loading={data.loading}
leftIcon={() => <ExternalLinkIcon />}
onClick={() => openInTab()}
>
<ExternalLinkIcon />

<Box as={'span'} marginLeft={2}>
{label()}
</Box>
{label()}
</Button>
);
};
12 changes: 11 additions & 1 deletion packages/ui/src/lib/primitives/Button/Button.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const button = style([

':disabled': {
cursor: 'default',
opacity: 0.3,
opacity: 0.5,
},

':focus': {
Expand All @@ -35,6 +35,10 @@ export const button = style([
},
]);

export const buttonIcon = style({
marginRight: themeVars.spacing['2'],
});

export const buttonVariant = recipe({
base: [button],
variants: {
Expand All @@ -49,6 +53,12 @@ export const buttonVariant = recipe({
borderRadius: themeVars.borderRadius.full,
},
},
loading: {
true: {
opacity: 0.7,
pointerEvents: 'none',
},
},
// Button type
variant: {
solid: {
Expand Down
51 changes: 41 additions & 10 deletions packages/ui/src/lib/primitives/Button/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,57 @@
import * as styles from './Button.css';
import {elements} from '@solid-primitives/refs';
import clsx from 'clsx';
import {Button as ShButton} from 'solid-headless';
import {ButtonProps as ShButtonProps} from 'solid-headless/dist/types/components/Button';
import {ValidConstructor} from 'solid-headless/dist/types/utils/dynamic-prop';
import {JSXElement} from 'solid-js';
import {children, JSXElement, ParentProps, Show} from 'solid-js';
import {omitProps} from 'solid-use';
import {CustomComponentProps} from '../../utils';
import {LoadingCircle} from '../Loader';
import * as styles from './Button.css';

type ButtonProps<T extends ValidConstructor = 'button'> = ShButtonProps<T> &
styles.ButtonVariants;
type ButtonProps<T extends ValidConstructor = 'button'> = CustomComponentProps<
T,
{
leftIcon?: JSXElement;
loading?: boolean;
} & ShButtonProps<T> &
styles.ButtonVariants
>;

export function Button<T extends ValidConstructor = 'button'>(
props: ButtonProps<T>,
props: ParentProps<ButtonProps<T>>,
): JSXElement {
return (
<ShButton
{...props}
class={`${styles.buttonVariant({
const classes = () =>
clsx(
styles.buttonVariant({
pill: props.pill,
block: props.block,
theme: props.theme,
variant: props.variant,
size: props.size || 'sm',
})} ${props.class || ''}`}
loading: props.loading,
}),
props.class,
);

const resolvedIcon = (): JSXElement => {
if (!props.leftIcon) return null;
const item = children(() => props.leftIcon);
const els = elements(item, SVGElement, HTMLElement);
// Should always be 1
els().forEach(el => el.classList.add(styles.buttonIcon));
return els();
};

return (
<ShButton
{...omitProps(props, ['class', 'leftIcon', 'loading'])}
class={classes()}
>
<Show when={!!props.leftIcon && !props.loading}>{resolvedIcon()}</Show>
<Show when={props.loading}>
<LoadingCircle class={styles.buttonIcon} size={props.size ?? 'sm'} />
</Show>
{props.children}
</ShButton>
);
Expand Down
3 changes: 2 additions & 1 deletion packages/ui/src/lib/primitives/Icon/SvgIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ export type SvgIconProps = _SvgIconProps &

export function SvgIcon(props: SvgIconProps): JSX.Element {
const computedProps = mergeProps({size: 'md'} as _SvgIconProps, props);
const classes = () => clsx(svgIcon({size: computedProps.size}), props.class);

return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
{...props}
class={clsx(props.class, svgIcon({size: computedProps.size}))}
class={classes()}
>
{props.children}
</svg>
Expand Down
19 changes: 19 additions & 0 deletions packages/ui/src/lib/primitives/Loader/LoadingCircle.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {keyframes, style} from '@vanilla-extract/css';

export const spinner = keyframes({
to: {
transform: 'rotate(1turn)',
},
});

export const loadingIcon = style({
animation: `${spinner} 1s linear infinite`,
});

export const circle = style({
opacity: '.25',
});

export const ring = style({
opacity: '.75',
});
29 changes: 29 additions & 0 deletions packages/ui/src/lib/primitives/Loader/LoadingCircle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import clsx from 'clsx';
import {SvgIcon, SvgIconProps} from '../Icon';
import * as styles from './LoadingCircle.css';

function LoadingCircleIcon(props: SvgIconProps) {
return (
<SvgIcon fill="none" viewBox="0 0 24 24" {...props}>
<circle
class={styles.circle}
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class={styles.ring}
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</SvgIcon>
);
}

export function LoadingCircle(props: SvgIconProps) {
const classes = () => clsx(props.class, styles.loadingIcon);

return <LoadingCircleIcon {...props} class={classes()} />;
}
1 change: 1 addition & 0 deletions packages/ui/src/lib/primitives/Loader/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './Loading';
export * from './LoadingCircle';
export * from './LoadingOverlay';

0 comments on commit 4f7b321

Please sign in to comment.