@furystack/shades-common-components
Version:
Common UI components for FuryStack Shades
139 lines • 5.55 kB
JavaScript
import { Shade, createComponent } from '@furystack/shades';
import { buildTransition, cssVariableTheme } from '../../services/css-variable-theme.js';
import { collapse, expand } from '../animations.js';
import { Icon } from '../icons/icon.js';
import { chevronDown } from '../icons/icon-definitions.js';
/**
* An individual collapsible section for use within an Accordion container.
*
* Supports animated expand/collapse, keyboard accessibility, and optional icon.
*
* @example
* ```tsx
* <AccordionItem title="Section 1" defaultExpanded>
* <Typography variant="body1">Content goes here</Typography>
* </AccordionItem>
* ```
*/
export const AccordionItem = Shade({
customElementName: 'shade-accordion-item',
css: {
display: 'block',
fontFamily: cssVariableTheme.typography.fontFamily,
'&:not(:last-child)': {
borderBottom: `1px solid color-mix(in srgb, ${cssVariableTheme.text.secondary} 20%, transparent)`,
},
// Header
'& .accordion-header': {
display: 'flex',
alignItems: 'center',
gap: cssVariableTheme.spacing.sm,
padding: `${cssVariableTheme.spacing.md} ${cssVariableTheme.spacing.lg}`,
cursor: 'pointer',
userSelect: 'none',
backgroundColor: 'transparent',
border: 'none',
width: '100%',
textAlign: 'left',
outline: 'none',
font: 'inherit',
color: 'inherit',
transition: buildTransition([
'background-color',
cssVariableTheme.transitions.duration.fast,
cssVariableTheme.transitions.easing.default,
]),
},
'& .accordion-header:hover:not(:disabled)': {
backgroundColor: `color-mix(in srgb, ${cssVariableTheme.text.secondary} 8%, transparent)`,
},
'& .accordion-header:focus-visible': {
outline: 'none',
boxShadow: cssVariableTheme.action.focusRing,
},
// Icon
'& .accordion-icon': {
display: 'inline-flex',
alignItems: 'center',
flexShrink: '0',
fontSize: '1.2em',
},
// Title
'& .accordion-title': {
flex: '1',
fontSize: cssVariableTheme.typography.fontSize.md,
fontWeight: cssVariableTheme.typography.fontWeight.medium,
color: cssVariableTheme.text.primary,
},
// Chevron
'& .accordion-chevron': {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: '0',
fontSize: '1.2em',
color: cssVariableTheme.text.secondary,
transform: 'rotate(-90deg)',
transition: buildTransition([
'transform',
cssVariableTheme.transitions.duration.fast,
cssVariableTheme.transitions.easing.default,
]),
},
'&[data-expanded] .accordion-chevron': {
transform: 'rotate(0deg)',
},
// Content wrapper (animated)
'& .accordion-content': {
overflow: 'hidden',
},
// Content inner (provides padding)
'& .accordion-content-inner': {
padding: `0 ${cssVariableTheme.spacing.lg} ${cssVariableTheme.spacing.lg}`,
},
// Disabled state
'&[data-disabled]': {
opacity: cssVariableTheme.action.disabledOpacity,
pointerEvents: 'none',
},
'&[data-disabled] .accordion-header': {
cursor: 'not-allowed',
},
},
render: ({ props, children, useHostProps, useRef, useState }) => {
const [isExpanded, setIsExpanded] = useState('expanded', !!props.defaultExpanded);
useHostProps({
'data-disabled': props.disabled ? '' : undefined,
'data-expanded': isExpanded ? '' : undefined,
});
const contentRef = useRef('content');
const handleToggle = async () => {
if (props.disabled)
return;
const content = contentRef.current;
if (!content)
return;
if (isExpanded) {
setIsExpanded(false);
await collapse(content, { duration: 250 });
}
else {
content.inert = false;
setIsExpanded(true);
await expand(content, { duration: 250 });
}
};
return (createComponent(createComponent, null,
createComponent("button", { className: "accordion-header", type: "button", disabled: props.disabled, onclick: handleToggle, "aria-expanded": String(isExpanded) },
props.icon ? createComponent("span", { className: "accordion-icon" }, props.icon) : null,
createComponent("span", { className: "accordion-title" }, props.title),
createComponent("span", { className: "accordion-chevron" },
createComponent(Icon, { icon: chevronDown, size: 16 }))),
createComponent("div", { ref: contentRef, className: "accordion-content", inert: !isExpanded, style: {
height: isExpanded ? undefined : '0px',
opacity: isExpanded ? '1' : '0',
} },
createComponent("div", { className: "accordion-content-inner" }, children))));
},
});
//# sourceMappingURL=accordion-item.js.map