Skip to content
Draft
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
52 changes: 52 additions & 0 deletions packages/ndla-ui/src/LoadingButton/LoadingButton.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Copyright (c) 2025-present, NDLA.
*
* This source code is licensed under the GPLv3 license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import type { Meta, StoryFn, StoryObj } from "@storybook/react";
import { LoadingButton } from "./LoadingButton";

export default {
title: "Components/LoadingButton/LoadingButton",
component: LoadingButton,
tags: ["autodocs"],
parameters: {
inlineStories: true,
},
args: {
children: "Button",
size: "medium",
variant: "primary",
loading: true,
"aria-label": "Laster",
},
} as Meta<typeof LoadingButton>;

export const Primary: StoryFn<typeof LoadingButton> = ({ ...args }) => {
return <LoadingButton {...args} />;
};

export const LoadingReplace: StoryObj<typeof LoadingButton> = {
args: {
loading: true,
replaceContent: true,
},
};

export const CustomLoading: StoryObj<typeof LoadingButton> = {
args: {
loading: true,
loadingContent: "...",
},
};

export const CustomLoadingReplace: StoryObj<typeof LoadingButton> = {
args: {
loading: true,
replaceContent: true,
loadingContent: "Laster...",
},
};
74 changes: 74 additions & 0 deletions packages/ndla-ui/src/LoadingButton/LoadingButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* Copyright (c) 2025-present, NDLA.
*
* This source code is licensed under the GPLv3 license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import { forwardRef, useMemo, type ReactNode } from "react";
import { type HTMLArkProps } from "@ark-ui/react";
import { Button, Spinner, type ButtonVariantProps, type IconButtonVariantProps } from "@ndla/primitives";
import { styled } from "@ndla/styled-system/jsx";
import type { JsxStyleProps } from "@ndla/styled-system/types";

const StyledButton = styled(Button, {}, { baseComponent: true, defaultProps: { type: "button" } });

export interface BaseButtonProps extends HTMLArkProps<"button">, JsxStyleProps {
loading?: boolean;
loadingContent?: ReactNode;
replaceContent?: boolean;
}

export const BaseButton = forwardRef<HTMLButtonElement, BaseButtonProps>(
({ loading, loadingContent: loadingContentProp, replaceContent, onClick: _onClick, children, ...props }, ref) => {
const ariaDisabled = loading ? { "aria-disabled": true } : {};

const onClick = useMemo(() => (loading ? undefined : _onClick), [_onClick, loading]);

const loadingContent = useMemo(() => {
return replaceContent ? (
loadingContentProp
) : (
<>
{loadingContentProp}
{children}
</>
);
}, [children, loadingContentProp, replaceContent]);

return (
<StyledButton onClick={onClick} {...ariaDisabled} {...props} ref={ref}>
{loading ? loadingContent : children}
</StyledButton>
);
},
);
interface LoadingButtonProps extends BaseButtonProps, ButtonVariantProps {
"aria-label": string;
}

export const LoadingButton = forwardRef<HTMLButtonElement, LoadingButtonProps>(
({ loadingContent, "aria-label": ariaLabel, ...props }, ref) => (
<BaseButton
{...props}
loadingContent={loadingContent ?? <Spinner size="small" aria-label={ariaLabel} />}
ref={ref}
/>
),
);

interface LoadingIconButtonProps extends BaseButtonProps, IconButtonVariantProps {
"aria-label": string;
}

export const LoadingIconButton = forwardRef<HTMLButtonElement, LoadingIconButtonProps>(
({ loadingContent, replaceContent = true, "aria-label": ariaLabel, ...props }, ref) => (
<BaseButton
{...props}
loadingContent={loadingContent ?? <Spinner size="small" aria-label={ariaLabel} />}
replaceContent={replaceContent}
ref={ref}
/>
),
);
48 changes: 48 additions & 0 deletions packages/ndla-ui/src/LoadingButton/LoadingIconButton.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Copyright (c) 2025-present, NDLA.
*
* This source code is licensed under the GPLv3 license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import type { Meta, StoryFn, StoryObj } from "@storybook/react";
import { CloseLine } from "@ndla/icons";
import { LoadingIconButton } from "./LoadingButton";

