Update Accordion component

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2026-02-13 16:32:32 +00:00
parent ddd54c755c
commit becee36224
9 changed files with 387 additions and 152 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/ui': patch
---
Migrated Accordion components to use `useDefinition` instead of `useStyles`, and added automatic background adaptation based on parent container context.
+88 -26
View File
@@ -66,15 +66,15 @@ export const Accordion: ForwardRefExoticComponent<
// @public
export const AccordionDefinition: {
readonly styles: {
readonly [key: string]: string;
};
readonly classNames: {
readonly root: 'bui-Accordion';
readonly trigger: 'bui-AccordionTrigger';
readonly triggerButton: 'bui-AccordionTriggerButton';
readonly triggerTitle: 'bui-AccordionTriggerTitle';
readonly triggerSubtitle: 'bui-AccordionTriggerSubtitle';
readonly triggerIcon: 'bui-AccordionTriggerIcon';
readonly panel: 'bui-AccordionPanel';
readonly group: 'bui-AccordionGroup';
};
readonly bg: 'consumer';
readonly propDefs: {
readonly className: {};
};
};
@@ -84,11 +84,36 @@ export const AccordionGroup: ForwardRefExoticComponent<
>;
// @public
export interface AccordionGroupProps extends DisclosureGroupProps {
allowsMultiple?: boolean;
// (undocumented)
export const AccordionGroupDefinition: {
readonly styles: {
readonly [key: string]: string;
};
readonly classNames: {
readonly root: 'bui-AccordionGroup';
};
readonly propDefs: {
readonly className: {};
readonly allowsMultiple: {
readonly default: false;
};
};
};
// @public
export type AccordionGroupOwnProps = {
className?: string;
}
allowsMultiple?: boolean;
};
// @public
export interface AccordionGroupProps
extends Omit<DisclosureGroupProps, 'className'>,
AccordionGroupOwnProps {}
// @public
export type AccordionOwnProps = {
className?: string;
};
// @public (undocumented)
export const AccordionPanel: ForwardRefExoticComponent<
@@ -96,16 +121,32 @@ export const AccordionPanel: ForwardRefExoticComponent<
>;
// @public
export interface AccordionPanelProps extends DisclosurePanelProps {
// (undocumented)
className?: string;
}
export const AccordionPanelDefinition: {
readonly styles: {
readonly [key: string]: string;
};
readonly classNames: {
readonly root: 'bui-AccordionPanel';
};
readonly propDefs: {
readonly className: {};
};
};
// @public
export interface AccordionProps extends DisclosureProps {
// (undocumented)
export type AccordionPanelOwnProps = {
className?: string;
}
};
// @public
export interface AccordionPanelProps
extends Omit<DisclosurePanelProps, 'className'>,
AccordionPanelOwnProps {}
// @public
export interface AccordionProps
extends Omit<DisclosureProps, 'className'>,
AccordionOwnProps {}
// @public (undocumented)
export const AccordionTrigger: ForwardRefExoticComponent<
@@ -113,16 +154,37 @@ export const AccordionTrigger: ForwardRefExoticComponent<
>;
// @public
export interface AccordionTriggerProps extends HeadingProps {
// (undocumented)
children?: React.ReactNode;
// (undocumented)
export const AccordionTriggerDefinition: {
readonly styles: {
readonly [key: string]: string;
};
readonly classNames: {
readonly root: 'bui-AccordionTrigger';
readonly button: 'bui-AccordionTriggerButton';
readonly title: 'bui-AccordionTriggerTitle';
readonly subtitle: 'bui-AccordionTriggerSubtitle';
readonly icon: 'bui-AccordionTriggerIcon';
};
readonly propDefs: {
readonly className: {};
readonly title: {};
readonly subtitle: {};
readonly children: {};
};
};
// @public
export type AccordionTriggerOwnProps = {
className?: string;
// (undocumented)
subtitle?: string;
// (undocumented)
title?: string;
}
subtitle?: string;
children?: ReactNode;
};
// @public
export interface AccordionTriggerProps
extends Omit<HeadingProps, 'children' | 'className'>,
AccordionTriggerOwnProps {}
// @public
export const Alert: ForwardRefExoticComponent<
@@ -22,6 +22,18 @@
background-color: var(--bui-bg-neutral-1);
border-radius: var(--bui-radius-3);
padding: var(--bui-space-3);
&[data-on-bg='neutral-1'] {
background-color: var(--bui-bg-neutral-2);
}
&[data-on-bg='neutral-2'] {
background-color: var(--bui-bg-neutral-3);
}
&[data-on-bg='neutral-3'] {
background-color: var(--bui-bg-neutral-4);
}
}
.bui-AccordionTrigger {
@@ -21,6 +21,7 @@ import {
AccordionGroup,
} from './Accordion';
import { Box } from '../Box';
import { Flex } from '../Flex';
import { Text } from '../Text';
const Content = () => (
@@ -171,3 +172,57 @@ export const GroupMultipleOpen = meta.story({
</AccordionGroup>
),
});
export const AutoBg = meta.story({
render: () => (
<Flex direction="column" gap="4">
<div style={{ maxWidth: '600px' }}>
Accordions automatically detect their parent bg context and increment
the neutral level by 1. No prop is needed on the accordion -- it's fully
automatic.
</div>
<Flex direction="column" gap="4">
<Text>Default (no container)</Text>
<Accordion defaultExpanded>
<AccordionTrigger title="Toggle Panel" />
<AccordionPanel>
<Content />
</AccordionPanel>
</Accordion>
</Flex>
<Box bg="neutral-1" p="4">
<Text>Neutral 1 container</Text>
<Flex mt="2">
<Accordion defaultExpanded>
<AccordionTrigger title="Auto (neutral-2)" />
<AccordionPanel>
<Content />
</AccordionPanel>
</Accordion>
</Flex>
</Box>
<Box bg="neutral-2" p="4">
<Text>Neutral 2 container</Text>
<Flex mt="2">
<Accordion defaultExpanded>
<AccordionTrigger title="Auto (neutral-3)" />
<AccordionPanel>
<Content />
</AccordionPanel>
</Accordion>
</Flex>
</Box>
<Box bg="neutral-3" p="4">
<Text>Neutral 3 container</Text>
<Flex mt="2">
<Accordion defaultExpanded>
<AccordionTrigger title="Auto (neutral-4)" />
<AccordionPanel>
<Content />
</AccordionPanel>
</Accordion>
</Flex>
</Box>
</Flex>
),
});
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { forwardRef } from 'react';
import { forwardRef, Ref } from 'react';
import {
Disclosure as RADisclosure,
Button as RAButton,
@@ -23,132 +23,128 @@ import {
Heading as RAHeading,
} from 'react-aria-components';
import { RiArrowDownSLine } from '@remixicon/react';
import clsx from 'clsx';
import type {
AccordionProps,
AccordionTriggerProps,
AccordionPanelProps,
AccordionGroupProps,
} from './types';
import { useStyles } from '../../hooks/useStyles';
import { AccordionDefinition } from './definition';
import styles from './Accordion.module.css';
import { useDefinition } from '../../hooks/useDefinition';
import {
AccordionDefinition,
AccordionTriggerDefinition,
AccordionPanelDefinition,
AccordionGroupDefinition,
} from './definition';
import { Flex } from '../Flex';
/** @public */
export const Accordion = forwardRef<
React.ElementRef<typeof RADisclosure>,
AccordionProps
>(({ className, ...props }, ref) => {
const { classNames, cleanedProps } = useStyles(AccordionDefinition, props);
export const Accordion = forwardRef(
(props: AccordionProps, ref: Ref<React.ElementRef<typeof RADisclosure>>) => {
const { ownProps, restProps, dataAttributes } = useDefinition(
AccordionDefinition,
props,
);
const { classes } = ownProps;
return (
<RADisclosure
ref={ref}
className={clsx(classNames.root, styles[classNames.root], className)}
{...cleanedProps}
/>
);
});
return (
<RADisclosure
ref={ref}
className={classes.root}
{...dataAttributes}
{...restProps}
/>
);
},
);
Accordion.displayName = 'Accordion';
/** @public */
export const AccordionTrigger = forwardRef<
React.ElementRef<typeof RAHeading>,
AccordionTriggerProps
>(({ className, title, subtitle, children, ...props }, ref) => {
const { classNames, cleanedProps } = useStyles(AccordionDefinition, props);
export const AccordionTrigger = forwardRef(
(
props: AccordionTriggerProps,
ref: Ref<React.ElementRef<typeof RAHeading>>,
) => {
const { ownProps, restProps, dataAttributes } = useDefinition(
AccordionTriggerDefinition,
props,
);
const { classes, title, subtitle, children } = ownProps;
return (
<RAHeading
ref={ref}
className={clsx(
classNames.trigger,
styles[classNames.trigger],
className,
)}
{...cleanedProps}
>
<RAButton
slot="trigger"
className={clsx(
classNames.triggerButton,
styles[classNames.triggerButton],
)}
return (
<RAHeading
ref={ref}
className={classes.root}
{...dataAttributes}
{...restProps}
>
{children ? (
children
) : (
<Flex gap="2" align="center">
<span
className={clsx(
classNames.triggerTitle,
styles[classNames.triggerTitle],
)}
>
{title}
</span>
{subtitle && (
<span
className={clsx(
classNames.triggerSubtitle,
styles[classNames.triggerSubtitle],
)}
>
{subtitle}
</span>
)}
</Flex>
)}
<RiArrowDownSLine
className={clsx(
classNames.triggerIcon,
styles[classNames.triggerIcon],
<RAButton slot="trigger" className={classes.button}>
{children ? (
children
) : (
<Flex gap="2" align="center">
<span className={classes.title}>{title}</span>
{subtitle && <span className={classes.subtitle}>{subtitle}</span>}
</Flex>
)}
size={16}
/>
</RAButton>
</RAHeading>
);
});
<RiArrowDownSLine className={classes.icon} size={16} />
</RAButton>
</RAHeading>
);
},
);
AccordionTrigger.displayName = 'AccordionTrigger';
/** @public */
export const AccordionPanel = forwardRef<
React.ElementRef<typeof RADisclosurePanel>,
AccordionPanelProps
>(({ className, ...props }, ref) => {
const { classNames, cleanedProps } = useStyles(AccordionDefinition, props);
export const AccordionPanel = forwardRef(
(
props: AccordionPanelProps,
ref: Ref<React.ElementRef<typeof RADisclosurePanel>>,
) => {
const { ownProps, restProps, dataAttributes } = useDefinition(
AccordionPanelDefinition,
props,
);
const { classes } = ownProps;
return (
<RADisclosurePanel
ref={ref}
className={clsx(classNames.panel, styles[classNames.panel], className)}
{...cleanedProps}
/>
);
});
return (
<RADisclosurePanel
ref={ref}
className={classes.root}
{...dataAttributes}
{...restProps}
/>
);
},
);
AccordionPanel.displayName = 'AccordionPanel';
/** @public */
export const AccordionGroup = forwardRef<
React.ElementRef<typeof RADisclosureGroup>,
AccordionGroupProps
>(({ className, allowsMultiple = false, ...props }, ref) => {
const { classNames, cleanedProps } = useStyles(AccordionDefinition, props);
export const AccordionGroup = forwardRef(
(
props: AccordionGroupProps,
ref: Ref<React.ElementRef<typeof RADisclosureGroup>>,
) => {
const { ownProps, restProps, dataAttributes } = useDefinition(
AccordionGroupDefinition,
props,
);
const { classes, allowsMultiple } = ownProps;
return (
<RADisclosureGroup
ref={ref}
allowsMultipleExpanded={allowsMultiple}
className={clsx(classNames.group, styles[classNames.group], className)}
{...cleanedProps}
/>
);
});
return (
<RADisclosureGroup
ref={ref}
allowsMultipleExpanded={allowsMultiple}
className={classes.root}
{...dataAttributes}
{...restProps}
/>
);
},
);
AccordionGroup.displayName = 'AccordionGroup';
@@ -14,21 +14,79 @@
* limitations under the License.
*/
import type { ComponentDefinition } from '../../types';
import { defineComponent } from '../../hooks/useDefinition';
import type {
AccordionOwnProps,
AccordionTriggerOwnProps,
AccordionPanelOwnProps,
AccordionGroupOwnProps,
} from './types';
import styles from './Accordion.module.css';
/**
* Component definition for Accordion
* @public
*/
export const AccordionDefinition = {
export const AccordionDefinition = defineComponent<AccordionOwnProps>()({
styles,
classNames: {
root: 'bui-Accordion',
trigger: 'bui-AccordionTrigger',
triggerButton: 'bui-AccordionTriggerButton',
triggerTitle: 'bui-AccordionTriggerTitle',
triggerSubtitle: 'bui-AccordionTriggerSubtitle',
triggerIcon: 'bui-AccordionTriggerIcon',
panel: 'bui-AccordionPanel',
group: 'bui-AccordionGroup',
},
} as const satisfies ComponentDefinition;
bg: 'consumer',
propDefs: {
className: {},
},
});
/**
* Component definition for AccordionTrigger
* @public
*/
export const AccordionTriggerDefinition =
defineComponent<AccordionTriggerOwnProps>()({
styles,
classNames: {
root: 'bui-AccordionTrigger',
button: 'bui-AccordionTriggerButton',
title: 'bui-AccordionTriggerTitle',
subtitle: 'bui-AccordionTriggerSubtitle',
icon: 'bui-AccordionTriggerIcon',
},
propDefs: {
className: {},
title: {},
subtitle: {},
children: {},
},
});
/**
* Component definition for AccordionPanel
* @public
*/
export const AccordionPanelDefinition =
defineComponent<AccordionPanelOwnProps>()({
styles,
classNames: {
root: 'bui-AccordionPanel',
},
propDefs: {
className: {},
},
});
/**
* Component definition for AccordionGroup
* @public
*/
export const AccordionGroupDefinition =
defineComponent<AccordionGroupOwnProps>()({
styles,
classNames: {
root: 'bui-AccordionGroup',
},
propDefs: {
className: {},
allowsMultiple: { default: false },
},
});
+10 -1
View File
@@ -20,10 +20,19 @@ export {
AccordionPanel,
AccordionGroup,
} from './Accordion';
export { AccordionDefinition } from './definition';
export {
AccordionDefinition,
AccordionTriggerDefinition,
AccordionPanelDefinition,
AccordionGroupDefinition,
} from './definition';
export type {
AccordionOwnProps,
AccordionProps,
AccordionTriggerOwnProps,
AccordionTriggerProps,
AccordionPanelOwnProps,
AccordionPanelProps,
AccordionGroupOwnProps,
AccordionGroupProps,
} from './types';
+46 -13
View File
@@ -14,6 +14,7 @@
* limitations under the License.
*/
import type { ReactNode } from 'react';
import type {
DisclosureProps as RADisclosureProps,
HeadingProps as RAHeadingProps,
@@ -21,42 +22,74 @@ import type {
DisclosureGroupProps as RADisclosureGroupProps,
} from 'react-aria-components';
/**
* Own props for the Accordion component.
* @public
*/
export type AccordionOwnProps = {
className?: string;
};
/**
* Props for the Accordion component.
* @public
*/
export interface AccordionProps extends RADisclosureProps {
export interface AccordionProps
extends Omit<RADisclosureProps, 'className'>,
AccordionOwnProps {}
/**
* Own props for the AccordionTrigger component.
* @public
*/
export type AccordionTriggerOwnProps = {
className?: string;
}
title?: string;
subtitle?: string;
children?: ReactNode;
};
/**
* Props for the AccordionTrigger component.
* @public
*/
export interface AccordionTriggerProps extends RAHeadingProps {
export interface AccordionTriggerProps
extends Omit<RAHeadingProps, 'children' | 'className'>,
AccordionTriggerOwnProps {}
/**
* Own props for the AccordionPanel component.
* @public
*/
export type AccordionPanelOwnProps = {
className?: string;
title?: string;
subtitle?: string;
children?: React.ReactNode;
}
};
/**
* Props for the AccordionPanel component.
* @public
*/
export interface AccordionPanelProps extends RADisclosurePanelProps {
className?: string;
}
export interface AccordionPanelProps
extends Omit<RADisclosurePanelProps, 'className'>,
AccordionPanelOwnProps {}
/**
* Props for the AccordionGroup component.
* Own props for the AccordionGroup component.
* @public
*/
export interface AccordionGroupProps extends RADisclosureGroupProps {
export type AccordionGroupOwnProps = {
className?: string;
/**
* Whether multiple accordions can be expanded at the same time.
* @defaultValue false
*/
allowsMultiple?: boolean;
}
};
/**
* Props for the AccordionGroup component.
* @public
*/
export interface AccordionGroupProps
extends Omit<RADisclosureGroupProps, 'className'>,
AccordionGroupOwnProps {}
+6 -1
View File
@@ -19,7 +19,12 @@
* @packageDocumentation
*/
export { AccordionDefinition } from './components/Accordion/definition';
export {
AccordionDefinition,
AccordionTriggerDefinition,
AccordionPanelDefinition,
AccordionGroupDefinition,
} from './components/Accordion/definition';
export { AlertDefinition } from './components/Alert/definition';
export { AvatarDefinition } from './components/Avatar/definition';
export { BoxDefinition } from './components/Box/definition';