feat(ui): add isPending prop and deprecate loading

Add `isPending` prop to Alert, Button, ButtonIcon, Table, and
TableRoot, aligning with React Aria naming conventions. The
`loading` prop is deprecated but remains functional as an alias.

CSS selectors now target `data-ispending` instead of `data-loading`
for pending state styling. The `data-loading` attribute is still
emitted for backward compatibility.

Internal Table hooks (`PaginationResult`, `UsePageCacheResult`)
renamed `loading` to `isPending`. The `useTable` hook returns both
`isPending` and `loading` on `tableProps` to preserve backward
compatibility.

Updated docs-ui documentation and stories accordingly.

Signed-off-by: Johan Persson <johanopersson@gmail.com>
This commit is contained in:
Johan Persson
2026-04-17 16:20:38 +02:00
parent 7192e84cad
commit e8a1a35714
42 changed files with 257 additions and 149 deletions
+7
View File
@@ -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
@@ -119,20 +119,20 @@ export const WithActionsAndDescriptions = () => {
);
};
export const LoadingStates = () => {
export const PendingStates = () => {
return (
<Flex direction="column" gap="4">
<Alert
status="info"
icon={true}
loading
isPending
title="Processing your request..."
/>
<Alert status="success" icon={true} loading title="Saving changes..." />
<Alert status="success" icon={true} isPending title="Saving changes..." />
<Alert
status="info"
icon={true}
loading
isPending
title="Processing your request"
description="This may take a few moments. Please do not close this window."
/>
+6 -6
View File
@@ -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.
<Snippet
align="center"
py={4}
open
preview={<LoadingStates />}
code={loadingStatesSnippet}
preview={<PendingStates />}
code={pendingStatesSnippet}
/>
### Without Icons
@@ -10,31 +10,45 @@ export const alertPropDefs: Record<string, PropDef> = {
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',
+4 -4
View File
@@ -97,13 +97,13 @@ export const withActionsAndDescriptionsSnippet = `<Alert
}
/>`;
export const loadingStatesSnippet = `<Flex direction="column" gap="4">
<Alert status="info" icon={true} loading title="Processing your request..." />
<Alert status="success" icon={true} loading title="Saving changes..." />
export const pendingStatesSnippet = `<Flex direction="column" gap="4">
<Alert status="info" icon={true} isPending title="Processing your request..." />
<Alert status="success" icon={true} isPending title="Saving changes..." />
<Alert
status="info"
icon={true}
loading
isPending
title="Processing your request"
description="This may take a few moments. Please do not close this window."
/>
@@ -56,12 +56,12 @@ export const Disabled = () => {
);
};
export const Loading = () => {
export const Pending = () => {
return (
<ButtonIcon
icon={<RiCloudLine />}
variant="primary"
loading
isPending
aria-label="Cloud"
/>
);
@@ -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={<Loading />}
code={loadingSnippet}
preview={<Pending />}
code={isPendingSnippet}
/>
<Theming definition={ButtonIconDefinition} />
@@ -42,11 +42,16 @@ export const buttonIconPropDefs: Record<string, PropDef> = {
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'],
@@ -20,4 +20,4 @@ export const disabledSnippet = `<Flex direction="row" gap="2">
<ButtonIcon isDisabled icon={<RiCloudLine />} variant="tertiary" aria-label="Cloud" />
</Flex>`;
export const loadingSnippet = `<ButtonIcon icon={<RiCloudLine />} variant="primary" loading aria-label="Cloud" />`;
export const isPendingSnippet = `<ButtonIcon icon={<RiCloudLine />} variant="primary" isPending aria-label="Cloud" />`;
@@ -75,9 +75,9 @@ export const Destructive = () => {
);
};
export const Loading = () => {
export const Pending = () => {
return (
<Button variant="primary" loading={true}>
<Button variant="primary" isPending={true}>
Load more items
</Button>
);
+6 -6
View File
@@ -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 = {
<PageTitle
title="Button"
description="A button with primary, secondary, and tertiary variants, destructive styling, and loading state."
description="A button with primary, secondary, and tertiary variants, destructive styling, and pending state."
/>
<Snippet align="center" py={4} preview={<Variants />} 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={<Loading />}
code={loadingSnippet}
preview={<Pending />}
code={isPendingSnippet}
layout="side-by-side"
/>
@@ -48,11 +48,16 @@ export const buttonPropDefs: Record<string, PropDef> = {
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'],
@@ -55,7 +55,7 @@ export const destructiveSnippet = `<Flex gap="4">
</Button>
</Flex>`;
export const loadingSnippet = `<Button variant="primary" loading={true}>
export const isPendingSnippet = `<Button variant="primary" isPending={true}>
Load more items
</Button>`;
+3 -3
View File
@@ -202,11 +202,11 @@ Use `mode: 'cursor'` when your API uses cursor-based pagination (common with Gra
<CodeBlock code={tableCursorPaginationSnippet} />
### 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
@@ -166,7 +166,7 @@ export const useTableReturnPropDefs: Record<string, PropDef> = {
description: (
<>
Props to spread onto the <Chip>Table</Chip> component. Includes data,
loading, error, pagination, and sort state.
isPending, error, pagination, and sort state.
</>
),
},
@@ -207,10 +207,15 @@ export const tablePropDefs: Record<string, PropDef> = {
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<string, PropDef> = {
</>
),
},
loading: {
isPending: {
type: 'boolean',
default: 'false',
description: (
<>
Whether the table is in a loading state (e.g., initial data fetch). Adds{' '}
<Chip>aria-busy</Chip> attribute and <Chip>data-loading</Chip> data
Whether the table is in a pending state (e.g., initial data fetch). Adds{' '}
<Chip>aria-busy</Chip> attribute and <Chip>data-ispending</Chip> data
attribute for styling.
</>
),
},
loading: {
type: 'boolean',
default: 'false',
description: 'Deprecated. Use `isPending` instead.',
},
};
export const columnPropDefs: Record<string, PropDef> = {
+19 -1
View File
@@ -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<T extends TableItem> {
// (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;
};
@@ -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: () => (
<Flex direction="column" gap="4">
<Text>Info</Text>
<Alert
status="info"
icon={true}
loading
isPending
title="Processing your request..."
/>
<Text>Success</Text>
<Alert status="success" icon={true} loading title="Saving changes..." />
<Alert status="success" icon={true} isPending title="Saving changes..." />
<Text>Warning</Text>
<Alert
status="warning"
icon={true}
loading
isPending
title="Checking for issues..."
/>
@@ -234,27 +234,27 @@ export const LoadingVariants = meta.story({
<Alert
status="danger"
icon={true}
loading
isPending
title="Attempting recovery..."
/>
</Flex>
),
});
export const LoadingWithDescription = meta.story({
export const PendingWithDescription = meta.story({
render: () => (
<Flex direction="column" gap="4">
<Alert
status="info"
icon={true}
loading
isPending
title="Processing your request"
description="This may take a few moments. Please do not close this window."
/>
<Alert
status="success"
icon={true}
loading
isPending
title="Deployment in Progress"
description="Your application is being deployed to production. You'll receive a notification when complete."
/>
+14 -6
View File
@@ -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
* <Alert
* status="success"
* icon={true}
* title="Operation completed"
* description="Your changes have been saved successfully."
* loading={isProcessing}
* isPending={isProcessing}
* customActions={
* <>
* <Button size="small" variant="tertiary">Dismiss</Button>
@@ -76,13 +76,21 @@ export const Alert = forwardRef(
(props: AlertProps, ref: Ref<HTMLDivElement>) => {
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'}
>
<div className={classes.contentWrapper}>
{loading ? (
{isPending ? (
<div className={classes.icon}>
<ProgressBar
aria-label="Loading"
@@ -36,6 +36,7 @@ export const AlertDefinition = defineComponent<AlertOwnProps>()({
},
propDefs: {
status: { dataAttribute: true, default: 'info' },
isPending: { dataAttribute: true },
loading: { dataAttribute: true },
icon: {},
customActions: {},
@@ -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;
@@ -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;
}
@@ -107,7 +107,7 @@ export const Destructive = meta.story({
<Button variant="primary" destructive isDisabled>
Disabled
</Button>
<Button variant="primary" destructive loading>
<Button variant="primary" destructive isPending>
Loading
</Button>
</Flex>
@@ -124,7 +124,7 @@ export const Destructive = meta.story({
<Button variant="secondary" destructive isDisabled>
Disabled
</Button>
<Button variant="secondary" destructive loading>
<Button variant="secondary" destructive isPending>
Loading
</Button>
</Flex>
@@ -141,7 +141,7 @@ export const Destructive = meta.story({
<Button variant="tertiary" destructive isDisabled>
Disabled
</Button>
<Button variant="tertiary" destructive loading>
<Button variant="tertiary" destructive isPending>
Loading
</Button>
</Flex>
@@ -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 (
<Button variant="primary" loading={isLoading} onPress={handleClick}>
<Button variant="primary" isPending={isPending} onPress={handleClick}>
Load more items
</Button>
);
},
});
export const LoadingVariants = meta.story({
export const PendingVariants = meta.story({
render: () => (
<Flex direction="column" gap="4">
<Text>Primary</Text>
<Flex align="center" gap="4">
<Button variant="primary" size="small" loading>
<Button variant="primary" size="small" isPending>
Small Loading
</Button>
<Button variant="primary" size="medium" loading>
<Button variant="primary" size="medium" isPending>
Medium Loading
</Button>
<Button variant="primary" loading iconStart={<RiCloudLine />}>
<Button variant="primary" isPending iconStart={<RiCloudLine />}>
With Icon
</Button>
</Flex>
<Text>Secondary</Text>
<Flex align="center" gap="4">
<Button variant="secondary" size="small" loading>
<Button variant="secondary" size="small" isPending>
Small Loading
</Button>
<Button variant="secondary" size="medium" loading>
<Button variant="secondary" size="medium" isPending>
Medium Loading
</Button>
<Button variant="secondary" loading iconStart={<RiCloudLine />}>
<Button variant="secondary" isPending iconStart={<RiCloudLine />}>
With Icon
</Button>
</Flex>
<Text>Tertiary</Text>
<Flex align="center" gap="4">
<Button variant="tertiary" size="small" loading>
<Button variant="tertiary" size="small" isPending>
Small Loading
</Button>
<Button variant="tertiary" size="medium" loading>
<Button variant="tertiary" size="medium" isPending>
Medium Loading
</Button>
<Button variant="tertiary" loading iconStart={<RiCloudLine />}>
<Button variant="tertiary" isPending iconStart={<RiCloudLine />}>
With Icon
</Button>
</Flex>
<Text>Primary Destructive</Text>
<Flex align="center" gap="4">
<Button variant="primary" destructive size="small" loading>
<Button variant="primary" destructive size="small" isPending>
Small Loading
</Button>
<Button variant="primary" destructive size="medium" loading>
<Button variant="primary" destructive size="medium" isPending>
Medium Loading
</Button>
<Button
variant="primary"
destructive
loading
isPending
iconStart={<RiCloudLine />}
>
With Icon
</Button>
</Flex>
<Text>Loading vs Disabled</Text>
<Text>Pending vs Disabled</Text>
<Flex align="center" gap="4">
<Button variant="primary" loading>
<Button variant="primary" isPending>
Loading
</Button>
<Button variant="primary" isDisabled>
Disabled
</Button>
<Button variant="primary" loading isDisabled>
<Button variant="primary" isPending isDisabled>
Both (Disabled Wins)
</Button>
</Flex>
+12 -4
View File
@@ -43,7 +43,7 @@ import { ButtonDefinition } from './definition';
* variant="primary"
* size="medium"
* iconStart={<IconComponent />}
* loading={isSubmitting}
* isPending={isSubmitting}
* >
* Submit
* </Button>
@@ -55,15 +55,23 @@ export const Button = forwardRef(
(props: ButtonProps, ref: Ref<HTMLButtonElement>) => {
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 (
<RAButton
className={classes.root}
ref={ref}
isPending={loading}
isPending={isPending}
{...dataAttributes}
{...restProps}
>
@@ -34,6 +34,7 @@ export const ButtonDefinition = defineComponent<ButtonOwnProps>()({
size: { dataAttribute: true, default: 'small' },
variant: { dataAttribute: true, default: 'primary' },
destructive: { dataAttribute: true },
isPending: { dataAttribute: true },
loading: { dataAttribute: true },
iconStart: {},
iconEnd: {},
@@ -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;
@@ -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;
}
@@ -82,14 +82,14 @@ export const Responsive = meta.story({
render: args => <ButtonIcon {...args} icon={<RiCloudLine />} />,
});
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({
<ButtonIcon
variant="primary"
icon={<RiCloudLine />}
loading={isLoading}
isPending={isPending}
onPress={handleClick}
/>
);
},
});
export const LoadingVariants = meta.story({
export const PendingVariants = meta.story({
render: () => (
<Flex direction="column" gap="4">
<Text>Primary</Text>
@@ -113,13 +113,13 @@ export const LoadingVariants = meta.story({
variant="primary"
size="small"
icon={<RiCloudLine />}
loading
isPending
/>
<ButtonIcon
variant="primary"
size="medium"
icon={<RiCloudLine />}
loading
isPending
/>
</Flex>
@@ -129,13 +129,13 @@ export const LoadingVariants = meta.story({
variant="secondary"
size="small"
icon={<RiCloudLine />}
loading
isPending
/>
<ButtonIcon
variant="secondary"
size="medium"
icon={<RiCloudLine />}
loading
isPending
/>
</Flex>
@@ -145,24 +145,24 @@ export const LoadingVariants = meta.story({
variant="tertiary"
size="small"
icon={<RiCloudLine />}
loading
isPending
/>
<ButtonIcon
variant="tertiary"
size="medium"
icon={<RiCloudLine />}
loading
isPending
/>
</Flex>
<Text>Loading vs Disabled</Text>
<Text>Pending vs Disabled</Text>
<Flex align="center" gap="4">
<ButtonIcon variant="primary" icon={<RiCloudLine />} loading />
<ButtonIcon variant="primary" icon={<RiCloudLine />} isPending />
<ButtonIcon variant="primary" icon={<RiCloudLine />} isDisabled />
<ButtonIcon
variant="primary"
icon={<RiCloudLine />}
loading
isPending
isDisabled
/>
</Flex>
@@ -30,15 +30,23 @@ export const ButtonIcon = forwardRef(
(props: ButtonIconProps, ref: Ref<HTMLButtonElement>) => {
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 (
<RAButton
className={classes.root}
ref={ref}
isPending={loading}
isPending={isPending}
{...dataAttributes}
{...restProps}
>
@@ -33,6 +33,7 @@ export const ButtonIconDefinition = defineComponent<ButtonIconOwnProps>()({
propDefs: {
size: { dataAttribute: true, default: 'small' },
variant: { dataAttribute: true, default: 'primary' },
isPending: { dataAttribute: true },
loading: { dataAttribute: true },
icon: {},
className: {},
@@ -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;
};
@@ -41,7 +41,7 @@
min-height: 0;
&[data-stale='true'],
&[data-loading='true'] {
&[data-ispending='true'] {
opacity: 0.6;
}
}
@@ -107,6 +107,7 @@ function useLiveRegionLabel(
export function Table<T extends TableItem>({
columnConfig,
data,
isPending = false,
loading = false,
isStale = false,
error,
@@ -119,6 +120,7 @@ export function Table<T extends TableItem>({
style,
virtualized,
}: TableProps<T>) {
const pending = isPending || loading;
const {
ownProps: { classes },
} = useDefinition(TableWrapperDefinition, { className });
@@ -137,7 +139,7 @@ export function Table<T extends TableItem>({
onSelectionChange,
} = selection || {};
const isInitialLoading = loading && !data;
const isInitialLoading = pending && !data;
if (error) {
return (
@@ -202,7 +204,7 @@ export function Table<T extends TableItem>({
onSortChange={sort?.onSortChange}
disabledKeys={disabledRows}
stale={isStale}
loading={isInitialLoading}
isPending={isInitialLoading}
aria-describedby={liveRegionId}
>
<TableHeader columns={visibleColumns}>
@@ -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 (
<ReactAriaTable
className={ownProps.classes.root}
aria-label="Data table"
aria-busy={ownProps.stale || ownProps.loading}
aria-busy={ownProps.stale || ownProps.isPending}
{...dataAttributes}
{...restProps}
/>
@@ -52,6 +52,7 @@ export const TableDefinition = defineComponent<TableRootOwnProps>()({
},
propDefs: {
stale: { dataAttribute: true },
isPending: { dataAttribute: true },
loading: { dataAttribute: true },
},
});
@@ -158,7 +158,7 @@ export interface UseTableResult<T extends TableItem, TFilter = unknown> {
/** @internal */
export interface PaginationResult<T> {
data: T[] | undefined;
loading: boolean;
isPending: boolean;
error: Error | undefined;
totalCount: number | undefined;
offset?: number;
@@ -48,7 +48,7 @@ export function useCompletePagination<T extends TableItem, TFilter>(
const { sort, filter, search } = query;
const [items, setItems] = useState<T[] | undefined>(undefined);
const [isLoading, setIsLoading] = useState(!data);
const [isPending, setIsPending] = useState(!data);
const [error, setError] = useState<Error | undefined>(undefined);
const [loadCount, setLoadCount] = useState(0);
@@ -64,7 +64,7 @@ export function useCompletePagination<T extends TableItem, TFilter>(
// 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<T extends TableItem, TFilter>(
}
let cancelled = false;
setIsLoading(true);
setIsPending(true);
setError(undefined);
(async () => {
@@ -82,12 +82,12 @@ export function useCompletePagination<T extends TableItem, TFilter>(
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<T extends TableItem, TFilter>(
return {
data: paginatedData,
loading: isLoading,
isPending: isPending,
error,
totalCount,
offset,
@@ -78,7 +78,7 @@ export function useCursorPagination<T extends TableItem, TFilter>(
return {
data: cache.data,
loading: cache.loading,
isPending: cache.isPending,
error: cache.error,
totalCount: cache.totalCount,
offset: undefined,
@@ -91,7 +91,7 @@ export function useOffsetPagination<T extends TableItem, TFilter>(
return {
data: cache.data,
loading: cache.loading,
isPending: cache.isPending,
error: cache.error,
totalCount: cache.totalCount,
offset: cache.currentCursor ?? 0,
@@ -48,7 +48,7 @@ export interface UsePageCacheOptions<T, TCursor extends CursorType = string> {
/** @internal */
export interface UsePageCacheResult<T, TCursor extends CursorType = string> {
loading: boolean;
isPending: boolean;
error: Error | undefined;
data: T[] | undefined;
totalCount: number | undefined;
@@ -149,7 +149,7 @@ export function usePageCache<T, TCursor extends CursorType = string>(
const cacheStore = useRef(new PageCacheStore<T, TCursor>()).current;
const [loading, setLoading] = useState(true);
const [isPending, setIsPending] = useState(true);
const [error, setError] = useState<Error | undefined>(undefined);
const [totalCount, setTotalCount] = useState<number | undefined>(undefined);
@@ -189,7 +189,7 @@ export function usePageCache<T, TCursor extends CursorType = string>(
const abortController = new AbortController();
abortControllerRef.current = abortController;
setLoading(true);
setIsPending(true);
setError(undefined);
try {
@@ -215,14 +215,14 @@ export function usePageCache<T, TCursor extends CursorType = string>(
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<T, TCursor extends CursorType = string>(
}, []);
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<T, TCursor extends CursorType = string>(
);
return {
loading,
isPending,
error,
data,
totalCount,
@@ -52,7 +52,7 @@ function useTableProps<T extends TableItem>(
}
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<T extends TableItem>(
return useMemo(
() => ({
data: displayData,
loading: paginationResult.loading,
isPending: paginationResult.isPending,
loading: paginationResult.isPending,
isStale,
error: paginationResult.error,
pagination,
@@ -112,7 +113,7 @@ function useTableProps<T extends TableItem>(
}),
[
displayData,
paginationResult.loading,
paginationResult.isPending,
isStale,
paginationResult.error,
pagination,
@@ -274,7 +274,7 @@ export const LoadingState: Story = {
<Table
columnConfig={columns}
data={undefined}
loading={true}
isPending
pagination={{ type: 'none' }}
/>
);
@@ -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<T extends TableItem> {
columnConfig: readonly ColumnConfig<T>[];
data: T[] | undefined;
isPending?: boolean;
/** @deprecated Use `isPending` instead. */
loading?: boolean;
isStale?: boolean;
error?: Error;