From 4c9869f5a9f937148d3ea1f20927757ca3f8f998 Mon Sep 17 00:00:00 2001 From: Katrine Wist Date: Wed, 5 Mar 2025 08:32:24 +0100 Subject: [PATCH 1/3] Make spinner accessible --- packages/primitives/src/Spinner.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/primitives/src/Spinner.tsx b/packages/primitives/src/Spinner.tsx index d2835391c4..3cc9bcc407 100644 --- a/packages/primitives/src/Spinner.tsx +++ b/packages/primitives/src/Spinner.tsx @@ -52,10 +52,20 @@ export const spinnerRecipe = cva({ export type SpinnerVariantProps = RecipeVariantProps; -export type SpinnerProps = HTMLArkProps<"div"> & JsxStyleProps & SpinnerVariantProps; +interface Props extends HTMLArkProps<"div"> { + "aria-label": string; +} + +export type SpinnerProps = Props & JsxStyleProps & SpinnerVariantProps; const StyledSpinner = styled(ark.div, {}, { baseComponent: true }); export const Spinner = forwardRef(({ size, css: cssProp, ...props }, ref) => ( - + )); From dfb651743562b6a2b7d090709714f51a353460af Mon Sep 17 00:00:00 2001 From: Katrine Wist Date: Wed, 5 Mar 2025 08:35:17 +0100 Subject: [PATCH 2/3] Remove loading from Button - create LoadingButton and LoadingIconButton as separate components --- .../LoadingButton/LoadingButton.stories.tsx | 52 +++++++++++++ .../src/LoadingButton/LoadingButton.tsx | 74 ++++++++++++++++++ .../LoadingIconButton.stories.tsx | 48 ++++++++++++ packages/ndla-ui/src/LoadingButton/index.ts | 9 +++ packages/ndla-ui/src/index.ts | 2 + packages/primitives/src/Button.stories.tsx | 28 ------- packages/primitives/src/Button.tsx | 75 +++++-------------- .../primitives/src/IconButton.stories.tsx | 20 ----- 8 files changed, 204 insertions(+), 104 deletions(-) create mode 100644 packages/ndla-ui/src/LoadingButton/LoadingButton.stories.tsx create mode 100644 packages/ndla-ui/src/LoadingButton/LoadingButton.tsx create mode 100644 packages/ndla-ui/src/LoadingButton/LoadingIconButton.stories.tsx create mode 100644 packages/ndla-ui/src/LoadingButton/index.ts diff --git a/packages/ndla-ui/src/LoadingButton/LoadingButton.stories.tsx b/packages/ndla-ui/src/LoadingButton/LoadingButton.stories.tsx new file mode 100644 index 0000000000..cca0af2aba --- /dev/null +++ b/packages/ndla-ui/src/LoadingButton/LoadingButton.stories.tsx @@ -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; + +export const Primary: StoryFn = ({ ...args }) => { + return ; +}; + +export const LoadingReplace: StoryObj = { + args: { + loading: true, + replaceContent: true, + }, +}; + +export const CustomLoading: StoryObj = { + args: { + loading: true, + loadingContent: "...", + }, +}; + +export const CustomLoadingReplace: StoryObj = { + args: { + loading: true, + replaceContent: true, + loadingContent: "Laster...", + }, +}; diff --git a/packages/ndla-ui/src/LoadingButton/LoadingButton.tsx b/packages/ndla-ui/src/LoadingButton/LoadingButton.tsx new file mode 100644 index 0000000000..5c7cf3ce60 --- /dev/null +++ b/packages/ndla-ui/src/LoadingButton/LoadingButton.tsx @@ -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( + ({ 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 ( + + {loading ? loadingContent : children} + + ); + }, +); +interface LoadingButtonProps extends BaseButtonProps, ButtonVariantProps { + "aria-label": string; +} + +export const LoadingButton = forwardRef( + ({ loadingContent, "aria-label": ariaLabel, ...props }, ref) => ( + } + ref={ref} + /> + ), +); + +interface LoadingIconButtonProps extends BaseButtonProps, IconButtonVariantProps { + "aria-label": string; +} + +export const LoadingIconButton = forwardRef( + ({ loadingContent, replaceContent = true, "aria-label": ariaLabel, ...props }, ref) => ( + } + replaceContent={replaceContent} + ref={ref} + /> + ), +); diff --git a/packages/ndla-ui/src/LoadingButton/LoadingIconButton.stories.tsx b/packages/ndla-ui/src/LoadingButton/LoadingIconButton.stories.tsx new file mode 100644 index 0000000000..5d52422f38 --- /dev/null +++ b/packages/ndla-ui/src/LoadingButton/LoadingIconButton.stories.tsx @@ -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: , + size: "medium", + loading: true, + "aria-label": "Laster", + }, + argTypes: { + children: { + table: { + disable: true, + }, + }, + }, +} as Meta; + +export const Primary: StoryFn = ({ ...args }) => { + return ; +}; + +export const LoadingReplace: StoryObj = { + args: { + loading: true, + replaceContent: true, + }, +}; + +export const CustomLoading: StoryObj = { + args: { + loading: true, + loadingContent: "...", + }, +}; diff --git a/packages/ndla-ui/src/LoadingButton/index.ts b/packages/ndla-ui/src/LoadingButton/index.ts new file mode 100644 index 0000000000..0e9c1cac9a --- /dev/null +++ b/packages/ndla-ui/src/LoadingButton/index.ts @@ -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"; diff --git a/packages/ndla-ui/src/index.ts b/packages/ndla-ui/src/index.ts index 3f2acb0645..b3d8e9016a 100644 --- a/packages/ndla-ui/src/index.ts +++ b/packages/ndla-ui/src/index.ts @@ -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"; diff --git a/packages/primitives/src/Button.stories.tsx b/packages/primitives/src/Button.stories.tsx index dbfd791822..860fb00113 100644 --- a/packages/primitives/src/Button.stories.tsx +++ b/packages/primitives/src/Button.stories.tsx @@ -83,34 +83,6 @@ export const Disabled: StoryObj = { }, }; -export const Loading: StoryObj = { - args: { - loading: true, - }, -}; - -export const LoadingReplace: StoryObj = { - args: { - loading: true, - replaceContent: true, - }, -}; - -export const CustomLoading: StoryObj = { - args: { - loading: true, - loadingContent: "...", - }, -}; - -export const CustomLoadingReplace: StoryObj = { - args: { - loading: true, - replaceContent: true, - loadingContent: "Laster...", - }, -}; - export const WithIcon: StoryObj = { args: { children: ( diff --git a/packages/primitives/src/Button.tsx b/packages/primitives/src/Button.tsx index c6a48c0d3e..57b569765c 100644 --- a/packages/primitives/src/Button.tsx +++ b/packages/primitives/src/Button.tsx @@ -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: { @@ -249,75 +248,39 @@ export const iconButtonRecipe = cva({ }, }); +const StyledButton = styled(ark.button, {}, { baseComponent: true, defaultProps: { type: "button" } }); + type Variant = RecipeVariant["variant"]; type ButtonVariant = Exclude; export type ButtonVariantProps = { variant?: ButtonVariant } & RecipeVariantProps; -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( - ({ 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 ( - - {loading ? loadingContent : children} - - ); - }, -); - -export const Button = forwardRef( - ({ variant, loadingContent, size, css: cssProp, ...props }, ref) => ( - } - css={css.raw( - buttonBaseRecipe.raw({ variant }), - variant !== "link" ? buttonRecipe.raw({ size }) : undefined, - cssProp, - )} - ref={ref} - /> - ), -); +export const Button = forwardRef(({ variant, size, css: cssProp, ...props }, ref) => ( + +)); type IconButtonVariant = Exclude; export type IconButtonVariantProps = { variant?: IconButtonVariant } & RecipeVariantProps; -export type IconButtonProps = BaseButtonProps & IconButtonVariantProps; +export type IconButtonProps = HTMLArkProps<"button"> & JsxStyleProps & IconButtonVariantProps; export const IconButton = forwardRef( - ({ variant, css: cssProp, loadingContent, size, replaceContent = true, ...props }, ref) => ( - ( + } - replaceContent={replaceContent} ref={ref} /> ), diff --git a/packages/primitives/src/IconButton.stories.tsx b/packages/primitives/src/IconButton.stories.tsx index c38a933eee..9da5502922 100644 --- a/packages/primitives/src/IconButton.stories.tsx +++ b/packages/primitives/src/IconButton.stories.tsx @@ -92,23 +92,3 @@ export const Disabled: StoryObj = { disabled: true, }, }; - -export const Loading: StoryObj = { - args: { - loading: true, - }, -}; - -export const LoadingReplace: StoryObj = { - args: { - loading: true, - replaceContent: true, - }, -}; - -export const CustomLoading: StoryObj = { - args: { - loading: true, - loadingContent: "...", - }, -}; From cb3935aaece19dff52aca5b1a06d0339208fb597 Mon Sep 17 00:00:00 2001 From: Katrine Wist Date: Wed, 5 Mar 2025 08:36:00 +0100 Subject: [PATCH 3/3] Add aria-label to Spinner --- packages/ndla-ui/src/i18n/useComponentTranslations.ts | 1 + packages/ndla-ui/src/locale/messages-en.ts | 1 + packages/ndla-ui/src/locale/messages-nb.ts | 1 + packages/ndla-ui/src/locale/messages-nn.ts | 1 + packages/ndla-ui/src/locale/messages-se.ts | 1 + packages/ndla-video-search/src/VideoResultList.tsx | 2 +- packages/ndla-video-search/src/VideoSearch.tsx | 1 + 7 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/ndla-ui/src/i18n/useComponentTranslations.ts b/packages/ndla-ui/src/i18n/useComponentTranslations.ts index 18cb32a800..a05ed111bb 100644 --- a/packages/ndla-ui/src/i18n/useComponentTranslations.ts +++ b/packages/ndla-ui/src/i18n/useComponentTranslations.ts @@ -193,6 +193,7 @@ export const useVideoSearchTranslations = (translations?: Partial )} - {!!isLoading && } + {!!isLoading && } {!!existsMoreVideos && } ); diff --git a/packages/ndla-video-search/src/VideoSearch.tsx b/packages/ndla-video-search/src/VideoSearch.tsx index a58fe43a8a..37a94e8aff 100644 --- a/packages/ndla-video-search/src/VideoSearch.tsx +++ b/packages/ndla-video-search/src/VideoSearch.tsx @@ -22,6 +22,7 @@ export interface VideoTranslations { previewVideo: string; addVideo: string; close: string; + loading: string; } interface Props {