export default {
title: "Components/LoadingButton/LoadingIconButton",
component: LoadingIconButton,
tags: ["autodocs"],
args: {
children: <CloseLine />,
size: "medium",
loading: true,
"aria-label": "Laster",
},
argTypes: {
children: {
table: {
disable: true,
},
},
},
} as Meta<typeof LoadingIconButton>;

export const Primary: StoryFn<typeof LoadingIconButton> = ({ ...args }) => {
return <LoadingIconButton {...args} />;
};

export const LoadingReplace: StoryObj<typeof LoadingIconButton> = {
args: {
loading: true,
replaceContent: true,
},
};

export const CustomLoading: StoryObj<typeof LoadingIconButton> = {
args: {
loading: true,
loadingContent: "...",
},
};
9 changes: 9 additions & 0 deletions packages/ndla-ui/src/LoadingButton/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Copyright (c) 2025-present, NDLA.
*
* This source code is licensed under the GPLv3 license found in the
* LICENSE file in the root directory of this source tree.
*
*/

export { LoadingButton, LoadingIconButton } from "./LoadingButton";
1 change: 1 addition & 0 deletions packages/ndla-ui/src/i18n/useComponentTranslations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ export const useVideoSearchTranslations = (translations?: Partial<VideoTranslati
previewVideo: t("previewVideo"),
addVideo: t("addVideo"),
close: t("close"),
loading: t("loading"),
...translations,
}),
[t, translations],
Expand Down
2 changes: 2 additions & 0 deletions packages/ndla-ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,5 @@ export { ZendeskButton } from "./ZendeskButton/ZendeskButton";
export type { ZendeskButtonProps } from "./ZendeskButton/ZendeskButton";

export { licenseAttributes } from "./utils/licenseAttributes";

export { LoadingButton, LoadingIconButton } from "./LoadingButton";
1 change: 1 addition & 0 deletions packages/ndla-ui/src/locale/messages-en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,7 @@ const messages = {
previewVideo: "Preview",
is360Video: "VR video",
close: "Lukk",
loading: "Loading videos",
},
},
richTextEditor: {
Expand Down
1 change: 1 addition & 0 deletions packages/ndla-ui/src/locale/messages-nb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@ const messages = {
previewVideo: "Forhåndsvis",
is360Video: "VR-video",
close: "Lukk",
loading: "Laster videoer",
},
},
richTextEditor: {
Expand Down
1 change: 1 addition & 0 deletions packages/ndla-ui/src/locale/messages-nn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@ const messages = {
previewVideo: "Førehandsvis",
is360Video: "VR-video",
close: "Lukk",
loading: "Lastar videoar",
},
},
richTextEditor: {
Expand Down
1 change: 1 addition & 0 deletions packages/ndla-ui/src/locale/messages-se.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ const messages = {
previewVideo: "Forhåndsvis",
is360Video: "VR-video",
close: "Lukk",
loading: "Laster videoer",
},
},
richTextEditor: {
Expand Down
2 changes: 1 addition & 1 deletion packages/ndla-video-search/src/VideoResultList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const VideoResultList = ({
))}
</StyledList>
)}
{!!isLoading && <Spinner />}
{!!isLoading && <Spinner aria-label={translations.loading} />}
{!!existsMoreVideos && <Button onClick={onShowMore}>{translations.loadMoreVideos}</Button>}
</StyledVideoResultWrapper>
);
Expand Down
1 change: 1 addition & 0 deletions packages/ndla-video-search/src/VideoSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface VideoTranslations {
previewVideo: string;
addVideo: string;
close: string;
loading: string;
}

interface Props {
Expand Down
28 changes: 0 additions & 28 deletions packages/primitives/src/Button.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,34 +83,6 @@ export const Disabled: StoryObj<typeof Button> = {
},
};

export const Loading: StoryObj<typeof Button> = {
args: {
loading: true,
},
};

export const LoadingReplace: StoryObj<typeof Button> = {
args: {
loading: true,
replaceContent: true,
},
};

export const CustomLoading: StoryObj<typeof Button> = {
args: {
loading: true,
loadingContent: "...",
},
};

