diff --git a/.changeset/brave-groups-learn.md b/.changeset/brave-groups-learn.md new file mode 100644 index 0000000000..773e8eed49 --- /dev/null +++ b/.changeset/brave-groups-learn.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +Added `isPending` prop to Alert, Button, ButtonIcon, Table, and TableRoot as a replacement for the `loading` prop, aligning with React Aria naming conventions. The `loading` prop is now deprecated but still supported as an alias. CSS selectors now use `data-ispending` instead of `data-loading` for styling pending states; `data-loading` is still emitted for backward compatibility but will be removed alongside the `loading` prop. + +**Affected components:** Alert, Button, ButtonIcon, Table, TableRoot diff --git a/docs-ui/src/app/components/alert/components.tsx b/docs-ui/src/app/components/alert/components.tsx index afc5eb654c..2df11d4a3a 100644 --- a/docs-ui/src/app/components/alert/components.tsx +++ b/docs-ui/src/app/components/alert/components.tsx @@ -119,20 +119,20 @@ export const WithActionsAndDescriptions = () => { ); }; -export const LoadingStates = () => { +export const PendingStates = () => { return ( - + diff --git a/docs-ui/src/app/components/alert/page.mdx b/docs-ui/src/app/components/alert/page.mdx index cdfdf184e0..3df813b7e9 100644 --- a/docs-ui/src/app/components/alert/page.mdx +++ b/docs-ui/src/app/components/alert/page.mdx @@ -8,7 +8,7 @@ import { statusVariantsSnippet, withDescriptionSnippet, withActionsSnippet, - loadingStatesSnippet, + pendingStatesSnippet, withoutIconsSnippet, customIconSnippet, } from './snippets'; @@ -17,7 +17,7 @@ import { StatusVariants, WithDescription, WithActions, - LoadingStates, + PendingStates, WithoutIcons, CustomIcon, } from './components'; @@ -79,16 +79,16 @@ Include custom actions like buttons for interactive alerts. code={withActionsSnippet} /> -### Loading States +### Pending State -The loading spinner replaces the icon to indicate an ongoing process. +The pending spinner replaces the icon to indicate an ongoing process. } - code={loadingStatesSnippet} + preview={} + code={pendingStatesSnippet} /> ### Without Icons diff --git a/docs-ui/src/app/components/alert/props-definition.ts b/docs-ui/src/app/components/alert/props-definition.ts index 6f2650fe42..85b6e021c1 100644 --- a/docs-ui/src/app/components/alert/props-definition.ts +++ b/docs-ui/src/app/components/alert/props-definition.ts @@ -10,31 +10,45 @@ export const alertPropDefs: Record = { values: ['info', 'success', 'warning', 'danger'], responsive: true, default: 'info', + description: + 'Visual status of the alert, which determines color and default icon.', }, icon: { type: 'enum', values: ['boolean', 'React.ReactElement'], responsive: false, + description: + 'Set to true to show the default status icon, or pass a custom icon element. Set to false to hide the icon.', + }, + isPending: { + type: 'boolean', + default: 'false', + description: + 'Replaces the icon with a spinner to indicate a pending operation.', }, loading: { - type: 'enum', - values: ['boolean'], - responsive: false, + type: 'boolean', + default: 'false', + description: 'Deprecated. Use `isPending` instead.', }, title: { type: 'enum', values: ['React.ReactNode'], responsive: false, + description: 'Primary message displayed in the alert.', }, description: { type: 'enum', values: ['React.ReactNode'], responsive: false, + description: 'Additional detail shown below the title.', }, customActions: { type: 'enum', values: ['React.ReactNode'], responsive: false, + description: + 'Custom action buttons displayed on the right side of the alert.', }, m: { type: 'enum', diff --git a/docs-ui/src/app/components/alert/snippets.ts b/docs-ui/src/app/components/alert/snippets.ts index 739dfde9db..aa4f05ed1c 100644 --- a/docs-ui/src/app/components/alert/snippets.ts +++ b/docs-ui/src/app/components/alert/snippets.ts @@ -97,13 +97,13 @@ export const withActionsAndDescriptionsSnippet = ``; -export const loadingStatesSnippet = ` - - +export const pendingStatesSnippet = ` + + diff --git a/docs-ui/src/app/components/button-icon/components.tsx b/docs-ui/src/app/components/button-icon/components.tsx index 3ed421289c..ba376549d2 100644 --- a/docs-ui/src/app/components/button-icon/components.tsx +++ b/docs-ui/src/app/components/button-icon/components.tsx @@ -56,12 +56,12 @@ export const Disabled = () => { ); }; -export const Loading = () => { +export const Pending = () => { return ( } variant="primary" - loading + isPending aria-label="Cloud" /> ); diff --git a/docs-ui/src/app/components/button-icon/page.mdx b/docs-ui/src/app/components/button-icon/page.mdx index 51ff0ea5a0..6f4678d29c 100644 --- a/docs-ui/src/app/components/button-icon/page.mdx +++ b/docs-ui/src/app/components/button-icon/page.mdx @@ -7,9 +7,9 @@ import { variantsSnippet, sizesSnippet, disabledSnippet, - loadingSnippet, + isPendingSnippet, } from './snippets'; -import { Variants, Sizes, Disabled, Loading } from './components'; +import { Variants, Sizes, Disabled, Pending } from './components'; import { PageTitle } from '@/components/PageTitle'; import { Theming } from '@/components/Theming'; import { ButtonIconDefinition } from '../../../utils/definitions'; @@ -63,7 +63,7 @@ export const reactAriaUrls = { code={disabledSnippet} /> -### Loading +### Pending Shows a spinner during async operations. @@ -71,8 +71,8 @@ Shows a spinner during async operations. align="center" py={4} open - preview={} - code={loadingSnippet} + preview={} + code={isPendingSnippet} /> diff --git a/docs-ui/src/app/components/button-icon/props-definition.tsx b/docs-ui/src/app/components/button-icon/props-definition.tsx index 05917cc2b4..d0b9a5717c 100644 --- a/docs-ui/src/app/components/button-icon/props-definition.tsx +++ b/docs-ui/src/app/components/button-icon/props-definition.tsx @@ -42,11 +42,16 @@ export const buttonIconPropDefs: Record = { default: 'false', description: 'Prevents interaction and applies disabled styling.', }, - loading: { + isPending: { type: 'boolean', default: 'false', description: 'Shows a spinner and disables the button.', }, + loading: { + type: 'boolean', + default: 'false', + description: 'Deprecated. Use `isPending` instead.', + }, type: { type: 'enum', values: ['button', 'submit', 'reset'], diff --git a/docs-ui/src/app/components/button-icon/snippets.ts b/docs-ui/src/app/components/button-icon/snippets.ts index b1ab27b77b..46e2eb2ab8 100644 --- a/docs-ui/src/app/components/button-icon/snippets.ts +++ b/docs-ui/src/app/components/button-icon/snippets.ts @@ -20,4 +20,4 @@ export const disabledSnippet = ` } variant="tertiary" aria-label="Cloud" /> `; -export const loadingSnippet = `} variant="primary" loading aria-label="Cloud" />`; +export const isPendingSnippet = `} variant="primary" isPending aria-label="Cloud" />`; diff --git a/docs-ui/src/app/components/button/components.tsx b/docs-ui/src/app/components/button/components.tsx index 1fe13f6630..5829bd978e 100644 --- a/docs-ui/src/app/components/button/components.tsx +++ b/docs-ui/src/app/components/button/components.tsx @@ -75,9 +75,9 @@ export const Destructive = () => { ); }; -export const Loading = () => { +export const Pending = () => { return ( - ); diff --git a/docs-ui/src/app/components/button/page.mdx b/docs-ui/src/app/components/button/page.mdx index 6434a7f4a4..8183f8ca6c 100644 --- a/docs-ui/src/app/components/button/page.mdx +++ b/docs-ui/src/app/components/button/page.mdx @@ -8,7 +8,7 @@ import { withIconsSnippet, disabledSnippet, destructiveSnippet, - loadingSnippet, + isPendingSnippet, asLinkSnippet, buttonSnippetUsage, buttonResponsiveSnippet, @@ -24,7 +24,7 @@ import { WithIcons, Disabled, Destructive, - Loading, + Pending, AsLink, } from './components'; @@ -34,7 +34,7 @@ export const reactAriaUrls = { } code={variantsSnippet} /> @@ -110,7 +110,7 @@ Use the `destructive` prop for dangerous actions like delete or remove. layout="side-by-side" /> -### Loading +### Pending Shows a spinner and disables interaction during async operations. @@ -118,8 +118,8 @@ Shows a spinner and disables interaction during async operations. align="center" py={4} open - preview={} - code={loadingSnippet} + preview={} + code={isPendingSnippet} layout="side-by-side" /> diff --git a/docs-ui/src/app/components/button/props-definition.tsx b/docs-ui/src/app/components/button/props-definition.tsx index 0f94fcffaf..3c82a59bb4 100644 --- a/docs-ui/src/app/components/button/props-definition.tsx +++ b/docs-ui/src/app/components/button/props-definition.tsx @@ -48,11 +48,16 @@ export const buttonPropDefs: Record = { default: 'false', description: 'Prevents interaction and applies disabled styling.', }, - loading: { + isPending: { type: 'boolean', default: 'false', description: 'Shows a spinner and disables the button.', }, + loading: { + type: 'boolean', + default: 'false', + description: 'Deprecated. Use `isPending` instead.', + }, children: { type: 'enum', values: ['ReactNode'], diff --git a/docs-ui/src/app/components/button/snippets.ts b/docs-ui/src/app/components/button/snippets.ts index 6b8aba7e26..2af42df2a7 100644 --- a/docs-ui/src/app/components/button/snippets.ts +++ b/docs-ui/src/app/components/button/snippets.ts @@ -55,7 +55,7 @@ export const destructiveSnippet = ` `; -export const loadingSnippet = ``; diff --git a/docs-ui/src/app/components/table/page.mdx b/docs-ui/src/app/components/table/page.mdx index d3ed0d5350..3bc6e0cd24 100644 --- a/docs-ui/src/app/components/table/page.mdx +++ b/docs-ui/src/app/components/table/page.mdx @@ -202,11 +202,11 @@ Use `mode: 'cursor'` when your API uses cursor-based pagination (common with Gra -### Loading States +### Pending States -When fetching data, the table shows a loading state. If the user triggers a new query (by paginating, sorting, or searching) while previous data is displayed, the table enters a "stale" state where it continues showing the previous data until new data arrives. This prevents jarring layout shifts. +When fetching data, the table shows a pending state. If the user triggers a new query (by paginating, sorting, or searching) while previous data is displayed, the table enters a "stale" state where it continues showing the previous data until new data arrives. This prevents jarring layout shifts. -You can access these states via `tableProps.loading` and `tableProps.isStale` if you need to render additional loading indicators. +You can access these states via `tableProps.isPending` and `tableProps.isStale` if you need to render additional pending indicators. ## Combining Features diff --git a/docs-ui/src/app/components/table/props-definition.tsx b/docs-ui/src/app/components/table/props-definition.tsx index 30381e30f7..fcec3a6278 100644 --- a/docs-ui/src/app/components/table/props-definition.tsx +++ b/docs-ui/src/app/components/table/props-definition.tsx @@ -166,7 +166,7 @@ export const useTableReturnPropDefs: Record = { description: ( <> Props to spread onto the Table component. Includes data, - loading, error, pagination, and sort state. + isPending, error, pagination, and sort state. ), }, @@ -207,10 +207,15 @@ export const tablePropDefs: Record = { values: ['T[]'], description: 'Array of data items to display in the table.', }, + isPending: { + type: 'boolean', + default: 'false', + description: 'Whether the table is in a pending state.', + }, loading: { type: 'boolean', default: 'false', - description: 'Whether the table is in a loading state.', + description: 'Deprecated. Use `isPending` instead.', }, isStale: { type: 'boolean', @@ -466,17 +471,22 @@ export const tableRootPropDefs: Record = { ), }, - loading: { + isPending: { type: 'boolean', default: 'false', description: ( <> - Whether the table is in a loading state (e.g., initial data fetch). Adds{' '} - aria-busy attribute and data-loading data + Whether the table is in a pending state (e.g., initial data fetch). Adds{' '} + aria-busy attribute and data-ispending data attribute for styling. ), }, + loading: { + type: 'boolean', + default: 'false', + description: 'Deprecated. Use `isPending` instead.', + }, }; export const columnPropDefs: Record = { diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index b3cdc5ced4..c48382dede 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -222,6 +222,9 @@ export const AlertDefinition: { readonly dataAttribute: true; readonly default: 'info'; }; + readonly isPending: { + readonly dataAttribute: true; + }; readonly loading: { readonly dataAttribute: true; }; @@ -239,6 +242,7 @@ export const AlertDefinition: { export type AlertOwnProps = { status?: Responsive<'info' | 'success' | 'warning' | 'danger'>; icon?: boolean | ReactElement; + isPending?: boolean; loading?: boolean; customActions?: ReactNode; title?: ReactNode; @@ -514,6 +518,9 @@ export const ButtonDefinition: { readonly destructive: { readonly dataAttribute: true; }; + readonly isPending: { + readonly dataAttribute: true; + }; readonly loading: { readonly dataAttribute: true; }; @@ -549,6 +556,9 @@ export const ButtonIconDefinition: { readonly dataAttribute: true; readonly default: 'primary'; }; + readonly isPending: { + readonly dataAttribute: true; + }; readonly loading: { readonly dataAttribute: true; }; @@ -562,6 +572,7 @@ export type ButtonIconOwnProps = { size?: Responsive<'small' | 'medium'>; variant?: Responsive<'primary' | 'secondary' | 'tertiary'>; icon?: ReactElement; + isPending?: boolean; loading?: boolean; className?: string; }; @@ -627,6 +638,7 @@ export type ButtonOwnProps = { destructive?: boolean; iconStart?: ReactElement; iconEnd?: ReactElement; + isPending?: boolean; loading?: boolean; children?: ReactNode; className?: string; @@ -2741,6 +2753,9 @@ export const TableDefinition: { readonly stale: { readonly dataAttribute: true; }; + readonly isPending: { + readonly dataAttribute: true; + }; readonly loading: { readonly dataAttribute: true; }; @@ -2844,8 +2859,10 @@ export interface TableProps { // (undocumented) error?: Error; // (undocumented) - isStale?: boolean; + isPending?: boolean; // (undocumented) + isStale?: boolean; + // @deprecated (undocumented) loading?: boolean; // (undocumented) pagination: TablePaginationType; @@ -2867,6 +2884,7 @@ export const TableRoot: (props: TableRootProps) => JSX_2.Element; // @public (undocumented) export type TableRootOwnProps = { stale?: boolean; + isPending?: boolean; loading?: boolean; }; diff --git a/packages/ui/src/components/Alert/Alert.stories.tsx b/packages/ui/src/components/Alert/Alert.stories.tsx index 589d55b7dd..486daad2ef 100644 --- a/packages/ui/src/components/Alert/Alert.stories.tsx +++ b/packages/ui/src/components/Alert/Alert.stories.tsx @@ -32,7 +32,7 @@ const meta = preview.meta({ icon: { control: 'boolean', }, - loading: { + isPending: { control: 'boolean', }, }, @@ -208,25 +208,25 @@ export const WithActionsAndDescriptions = WithActions.extend({ }, }); -export const LoadingVariants = meta.story({ +export const PendingVariants = meta.story({ render: () => ( Info Success - + Warning @@ -234,27 +234,27 @@ export const LoadingVariants = meta.story({ ), }); -export const LoadingWithDescription = meta.story({ +export const PendingWithDescription = meta.story({ render: () => ( diff --git a/packages/ui/src/components/Alert/Alert.tsx b/packages/ui/src/components/Alert/Alert.tsx index 54eb9cffae..ef08528cc4 100644 --- a/packages/ui/src/components/Alert/Alert.tsx +++ b/packages/ui/src/components/Alert/Alert.tsx @@ -32,7 +32,7 @@ import { AlertDefinition } from './definition'; * * @remarks * The Alert component supports multiple status variants (info, success, warning, danger) - * and can display icons, loading states, and custom actions. It automatically handles + * and can display icons, pending states, and custom actions. It automatically handles * icon selection based on status when the icon prop is set to true. * * @example @@ -53,14 +53,14 @@ import { AlertDefinition } from './definition'; * ``` * * @example - * With custom actions and loading state: + * With custom actions and pending state: * ```tsx * * @@ -76,13 +76,21 @@ export const Alert = forwardRef( (props: AlertProps, ref: Ref) => { const { ownProps, restProps, dataAttributes, utilityStyle } = useDefinition( AlertDefinition, - props, + // Merge deprecated `loading` into `isPending` so data attributes and + // internal logic only need to check a single prop. + { + ...props, + isPending: + props.isPending || props.loading + ? true + : props.isPending ?? props.loading, + }, ); const { classes, status, icon, - loading, + isPending, customActions, title, description, @@ -132,7 +140,7 @@ export const Alert = forwardRef( data-has-description={description ? 'true' : 'false'} >
- {loading ? ( + {isPending ? (
()({ }, propDefs: { status: { dataAttribute: true, default: 'info' }, + isPending: { dataAttribute: true }, loading: { dataAttribute: true }, icon: {}, customActions: {}, diff --git a/packages/ui/src/components/Alert/types.ts b/packages/ui/src/components/Alert/types.ts index ce36d4c5fd..7018f25f6b 100644 --- a/packages/ui/src/components/Alert/types.ts +++ b/packages/ui/src/components/Alert/types.ts @@ -21,6 +21,8 @@ import type { Responsive, MarginProps } from '../../types'; export type AlertOwnProps = { status?: Responsive<'info' | 'success' | 'warning' | 'danger'>; icon?: boolean | ReactElement; + isPending?: boolean; + /** @deprecated Use `isPending` instead. */ loading?: boolean; customActions?: ReactNode; title?: ReactNode; diff --git a/packages/ui/src/components/Button/Button.module.css b/packages/ui/src/components/Button/Button.module.css index 8273e77d5a..ffffaf52f0 100644 --- a/packages/ui/src/components/Button/Button.module.css +++ b/packages/ui/src/components/Button/Button.module.css @@ -54,7 +54,7 @@ cursor: not-allowed; } - &[data-loading='true'] { + &[data-ispending='true'] { cursor: wait; } } @@ -66,7 +66,7 @@ --fg: var(--bui-fg-solid); &[data-disabled='true'], - &[data-loading='true'] { + &[data-ispending='true'] { --bg: var(--bui-bg-solid-disabled); --bg-hover: var(--bui-bg-solid-disabled); --bg-active: var(--bui-bg-solid-disabled); @@ -100,7 +100,7 @@ --fg: var(--fg-solid-danger); &[data-disabled='true'], - &[data-loading='true'] { + &[data-ispending='true'] { --bg: var(--bg-solid-danger-disabled); --bg-hover: var(--bg-solid-danger-disabled); --bg-active: var(--bg-solid-danger-disabled); @@ -137,7 +137,7 @@ } &[data-disabled='true'], - &[data-loading='true'] { + &[data-ispending='true'] { --bg-hover: var(--bg); --bg-active: var(--bg); --fg: var(--bui-fg-disabled); @@ -166,7 +166,7 @@ --fg: var(--bui-fg-danger); &[data-disabled='true'], - &[data-loading='true'] { + &[data-ispending='true'] { --bg-hover: var(--bg); --bg-active: var(--bg); --fg: var(--bui-fg-disabled); @@ -198,7 +198,7 @@ } &[data-disabled='true'], - &[data-loading='true'] { + &[data-ispending='true'] { --bg-hover: var(--bg); --bg-active: var(--bg); --fg: var(--bui-fg-disabled); @@ -226,7 +226,7 @@ --fg: var(--bui-fg-danger); &[data-disabled='true'], - &[data-loading='true'] { + &[data-ispending='true'] { --bg-hover: var(--bg); --bg-active: var(--bg); --fg: var(--bui-fg-disabled); @@ -268,7 +268,7 @@ width: 100%; transition: opacity var(--loading-duration) ease-out; - .bui-Button[data-loading='true'] & { + .bui-Button[data-ispending='true'] & { opacity: 0; } } @@ -282,7 +282,7 @@ opacity: 0; transition: opacity var(--loading-duration) ease-in; - .bui-Button[data-loading='true'] & { + .bui-Button[data-ispending='true'] & { opacity: 1; } diff --git a/packages/ui/src/components/Button/Button.stories.tsx b/packages/ui/src/components/Button/Button.stories.tsx index 5781daca6a..3eec3ce36d 100644 --- a/packages/ui/src/components/Button/Button.stories.tsx +++ b/packages/ui/src/components/Button/Button.stories.tsx @@ -107,7 +107,7 @@ export const Destructive = meta.story({ - @@ -124,7 +124,7 @@ export const Destructive = meta.story({ - @@ -141,7 +141,7 @@ export const Destructive = meta.story({ - @@ -254,94 +254,94 @@ export const Responsive = meta.story({ }, }); -export const Loading = meta.story({ +export const Pending = meta.story({ render: () => { - const [isLoading, setIsLoading] = useState(false); + const [isPending, setIsPending] = useState(false); const handleClick = () => { - setIsLoading(true); + setIsPending(true); setTimeout(() => { - setIsLoading(false); + setIsPending(false); }, 3000); }; return ( - ); }, }); -export const LoadingVariants = meta.story({ +export const PendingVariants = meta.story({ render: () => ( Primary - - - Secondary - - - Tertiary - - - Primary Destructive - - - Loading vs Disabled + Pending vs Disabled - - diff --git a/packages/ui/src/components/Button/Button.tsx b/packages/ui/src/components/Button/Button.tsx index 069c8632e0..92675a4f50 100644 --- a/packages/ui/src/components/Button/Button.tsx +++ b/packages/ui/src/components/Button/Button.tsx @@ -43,7 +43,7 @@ import { ButtonDefinition } from './definition'; * variant="primary" * size="medium" * iconStart={} - * loading={isSubmitting} + * isPending={isSubmitting} * > * Submit * @@ -55,15 +55,23 @@ export const Button = forwardRef( (props: ButtonProps, ref: Ref) => { const { ownProps, restProps, dataAttributes } = useDefinition( ButtonDefinition, - props, + // Merge deprecated `loading` into `isPending` so data attributes and + // internal logic only need to check a single prop. + { + ...props, + isPending: + props.isPending || props.loading + ? true + : props.isPending ?? props.loading, + }, ); - const { classes, iconStart, iconEnd, loading, children } = ownProps; + const { classes, iconStart, iconEnd, isPending, children } = ownProps; return ( diff --git a/packages/ui/src/components/Button/definition.ts b/packages/ui/src/components/Button/definition.ts index 21d3c7017b..891e21360d 100644 --- a/packages/ui/src/components/Button/definition.ts +++ b/packages/ui/src/components/Button/definition.ts @@ -34,6 +34,7 @@ export const ButtonDefinition = defineComponent()({ size: { dataAttribute: true, default: 'small' }, variant: { dataAttribute: true, default: 'primary' }, destructive: { dataAttribute: true }, + isPending: { dataAttribute: true }, loading: { dataAttribute: true }, iconStart: {}, iconEnd: {}, diff --git a/packages/ui/src/components/Button/types.ts b/packages/ui/src/components/Button/types.ts index 2bf36d808e..4f7ed8c50b 100644 --- a/packages/ui/src/components/Button/types.ts +++ b/packages/ui/src/components/Button/types.ts @@ -25,6 +25,8 @@ export type ButtonOwnProps = { destructive?: boolean; iconStart?: ReactElement; iconEnd?: ReactElement; + isPending?: boolean; + /** @deprecated Use `isPending` instead. */ loading?: boolean; children?: ReactNode; className?: string; diff --git a/packages/ui/src/components/ButtonIcon/ButtonIcon.module.css b/packages/ui/src/components/ButtonIcon/ButtonIcon.module.css index 8a345647c5..df4584cdea 100644 --- a/packages/ui/src/components/ButtonIcon/ButtonIcon.module.css +++ b/packages/ui/src/components/ButtonIcon/ButtonIcon.module.css @@ -54,7 +54,7 @@ cursor: not-allowed; } - &[data-loading='true'] { + &[data-ispending='true'] { cursor: wait; } } @@ -66,7 +66,7 @@ --fg: var(--bui-fg-solid); &[data-disabled='true'], - &[data-loading='true'] { + &[data-ispending='true'] { --bg: var(--bui-bg-solid-disabled); --bg-hover: var(--bui-bg-solid-disabled); --bg-active: var(--bui-bg-solid-disabled); @@ -104,7 +104,7 @@ } &[data-disabled='true'], - &[data-loading='true'] { + &[data-ispending='true'] { --bg-hover: var(--bg); --bg-active: var(--bg); --fg: var(--bui-fg-disabled); @@ -138,7 +138,7 @@ } &[data-disabled='true'], - &[data-loading='true'] { + &[data-ispending='true'] { --bg-hover: var(--bg); --bg-active: var(--bg); --fg: var(--bui-fg-disabled); @@ -179,7 +179,7 @@ width: 100%; transition: opacity var(--loading-duration) ease-out; - .bui-ButtonIcon[data-loading='true'] & { + .bui-ButtonIcon[data-ispending='true'] & { opacity: 0; } } @@ -193,7 +193,7 @@ opacity: 0; transition: opacity var(--loading-duration) ease-in; - .bui-ButtonIcon[data-loading='true'] & { + .bui-ButtonIcon[data-ispending='true'] & { opacity: 1; } diff --git a/packages/ui/src/components/ButtonIcon/ButtonIcon.stories.tsx b/packages/ui/src/components/ButtonIcon/ButtonIcon.stories.tsx index be6dad7754..75d95fdc1f 100644 --- a/packages/ui/src/components/ButtonIcon/ButtonIcon.stories.tsx +++ b/packages/ui/src/components/ButtonIcon/ButtonIcon.stories.tsx @@ -82,14 +82,14 @@ export const Responsive = meta.story({ render: args => } />, }); -export const Loading = meta.story({ +export const Pending = meta.story({ render: () => { - const [isLoading, setIsLoading] = useState(false); + const [isPending, setIsPending] = useState(false); const handleClick = () => { - setIsLoading(true); + setIsPending(true); setTimeout(() => { - setIsLoading(false); + setIsPending(false); }, 3000); }; @@ -97,14 +97,14 @@ export const Loading = meta.story({ } - loading={isLoading} + isPending={isPending} onPress={handleClick} /> ); }, }); -export const LoadingVariants = meta.story({ +export const PendingVariants = meta.story({ render: () => ( Primary @@ -113,13 +113,13 @@ export const LoadingVariants = meta.story({ variant="primary" size="small" icon={} - loading + isPending /> } - loading + isPending /> @@ -129,13 +129,13 @@ export const LoadingVariants = meta.story({ variant="secondary" size="small" icon={} - loading + isPending /> } - loading + isPending /> @@ -145,24 +145,24 @@ export const LoadingVariants = meta.story({ variant="tertiary" size="small" icon={} - loading + isPending /> } - loading + isPending /> - Loading vs Disabled + Pending vs Disabled - } loading /> + } isPending /> } isDisabled /> } - loading + isPending isDisabled /> diff --git a/packages/ui/src/components/ButtonIcon/ButtonIcon.tsx b/packages/ui/src/components/ButtonIcon/ButtonIcon.tsx index e5ac723f42..9aebc18d08 100644 --- a/packages/ui/src/components/ButtonIcon/ButtonIcon.tsx +++ b/packages/ui/src/components/ButtonIcon/ButtonIcon.tsx @@ -30,15 +30,23 @@ export const ButtonIcon = forwardRef( (props: ButtonIconProps, ref: Ref) => { const { ownProps, restProps, dataAttributes } = useDefinition( ButtonIconDefinition, - props, + // Merge deprecated `loading` into `isPending` so data attributes and + // internal logic only need to check a single prop. + { + ...props, + isPending: + props.isPending || props.loading + ? true + : props.isPending ?? props.loading, + }, ); - const { classes, icon, loading } = ownProps; + const { classes, icon, isPending } = ownProps; return ( diff --git a/packages/ui/src/components/ButtonIcon/definition.ts b/packages/ui/src/components/ButtonIcon/definition.ts index 0027227094..6c065188d8 100644 --- a/packages/ui/src/components/ButtonIcon/definition.ts +++ b/packages/ui/src/components/ButtonIcon/definition.ts @@ -33,6 +33,7 @@ export const ButtonIconDefinition = defineComponent()({ propDefs: { size: { dataAttribute: true, default: 'small' }, variant: { dataAttribute: true, default: 'primary' }, + isPending: { dataAttribute: true }, loading: { dataAttribute: true }, icon: {}, className: {}, diff --git a/packages/ui/src/components/ButtonIcon/types.ts b/packages/ui/src/components/ButtonIcon/types.ts index f30f01ff99..69ebcc21ec 100644 --- a/packages/ui/src/components/ButtonIcon/types.ts +++ b/packages/ui/src/components/ButtonIcon/types.ts @@ -23,6 +23,8 @@ export type ButtonIconOwnProps = { size?: Responsive<'small' | 'medium'>; variant?: Responsive<'primary' | 'secondary' | 'tertiary'>; icon?: ReactElement; + isPending?: boolean; + /** @deprecated Use `isPending` instead. */ loading?: boolean; className?: string; }; diff --git a/packages/ui/src/components/Table/Table.module.css b/packages/ui/src/components/Table/Table.module.css index 953ccbed65..969d27b805 100644 --- a/packages/ui/src/components/Table/Table.module.css +++ b/packages/ui/src/components/Table/Table.module.css @@ -41,7 +41,7 @@ min-height: 0; &[data-stale='true'], - &[data-loading='true'] { + &[data-ispending='true'] { opacity: 0.6; } } diff --git a/packages/ui/src/components/Table/components/Table.tsx b/packages/ui/src/components/Table/components/Table.tsx index 450e40d281..24fd2a335e 100644 --- a/packages/ui/src/components/Table/components/Table.tsx +++ b/packages/ui/src/components/Table/components/Table.tsx @@ -107,6 +107,7 @@ function useLiveRegionLabel( export function Table({ columnConfig, data, + isPending = false, loading = false, isStale = false, error, @@ -119,6 +120,7 @@ export function Table({ style, virtualized, }: TableProps) { + const pending = isPending || loading; const { ownProps: { classes }, } = useDefinition(TableWrapperDefinition, { className }); @@ -137,7 +139,7 @@ export function Table({ onSelectionChange, } = selection || {}; - const isInitialLoading = loading && !data; + const isInitialLoading = pending && !data; if (error) { return ( @@ -202,7 +204,7 @@ export function Table({ onSortChange={sort?.onSortChange} disabledKeys={disabledRows} stale={isStale} - loading={isInitialLoading} + isPending={isInitialLoading} aria-describedby={liveRegionId} > diff --git a/packages/ui/src/components/Table/components/TableRoot.tsx b/packages/ui/src/components/Table/components/TableRoot.tsx index 5ea21046df..47c18b650b 100644 --- a/packages/ui/src/components/Table/components/TableRoot.tsx +++ b/packages/ui/src/components/Table/components/TableRoot.tsx @@ -28,14 +28,22 @@ import { TableRootProps } from '../types'; export const TableRoot = (props: TableRootProps) => { const { ownProps, restProps, dataAttributes } = useDefinition( TableDefinition, - props, + // Merge deprecated `loading` into `isPending` so data attributes and + // internal logic only need to check a single prop. + { + ...props, + isPending: + props.isPending || props.loading + ? true + : props.isPending ?? props.loading, + }, ); return ( diff --git a/packages/ui/src/components/Table/definition.ts b/packages/ui/src/components/Table/definition.ts index e2236e21b7..95e8e8635b 100644 --- a/packages/ui/src/components/Table/definition.ts +++ b/packages/ui/src/components/Table/definition.ts @@ -52,6 +52,7 @@ export const TableDefinition = defineComponent()({ }, propDefs: { stale: { dataAttribute: true }, + isPending: { dataAttribute: true }, loading: { dataAttribute: true }, }, }); diff --git a/packages/ui/src/components/Table/hooks/types.ts b/packages/ui/src/components/Table/hooks/types.ts index 06c980efdf..01cc8ac02e 100644 --- a/packages/ui/src/components/Table/hooks/types.ts +++ b/packages/ui/src/components/Table/hooks/types.ts @@ -158,7 +158,7 @@ export interface UseTableResult { /** @internal */ export interface PaginationResult { data: T[] | undefined; - loading: boolean; + isPending: boolean; error: Error | undefined; totalCount: number | undefined; offset?: number; diff --git a/packages/ui/src/components/Table/hooks/useCompletePagination.ts b/packages/ui/src/components/Table/hooks/useCompletePagination.ts index f34e2c8c06..22711511d4 100644 --- a/packages/ui/src/components/Table/hooks/useCompletePagination.ts +++ b/packages/ui/src/components/Table/hooks/useCompletePagination.ts @@ -48,7 +48,7 @@ export function useCompletePagination( const { sort, filter, search } = query; const [items, setItems] = useState(undefined); - const [isLoading, setIsLoading] = useState(!data); + const [isPending, setIsPending] = useState(!data); const [error, setError] = useState(undefined); const [loadCount, setLoadCount] = useState(0); @@ -64,7 +64,7 @@ export function useCompletePagination( // Load data on mount and when loadCount changes (reload trigger) useEffect(() => { if (data) { - setIsLoading(false); + setIsPending(false); return; } @@ -73,7 +73,7 @@ export function useCompletePagination( } let cancelled = false; - setIsLoading(true); + setIsPending(true); setError(undefined); (async () => { @@ -82,12 +82,12 @@ export function useCompletePagination( const resolvedData = result instanceof Promise ? await result : result; if (!cancelled) { setItems(resolvedData); - setIsLoading(false); + setIsPending(false); } } catch (err) { if (!cancelled) { setError(err instanceof Error ? err : new Error(String(err))); - setIsLoading(false); + setIsPending(false); } } })(); @@ -164,7 +164,7 @@ export function useCompletePagination( return { data: paginatedData, - loading: isLoading, + isPending: isPending, error, totalCount, offset, diff --git a/packages/ui/src/components/Table/hooks/useCursorPagination.ts b/packages/ui/src/components/Table/hooks/useCursorPagination.ts index 5da56b3ce4..e7ce5b1eb5 100644 --- a/packages/ui/src/components/Table/hooks/useCursorPagination.ts +++ b/packages/ui/src/components/Table/hooks/useCursorPagination.ts @@ -78,7 +78,7 @@ export function useCursorPagination( return { data: cache.data, - loading: cache.loading, + isPending: cache.isPending, error: cache.error, totalCount: cache.totalCount, offset: undefined, diff --git a/packages/ui/src/components/Table/hooks/useOffsetPagination.ts b/packages/ui/src/components/Table/hooks/useOffsetPagination.ts index b34dbe8a6c..dd92f98167 100644 --- a/packages/ui/src/components/Table/hooks/useOffsetPagination.ts +++ b/packages/ui/src/components/Table/hooks/useOffsetPagination.ts @@ -91,7 +91,7 @@ export function useOffsetPagination( return { data: cache.data, - loading: cache.loading, + isPending: cache.isPending, error: cache.error, totalCount: cache.totalCount, offset: cache.currentCursor ?? 0, diff --git a/packages/ui/src/components/Table/hooks/usePageCache.ts b/packages/ui/src/components/Table/hooks/usePageCache.ts index 9bf864749b..7dc490c6c4 100644 --- a/packages/ui/src/components/Table/hooks/usePageCache.ts +++ b/packages/ui/src/components/Table/hooks/usePageCache.ts @@ -48,7 +48,7 @@ export interface UsePageCacheOptions { /** @internal */ export interface UsePageCacheResult { - loading: boolean; + isPending: boolean; error: Error | undefined; data: T[] | undefined; totalCount: number | undefined; @@ -149,7 +149,7 @@ export function usePageCache( const cacheStore = useRef(new PageCacheStore()).current; - const [loading, setLoading] = useState(true); + const [isPending, setIsPending] = useState(true); const [error, setError] = useState(undefined); const [totalCount, setTotalCount] = useState(undefined); @@ -189,7 +189,7 @@ export function usePageCache( const abortController = new AbortController(); abortControllerRef.current = abortController; - setLoading(true); + setIsPending(true); setError(undefined); try { @@ -215,14 +215,14 @@ export function usePageCache( setTotalCount(result.totalCount); } - setLoading(false); + setIsPending(false); } catch (err) { if (abortController.signal.aborted) { return; } setError(err instanceof Error ? err : new Error(String(err))); - setLoading(false); + setIsPending(false); } }, [getData, initialCurrentCursor, currentCursor, cacheStore], @@ -239,18 +239,18 @@ export function usePageCache( }, []); const onNextPage = useCallback(() => { - if (loading) return; + if (isPending) return; const page = cacheStore.get(currentCursor); if (!page?.nextCursor) return; goToPage('next'); - }, [loading, currentCursor, goToPage, cacheStore]); + }, [isPending, currentCursor, goToPage, cacheStore]); const onPreviousPage = useCallback(() => { - if (loading) return; + if (isPending) return; const page = cacheStore.get(currentCursor); if (!page?.prevCursor) return; goToPage('prev'); - }, [loading, currentCursor, goToPage, cacheStore]); + }, [isPending, currentCursor, goToPage, cacheStore]); const reload = useCallback( (reloadOptions?: { keepCurrentCursor?: boolean }) => { @@ -266,7 +266,7 @@ export function usePageCache( ); return { - loading, + isPending, error, data, totalCount, diff --git a/packages/ui/src/components/Table/hooks/useTable.ts b/packages/ui/src/components/Table/hooks/useTable.ts index 7d1019f541..492ac7a0c6 100644 --- a/packages/ui/src/components/Table/hooks/useTable.ts +++ b/packages/ui/src/components/Table/hooks/useTable.ts @@ -52,7 +52,7 @@ function useTableProps( } const displayData = paginationResult.data ?? previousDataRef.current; - const isStale = paginationResult.loading && displayData !== undefined; + const isStale = paginationResult.isPending && displayData !== undefined; const pagination = useMemo(() => { if (paginationOptions.type === 'none') { @@ -104,7 +104,8 @@ function useTableProps( return useMemo( () => ({ data: displayData, - loading: paginationResult.loading, + isPending: paginationResult.isPending, + loading: paginationResult.isPending, isStale, error: paginationResult.error, pagination, @@ -112,7 +113,7 @@ function useTableProps( }), [ displayData, - paginationResult.loading, + paginationResult.isPending, isStale, paginationResult.error, pagination, diff --git a/packages/ui/src/components/Table/stories/Table.visual.stories.tsx b/packages/ui/src/components/Table/stories/Table.visual.stories.tsx index 23810660f1..fd3bbe8218 100644 --- a/packages/ui/src/components/Table/stories/Table.visual.stories.tsx +++ b/packages/ui/src/components/Table/stories/Table.visual.stories.tsx @@ -274,7 +274,7 @@ export const LoadingState: Story = { ); diff --git a/packages/ui/src/components/Table/types.ts b/packages/ui/src/components/Table/types.ts index 800a56052a..20261bda59 100644 --- a/packages/ui/src/components/Table/types.ts +++ b/packages/ui/src/components/Table/types.ts @@ -42,6 +42,8 @@ export interface SortState { /** @public */ export type TableRootOwnProps = { stale?: boolean; + isPending?: boolean; + /** @deprecated Use `isPending` instead. */ loading?: boolean; }; @@ -263,6 +265,8 @@ export type VirtualizedProp = export interface TableProps { columnConfig: readonly ColumnConfig[]; data: T[] | undefined; + isPending?: boolean; + /** @deprecated Use `isPending` instead. */ loading?: boolean; isStale?: boolean; error?: Error;