Merge branch 'backstage:master' into feature/catalog-export
This commit is contained in:
+2
-2
@@ -150,13 +150,13 @@ describe('PluginTaskManagerImpl', () => {
|
||||
id: 'task1',
|
||||
timeout: Duration.fromMillis(5000),
|
||||
frequency: Duration.fromObject({ years: 1 }),
|
||||
initialDelay: Duration.fromObject({ years: 1 }),
|
||||
initialDelay: Duration.fromObject({ seconds: 60 }),
|
||||
fn,
|
||||
scope: 'global',
|
||||
});
|
||||
|
||||
await manager.triggerTask('task1');
|
||||
jest.advanceTimersByTime(5000);
|
||||
await jest.advanceTimersByTimeAsync(65_000);
|
||||
|
||||
await promise;
|
||||
expect(fn).toHaveBeenCalledWith(expect.any(AbortSignal));
|
||||
|
||||
@@ -56,6 +56,29 @@ describe('util', () => {
|
||||
await promise;
|
||||
expect(true).toBe(true);
|
||||
}, 1_000);
|
||||
|
||||
it('handles durations longer than 2^31 ms by chunking setTimeout calls', async () => {
|
||||
jest.useFakeTimers();
|
||||
try {
|
||||
const thirtyDaysMs = 30 * 24 * 60 * 60 * 1000;
|
||||
let resolved = false;
|
||||
const promise = sleep(Duration.fromMillis(thirtyDaysMs)).then(() => {
|
||||
resolved = true;
|
||||
});
|
||||
|
||||
expect(jest.getTimerCount()).toBe(1);
|
||||
|
||||
await jest.advanceTimersByTimeAsync(2 ** 30);
|
||||
expect(resolved).toBe(false);
|
||||
expect(jest.getTimerCount()).toBe(1);
|
||||
|
||||
await jest.advanceTimersByTimeAsync(thirtyDaysMs - 2 ** 30);
|
||||
await promise;
|
||||
expect(resolved).toBe(true);
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('delegateAbortController', () => {
|
||||
|
||||
@@ -54,6 +54,11 @@ export function nowPlus(duration: Duration | undefined, knex: Knex) {
|
||||
return knex.raw(`now() + interval '${seconds} seconds'`);
|
||||
}
|
||||
|
||||
// Node.js setTimeout uses a 32-bit signed integer internally, so timeouts
|
||||
// longer than 2^31-1 ms (~24.8 days) fire immediately. We cap each individual
|
||||
// wait at 2^30 ms (~12.4 days) and loop until the full duration has elapsed.
|
||||
const MAX_TIMEOUT_MS = 2 ** 30;
|
||||
|
||||
/**
|
||||
* Sleep for the given duration, but return sooner if the abort signal
|
||||
* triggers.
|
||||
@@ -69,19 +74,34 @@ export async function sleep(
|
||||
return;
|
||||
}
|
||||
|
||||
await new Promise<void>(resolve => {
|
||||
let timeoutHandle: NodeJS.Timeout | undefined = undefined;
|
||||
let remaining = duration.as('milliseconds');
|
||||
if (!Number.isFinite(remaining) || remaining <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const done = () => {
|
||||
await new Promise<void>(resolve => {
|
||||
let timeoutHandle: NodeJS.Timeout | undefined;
|
||||
|
||||
const finish = () => {
|
||||
if (timeoutHandle) {
|
||||
clearTimeout(timeoutHandle);
|
||||
}
|
||||
abortSignal?.removeEventListener('abort', done);
|
||||
abortSignal?.removeEventListener('abort', finish);
|
||||
resolve();
|
||||
};
|
||||
|
||||
timeoutHandle = setTimeout(done, duration.as('milliseconds'));
|
||||
abortSignal?.addEventListener('abort', done);
|
||||
const tick = () => {
|
||||
if (remaining <= 0) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
const chunk = Math.min(remaining, MAX_TIMEOUT_MS);
|
||||
remaining -= chunk;
|
||||
timeoutHandle = setTimeout(tick, chunk);
|
||||
};
|
||||
|
||||
abortSignal?.addEventListener('abort', finish);
|
||||
tick();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -81,9 +81,8 @@ export class AwsCodeCommitIntegration implements ScmIntegration {
|
||||
*
|
||||
* @param url - The original URL
|
||||
* @param type - The desired type, e.g. 'blob', 'edit'
|
||||
* @public
|
||||
*/
|
||||
export function replaceCodeCommitUrlType(
|
||||
function replaceCodeCommitUrlType(
|
||||
url: string,
|
||||
repositoryName: string,
|
||||
type: 'browse' | 'edit',
|
||||
|
||||
@@ -209,11 +209,8 @@ export function buildGerritGitilesArchiveUrlFromLocation(
|
||||
* be used.
|
||||
*
|
||||
* @param config - A Gerrit provider config.
|
||||
* @public
|
||||
*/
|
||||
export function getAuthenticationPrefix(
|
||||
config: GerritIntegrationConfig,
|
||||
): string {
|
||||
function getAuthenticationPrefix(config: GerritIntegrationConfig): string {
|
||||
return config.password ? '/a/' : '/';
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ export function getGithubFileFetchUrl(
|
||||
}
|
||||
}
|
||||
|
||||
export function chooseEndpoint(
|
||||
function chooseEndpoint(
|
||||
config: GithubIntegrationConfig,
|
||||
credentials: GithubCredentials,
|
||||
): 'api' | 'raw' {
|
||||
|
||||
@@ -138,6 +138,7 @@ export class GitLabIntegration implements ScmIntegration {
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export async function sleep(
|
||||
durationMs: number,
|
||||
abortSignal: AbortSignal | null | undefined,
|
||||
|
||||
@@ -75,7 +75,7 @@ export function getGitLabRequestOptions(
|
||||
// Converts
|
||||
// from: https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath
|
||||
// to: https://gitlab.com/api/v4/projects/groupA%2Fteams%2FteamA%2FsubgroupA%2FrepoA/repository/files/filepath/raw?ref=branch
|
||||
export function buildProjectUrl(
|
||||
function buildProjectUrl(
|
||||
target: string,
|
||||
projectPathOrID: string | Number,
|
||||
config: GitLabIntegrationConfig,
|
||||
|
||||
@@ -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;
|
||||
@@ -431,6 +435,9 @@ export const BoxDefinition: {
|
||||
'py',
|
||||
'position',
|
||||
'display',
|
||||
'grow',
|
||||
'shrink',
|
||||
'basis',
|
||||
'width',
|
||||
'minWidth',
|
||||
'maxWidth',
|
||||
@@ -452,6 +459,7 @@ export type BoxOwnProps = {
|
||||
// @public (undocumented)
|
||||
export interface BoxProps
|
||||
extends SpaceProps,
|
||||
FlexItemProps,
|
||||
BoxOwnProps,
|
||||
BoxUtilityProps,
|
||||
Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {}
|
||||
@@ -510,6 +518,9 @@ export const ButtonDefinition: {
|
||||
readonly destructive: {
|
||||
readonly dataAttribute: true;
|
||||
};
|
||||
readonly isPending: {
|
||||
readonly dataAttribute: true;
|
||||
};
|
||||
readonly loading: {
|
||||
readonly dataAttribute: true;
|
||||
};
|
||||
@@ -545,6 +556,9 @@ export const ButtonIconDefinition: {
|
||||
readonly dataAttribute: true;
|
||||
readonly default: 'primary';
|
||||
};
|
||||
readonly isPending: {
|
||||
readonly dataAttribute: true;
|
||||
};
|
||||
readonly loading: {
|
||||
readonly dataAttribute: true;
|
||||
};
|
||||
@@ -558,6 +572,7 @@ export type ButtonIconOwnProps = {
|
||||
size?: Responsive<'small' | 'medium'>;
|
||||
variant?: Responsive<'primary' | 'secondary' | 'tertiary'>;
|
||||
icon?: ReactElement;
|
||||
isPending?: boolean;
|
||||
loading?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
@@ -623,6 +638,7 @@ export type ButtonOwnProps = {
|
||||
destructive?: boolean;
|
||||
iconStart?: ReactElement;
|
||||
iconEnd?: ReactElement;
|
||||
isPending?: boolean;
|
||||
loading?: boolean;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
@@ -642,6 +658,7 @@ export const Card: ForwardRefExoticComponent<
|
||||
export type CardBaseProps = {
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
// @public
|
||||
@@ -702,7 +719,9 @@ export const CardDefinition: {
|
||||
readonly target: {};
|
||||
readonly rel: {};
|
||||
readonly download: {};
|
||||
readonly style: {};
|
||||
};
|
||||
readonly utilityProps: readonly ['grow', 'shrink', 'basis'];
|
||||
};
|
||||
|
||||
// @public
|
||||
@@ -786,10 +805,12 @@ export type CardOwnProps = Pick<
|
||||
| 'target'
|
||||
| 'rel'
|
||||
| 'download'
|
||||
| 'style'
|
||||
>;
|
||||
|
||||
// @public
|
||||
export type CardProps = CardBaseProps &
|
||||
FlexItemProps &
|
||||
Omit<React.HTMLAttributes<HTMLDivElement>, 'onClick'> &
|
||||
(CardButtonVariant | CardLinkVariant | CardStaticVariant);
|
||||
|
||||
@@ -1315,12 +1336,22 @@ export const FlexDefinition: {
|
||||
'align',
|
||||
'justify',
|
||||
'direction',
|
||||
'grow',
|
||||
'shrink',
|
||||
'basis',
|
||||
];
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type FlexDirection = 'row' | 'column';
|
||||
|
||||
// @public
|
||||
export interface FlexItemProps {
|
||||
basis?: Responsive<CSSProperties['flexBasis']>;
|
||||
grow?: Responsive<number | boolean>;
|
||||
shrink?: Responsive<number | boolean>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type FlexOwnProps = {
|
||||
children: ReactNode;
|
||||
@@ -1332,6 +1363,7 @@ export type FlexOwnProps = {
|
||||
// @public (undocumented)
|
||||
export interface FlexProps
|
||||
extends SpaceProps,
|
||||
FlexItemProps,
|
||||
FlexOwnProps,
|
||||
Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {
|
||||
// (undocumented)
|
||||
@@ -1422,6 +1454,9 @@ export const GridDefinition: {
|
||||
'pt',
|
||||
'px',
|
||||
'py',
|
||||
'grow',
|
||||
'shrink',
|
||||
'basis',
|
||||
];
|
||||
};
|
||||
|
||||
@@ -1478,6 +1513,7 @@ export type GridOwnProps = {
|
||||
// @public (undocumented)
|
||||
export interface GridProps
|
||||
extends SpaceProps,
|
||||
FlexItemProps,
|
||||
GridOwnProps,
|
||||
Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
|
||||
// (undocumented)
|
||||
@@ -2717,6 +2753,9 @@ export const TableDefinition: {
|
||||
readonly stale: {
|
||||
readonly dataAttribute: true;
|
||||
};
|
||||
readonly isPending: {
|
||||
readonly dataAttribute: true;
|
||||
};
|
||||
readonly loading: {
|
||||
readonly dataAttribute: true;
|
||||
};
|
||||
@@ -2820,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;
|
||||
@@ -2843,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."
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -36,7 +36,7 @@ export const Box = forwardRef<HTMLDivElement, BoxProps>((props, ref) => {
|
||||
{
|
||||
ref,
|
||||
className: classes.root,
|
||||
style: { ...ownProps.style, ...utilityStyle },
|
||||
style: { ...utilityStyle, ...ownProps.style },
|
||||
...dataAttributes,
|
||||
...restProps,
|
||||
},
|
||||
|
||||
@@ -52,6 +52,9 @@ export const BoxDefinition = defineComponent<BoxOwnProps>()({
|
||||
'py',
|
||||
'position',
|
||||
'display',
|
||||
'grow',
|
||||
'shrink',
|
||||
'basis',
|
||||
'width',
|
||||
'minWidth',
|
||||
'maxWidth',
|
||||
|
||||
@@ -15,7 +15,12 @@
|
||||
*/
|
||||
|
||||
import type { ReactNode, CSSProperties } from 'react';
|
||||
import type { Responsive, ProviderBg, SpaceProps } from '../../types';
|
||||
import type {
|
||||
Responsive,
|
||||
ProviderBg,
|
||||
SpaceProps,
|
||||
FlexItemProps,
|
||||
} from '../../types';
|
||||
|
||||
/** @public */
|
||||
export type BoxOwnProps = {
|
||||
@@ -43,6 +48,7 @@ export type BoxUtilityProps = {
|
||||
/** @public */
|
||||
export interface BoxProps
|
||||
extends SpaceProps,
|
||||
FlexItemProps,
|
||||
BoxOwnProps,
|
||||
BoxUtilityProps,
|
||||
Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 @@ const INTERACTIVE_ELEMENT_SELECTOR =
|
||||
* @public
|
||||
*/
|
||||
export const Card = forwardRef<HTMLDivElement, CardProps>((props, ref) => {
|
||||
const { ownProps, restProps, dataAttributes } = useDefinition(
|
||||
const { ownProps, restProps, dataAttributes, utilityStyle } = useDefinition(
|
||||
CardDefinition,
|
||||
props,
|
||||
);
|
||||
@@ -98,6 +98,7 @@ export const Card = forwardRef<HTMLDivElement, CardProps>((props, ref) => {
|
||||
{...dataAttributes}
|
||||
{...restProps}
|
||||
onClick={isInteractive ? handleClick : undefined}
|
||||
style={{ ...utilityStyle, ...ownProps.style }}
|
||||
>
|
||||
{href && (
|
||||
<Link
|
||||
|
||||
@@ -42,7 +42,9 @@ export const CardDefinition = defineComponent<CardOwnProps>()({
|
||||
target: {},
|
||||
rel: {},
|
||||
download: {},
|
||||
style: {},
|
||||
},
|
||||
utilityProps: ['grow', 'shrink', 'basis'],
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,11 +14,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
import type { ReactNode, CSSProperties } from 'react';
|
||||
import type { ButtonProps as RAButtonProps } from 'react-aria-components';
|
||||
import type { FlexItemProps } from '../../types';
|
||||
|
||||
/** @public */
|
||||
export type CardBaseProps = { children?: ReactNode; className?: string };
|
||||
export type CardBaseProps = {
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type CardButtonVariant = {
|
||||
@@ -63,6 +68,7 @@ export type CardStaticVariant = {
|
||||
* @public
|
||||
*/
|
||||
export type CardProps = CardBaseProps &
|
||||
FlexItemProps &
|
||||
Omit<React.HTMLAttributes<HTMLDivElement>, 'onClick'> &
|
||||
(CardButtonVariant | CardLinkVariant | CardStaticVariant);
|
||||
|
||||
@@ -81,6 +87,7 @@ export type CardOwnProps = Pick<
|
||||
| 'target'
|
||||
| 'rel'
|
||||
| 'download'
|
||||
| 'style'
|
||||
>;
|
||||
|
||||
/** @public */
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
import preview from '../../../../../.storybook/preview';
|
||||
import { Flex } from './Flex';
|
||||
import { Text } from '../Text';
|
||||
import { Box } from '../Box';
|
||||
import { Box, BoxProps } from '../Box';
|
||||
import { Card, CardHeader, CardBody, CardFooter } from '../Card';
|
||||
import { Grid } from '../Grid';
|
||||
|
||||
const meta = preview.meta({
|
||||
title: 'Backstage UI/Flex',
|
||||
@@ -41,10 +43,9 @@ const meta = preview.meta({
|
||||
const DecorativeBox = ({
|
||||
width = '48px',
|
||||
height = '48px',
|
||||
}: {
|
||||
width?: string;
|
||||
height?: string;
|
||||
}) => {
|
||||
style,
|
||||
...props
|
||||
}: Omit<BoxProps, 'children'>) => {
|
||||
const diagonalStripePattern = (() => {
|
||||
const svg = `
|
||||
<svg width="6" height="6" viewBox="0 0 6 6" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -58,9 +59,11 @@ const DecorativeBox = ({
|
||||
|
||||
return (
|
||||
<Box
|
||||
{...props}
|
||||
width={width}
|
||||
height={height}
|
||||
style={{
|
||||
...style,
|
||||
background: '#eaf2fd',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #2563eb',
|
||||
@@ -199,6 +202,122 @@ export const ResponsiveAlign = meta.story({
|
||||
),
|
||||
});
|
||||
|
||||
export const FlexItems = meta.story({
|
||||
args: {
|
||||
component: 'Box',
|
||||
grow: 1,
|
||||
shrink: 0,
|
||||
basis: 'auto',
|
||||
},
|
||||
argTypes: {
|
||||
component: {
|
||||
control: { type: 'select' },
|
||||
options: ['Box', 'Card', 'Grid', 'Flex'],
|
||||
mapping: {
|
||||
Box: props => <DecorativeBox height="100%" width="256px" {...props} />,
|
||||
Card: props => (
|
||||
<Card style={{ height: '100%', width: '256px' }} {...props}>
|
||||
<CardHeader>
|
||||
<Text>Header</Text>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Text>
|
||||
This is the first paragraph of a long body text that
|
||||
demonstrates how the Card component handles extensive content.
|
||||
The card should adjust accordingly to display all the text
|
||||
properly while maintaining its structure.
|
||||
</Text>
|
||||
<Text>
|
||||
Here's a second paragraph that adds more content to our card
|
||||
body. Having multiple paragraphs helps to visualize how spacing
|
||||
works within the card component.
|
||||
</Text>
|
||||
<Text>
|
||||
This third paragraph continues to add more text to ensure we
|
||||
have a proper demonstration of a card with significant content.
|
||||
This makes it easier to test scrolling behavior and overall
|
||||
layout when content exceeds the initial view.
|
||||
</Text>
|
||||
</CardBody>
|
||||
<CardFooter>
|
||||
<Text>Footer</Text>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
),
|
||||
Grid: props => (
|
||||
<Grid.Root
|
||||
{...props}
|
||||
height="128px"
|
||||
style={{ width: '256px' }}
|
||||
columns="3"
|
||||
>
|
||||
<Grid.Item colSpan="1" rowSpan="2">
|
||||
<DecorativeBox height="100%" width="100%" />
|
||||
</Grid.Item>
|
||||
<Grid.Item colSpan="2">
|
||||
<DecorativeBox height="100%" width="100%" />
|
||||
</Grid.Item>
|
||||
<Grid.Item colSpan="2">
|
||||
<DecorativeBox height="100%" width="100%" />
|
||||
</Grid.Item>
|
||||
</Grid.Root>
|
||||
),
|
||||
Flex: props => (
|
||||
<Flex
|
||||
{...props}
|
||||
height="128px"
|
||||
style={{ width: '256px' }}
|
||||
justify="between"
|
||||
>
|
||||
<DecorativeBox height="100%" />
|
||||
<DecorativeBox height="100%" />
|
||||
<DecorativeBox height="100%" />
|
||||
</Flex>
|
||||
),
|
||||
},
|
||||
},
|
||||
grow: {
|
||||
control: 'radio',
|
||||
options: [undefined, 0, 1, false, true],
|
||||
},
|
||||
shrink: {
|
||||
control: 'radio',
|
||||
options: [undefined, 0, 1, false, true],
|
||||
},
|
||||
basis: {
|
||||
control: 'radio',
|
||||
options: [undefined, '0%', '25%', '50%', '100%', 100, '250px', 'auto'],
|
||||
},
|
||||
},
|
||||
render: ({ component: Component, ...args }) => {
|
||||
return (
|
||||
<Flex style={{ width: '100%', height: '256px' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '256px',
|
||||
flex: '1 1 auto',
|
||||
background:
|
||||
'repeating-linear-gradient(-45deg, transparent 0px, transparent 5px, #e8e8e8 5px, #e8e8e8 10px)',
|
||||
borderRadius: '12px',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Component {...args} />
|
||||
|
||||
<div
|
||||
style={{
|
||||
width: '256px',
|
||||
flex: '1 1 auto',
|
||||
background:
|
||||
'repeating-linear-gradient(-45deg, transparent 0px, transparent 5px, #e8e8e8 5px, #e8e8e8 10px)',
|
||||
borderRadius: '12px',
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const ResponsiveGap = meta.story({
|
||||
args: {
|
||||
gap: { xs: '4', md: '8', lg: '12' },
|
||||
|
||||
@@ -53,5 +53,8 @@ export const FlexDefinition = defineComponent<FlexOwnProps>()({
|
||||
'align',
|
||||
'justify',
|
||||
'direction',
|
||||
'grow',
|
||||
'shrink',
|
||||
'basis',
|
||||
],
|
||||
});
|
||||
|
||||
@@ -15,7 +15,13 @@
|
||||
*/
|
||||
|
||||
import type { ReactNode, CSSProperties } from 'react';
|
||||
import type { Responsive, Space, SpaceProps, ProviderBg } from '../../types';
|
||||
import type {
|
||||
Responsive,
|
||||
Space,
|
||||
SpaceProps,
|
||||
ProviderBg,
|
||||
FlexItemProps,
|
||||
} from '../../types';
|
||||
|
||||
/** @public */
|
||||
export type FlexOwnProps = {
|
||||
@@ -28,6 +34,7 @@ export type FlexOwnProps = {
|
||||
/** @public */
|
||||
export interface FlexProps
|
||||
extends SpaceProps,
|
||||
FlexItemProps,
|
||||
FlexOwnProps,
|
||||
Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {
|
||||
gap?: Responsive<Space>;
|
||||
|
||||
@@ -51,6 +51,9 @@ export const GridDefinition = defineComponent<GridOwnProps>()({
|
||||
'pt',
|
||||
'px',
|
||||
'py',
|
||||
'grow',
|
||||
'shrink',
|
||||
'basis',
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import type {
|
||||
Responsive,
|
||||
Columns,
|
||||
ProviderBg,
|
||||
FlexItemProps,
|
||||
} from '../../types';
|
||||
|
||||
/** @public */
|
||||
@@ -34,6 +35,7 @@ export type GridOwnProps = {
|
||||
/** @public */
|
||||
export interface GridProps
|
||||
extends SpaceProps,
|
||||
FlexItemProps,
|
||||
GridOwnProps,
|
||||
Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
|
||||
columns?: Responsive<Columns>;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -67,6 +67,18 @@
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.bui-grow {
|
||||
flex-grow: var(--grow);
|
||||
}
|
||||
|
||||
.bui-shrink {
|
||||
flex-shrink: var(--shrink);
|
||||
}
|
||||
|
||||
.bui-basis {
|
||||
flex-basis: var(--basis);
|
||||
}
|
||||
|
||||
/* Breakpoint xs */
|
||||
@media (min-width: 640px) {
|
||||
.xs\:bui-align-start {
|
||||
@@ -116,6 +128,18 @@
|
||||
.xs\:bui-fd-column-reverse {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.xs\:bui-grow {
|
||||
flex-grow: var(--grow-xs);
|
||||
}
|
||||
|
||||
.xs\:bui-shrink {
|
||||
flex-shrink: var(--shrink-xs);
|
||||
}
|
||||
|
||||
.xs\:bui-basis {
|
||||
flex-basis: var(--basis-xs);
|
||||
}
|
||||
}
|
||||
|
||||
/* Breakpoint sm */
|
||||
@@ -167,6 +191,18 @@
|
||||
.sm\:bui-fd-column-reverse {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.sm\:bui-grow {
|
||||
flex-grow: var(--grow-sm);
|
||||
}
|
||||
|
||||
.sm\:bui-shrink {
|
||||
flex-shrink: var(--shrink-sm);
|
||||
}
|
||||
|
||||
.sm\:bui-basis {
|
||||
flex-basis: var(--basis-sm);
|
||||
}
|
||||
}
|
||||
|
||||
/* Breakpoint md */
|
||||
@@ -218,6 +254,18 @@
|
||||
.md\:bui-fd-column-reverse {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.md\:bui-grow {
|
||||
flex-grow: var(--grow-md);
|
||||
}
|
||||
|
||||
.md\:bui-shrink {
|
||||
flex-shrink: var(--shrink-md);
|
||||
}
|
||||
|
||||
.md\:bui-basis {
|
||||
flex-basis: var(--basis-md);
|
||||
}
|
||||
}
|
||||
|
||||
/* Breakpoint lg */
|
||||
@@ -269,6 +317,18 @@
|
||||
.lg\:bui-fd-column-reverse {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.lg\:bui-grow {
|
||||
flex-grow: var(--grow-lg);
|
||||
}
|
||||
|
||||
.lg\:bui-shrink {
|
||||
flex-shrink: var(--shrink-lg);
|
||||
}
|
||||
|
||||
.lg\:bui-basis {
|
||||
flex-basis: var(--basis-lg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Breakpoint xl */
|
||||
@@ -320,5 +380,17 @@
|
||||
.xl\:bui-fd-column-reverse {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.xl\:bui-grow {
|
||||
flex-grow: var(--grow-xl);
|
||||
}
|
||||
|
||||
.xl\:bui-shrink {
|
||||
flex-shrink: var(--shrink-xl);
|
||||
}
|
||||
|
||||
.xl\:bui-basis {
|
||||
flex-basis: var(--basis-xl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ export function processUtilityProps<Keys extends string>(
|
||||
|
||||
const handleUtilityValue = (
|
||||
key: string,
|
||||
val: unknown,
|
||||
inputVal: unknown,
|
||||
prefix: string = '',
|
||||
) => {
|
||||
// Get utility class configuration for this key
|
||||
@@ -113,6 +113,11 @@ export function processUtilityProps<Keys extends string>(
|
||||
return;
|
||||
}
|
||||
|
||||
const val =
|
||||
'transform' in utilityConfig
|
||||
? utilityConfig.transform(inputVal)
|
||||
: inputVal;
|
||||
|
||||
// Check if value is in the list of valid values for this utility
|
||||
const values = utilityConfig.values as readonly (string | number)[];
|
||||
if (values.length > 0 && values.includes(val as string | number)) {
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { CSSProperties } from 'react';
|
||||
|
||||
/** @public */
|
||||
export type Breakpoint = 'initial' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||
|
||||
@@ -120,6 +122,20 @@ export interface PaddingProps {
|
||||
/** @public */
|
||||
export interface SpaceProps extends MarginProps, PaddingProps {}
|
||||
|
||||
/**
|
||||
* Flex item properties.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface FlexItemProps {
|
||||
/** Controls the flex-grow property. Values of `true` or `false` are converted to `1` or `0` respectively. */
|
||||
grow?: Responsive<number | boolean>;
|
||||
/** Controls the flex-shrink property. Values of `true` or `false` are converted to `1` or `0` respectively. */
|
||||
shrink?: Responsive<number | boolean>;
|
||||
/** Controls the flex-basis property. */
|
||||
basis?: Responsive<CSSProperties['flexBasis']>;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type TextVariants =
|
||||
| 'title-large'
|
||||
|
||||
@@ -196,7 +196,30 @@ export const utilityClassMap = {
|
||||
class: 'bui-row-span',
|
||||
values: columnsValues,
|
||||
},
|
||||
grow: {
|
||||
class: 'bui-grow',
|
||||
cssVar: '--grow',
|
||||
values: [],
|
||||
transform: input => (typeof input === 'boolean' ? Number(input) : input),
|
||||
},
|
||||
shrink: {
|
||||
class: 'bui-shrink',
|
||||
cssVar: '--shrink',
|
||||
values: [],
|
||||
transform: input => (typeof input === 'boolean' ? Number(input) : input),
|
||||
},
|
||||
basis: {
|
||||
class: 'bui-basis',
|
||||
cssVar: '--basis',
|
||||
values: [],
|
||||
transform: input => (typeof input === 'number' ? `${input}px` : input),
|
||||
},
|
||||
} as const satisfies Record<
|
||||
string,
|
||||
{ class: string; cssVar?: string; values: readonly (string | number)[] }
|
||||
{
|
||||
class: string;
|
||||
cssVar?: string;
|
||||
values: readonly (string | number)[];
|
||||
transform?: (input: unknown) => unknown;
|
||||
}
|
||||
>;
|
||||
|
||||
Reference in New Issue
Block a user