export const CustomLoadingReplace: StoryObj<typeof Button> = {
args: {
loading: true,
replaceContent: true,
loadingContent: "Laster...",
},
};

export const WithIcon: StoryObj<typeof Button> = {
args: {
children: (
Expand Down
75 changes: 19 additions & 56 deletions packages/primitives/src/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@
*
*/

import { type ReactNode, forwardRef, useMemo } from "react";
import { type HTMLArkProps, ark } from "@ark-ui/react";
import { forwardRef } from "react";
import { ark, type HTMLArkProps } from "@ark-ui/react";
import { type RecipeVariantProps, css, cva } from "@ndla/styled-system/css";
import { styled } from "@ndla/styled-system/jsx";
import type { JsxStyleProps, RecipeVariant } from "@ndla/styled-system/types";
import { Spinner } from "./Spinner";

export const buttonBaseRecipe = cva({
base: {
Expand Down Expand Up @@ -249,75 +248,39 @@ export const iconButtonRecipe = cva({
},
});

const StyledButton = styled(ark.button, {}, { baseComponent: true, defaultProps: { type: "button" } });

type Variant = RecipeVariant<typeof buttonBaseRecipe>["variant"];

type ButtonVariant = Exclude<Variant, "clear" | "clearSubtle">;

export type ButtonVariantProps = { variant?: ButtonVariant } & RecipeVariantProps<typeof buttonRecipe>;

export interface BaseButtonProps extends HTMLArkProps<"button">, JsxStyleProps {
loading?: boolean;
loadingContent?: ReactNode;
replaceContent?: boolean;
}

export type ButtonProps = BaseButtonProps & ButtonVariantProps;

const StyledButton = styled(ark.button, {}, { baseComponent: true, defaultProps: { type: "button" } });

export const BaseButton = forwardRef<HTMLButtonElement, BaseButtonProps>(
({ loading, loadingContent: loadingContentProp, replaceContent, onClick: _onClick, children, ...props }, ref) => {
const ariaDisabled = loading ? { "aria-disabled": true } : {};

const onClick = useMemo(() => (loading ? undefined : _onClick), [_onClick, loading]);

const loadingContent = useMemo(() => {
return replaceContent ? (
loadingContentProp
) : (
<>
{loadingContentProp}
{children}
</>
);
}, [children, loadingContentProp, replaceContent]);
export type ButtonProps = HTMLArkProps<"button"> & JsxStyleProps & ButtonVariantProps;

return (
<StyledButton onClick={onClick} {...ariaDisabled} {...props} ref={ref}>
{loading ? loadingContent : children}
</StyledButton>
);
},
);

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ variant, loadingContent, size, css: cssProp, ...props }, ref) => (
<BaseButton
{...props}
loadingContent={loadingContent ?? <Spinner size="small" />}
css={css.raw(
buttonBaseRecipe.raw({ variant }),
variant !== "link" ? buttonRecipe.raw({ size }) : undefined,
cssProp,
)}
ref={ref}
/>
),
);
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(({ variant, size, css: cssProp, ...props }, ref) => (
<StyledButton
{...props}
css={css.raw(
buttonBaseRecipe.raw({ variant }),
variant !== "link" ? buttonRecipe.raw({ size }) : undefined,
cssProp,
)}
ref={ref}
/>
));

type IconButtonVariant = Exclude<Variant, "link">;

export type IconButtonVariantProps = { variant?: IconButtonVariant } & RecipeVariantProps<typeof iconButtonRecipe>;

export type IconButtonProps = BaseButtonProps & IconButtonVariantProps;
export type IconButtonProps = HTMLArkProps<"button"> & JsxStyleProps & IconButtonVariantProps;

export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
({ variant, css: cssProp, loadingContent, size, replaceContent = true, ...props }, ref) => (
<BaseButton
({ variant, css: cssProp, size, ...props }, ref) => (
<StyledButton
{...props}
css={css.raw(buttonBaseRecipe.raw({ variant }), iconButtonRecipe.raw({ size }), cssProp)}
loadingContent={loadingContent ?? <Spinner size="small" />}
replaceContent={replaceContent}
ref={ref}
/>
),
Expand Down
Loading