Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save brandonbryant12/38c725ca1e56851cc198a0a2401c53fb to your computer and use it in GitHub Desktop.
Save brandonbryant12/38c725ca1e56851cc198a0a2401c53fb to your computer and use it in GitHub Desktop.
import * as React from 'react';
import { styled, useTheme, alpha } from '@mui/material/styles';
import ArrowForwardIosSharpIcon from '@mui/icons-material/ArrowForwardIosSharp';
import MuiAccordion, { AccordionProps } from '@mui/material/Accordion';
import MuiAccordionSummary, {
AccordionSummaryProps,
} from '@mui/material/AccordionSummary';
import MuiAccordionDetails from '@mui/material/AccordionDetails';
import Typography from '@mui/material/Typography';
const Accordion = styled((props: AccordionProps) => (
<MuiAccordion disableGutters elevation={0} square {...props} />
))(({ theme }) => ({
border: `1px solid ${theme.palette.divider}`,
'&:not(:last-child)': { borderBottom: 0 },
'&::before': { display: 'none' },
}));
const AccordionSummary = styled((props: AccordionSummaryProps) => (
<MuiAccordionSummary
expandIcon={<ArrowForwardIosSharpIcon sx={{ fontSize: '0.9rem' }} />}
{...props}
/>
))(() => ({
flexDirection: 'row-reverse',
'& .MuiAccordionSummary-expandIconWrapper.Mui-expanded': {
transform: 'rotate(90deg)',
},
'& .MuiAccordionSummary-content': { marginLeft: 8 },
}));
const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({
padding: theme.spacing(2),
borderTop: `1px solid ${theme.palette.divider}`,
}));
interface AccordionItem {
header: string;
body: string;
}
interface CustomizedAccordionsProps {
items: AccordionItem[];
exclusive?: boolean;
defaultExpandedId?: string | string[];
}
export default function CustomizedAccordions({
items,
exclusive = false,
defaultExpandedId,
}: CustomizedAccordionsProps) {
const theme = useTheme();
type ExpandedState = string | string[] | false;
const initialExpanded: ExpandedState = React.useMemo(() => {
if (exclusive) {
if (typeof defaultExpandedId === 'string') return defaultExpandedId;
if (Array.isArray(defaultExpandedId) && defaultExpandedId.length)
return defaultExpandedId[0];
return items.length ? 'panel0' : false;
}
if (Array.isArray(defaultExpandedId)) return defaultExpandedId;
if (typeof defaultExpandedId === 'string') return [defaultExpandedId];
return items.length ? ['panel0'] : [];
}, [exclusive, defaultExpandedId, items.length]);
const [expanded, setExpanded] = React.useState<ExpandedState>(initialExpanded);
const handleChange =
(panelId: string) =>
(_e: React.SyntheticEvent, isExpanded: boolean) => {
if (exclusive) {
setExpanded(isExpanded ? panelId : false);
} else {
setExpanded((prev) => {
const list = Array.isArray(prev) ? prev : [];
return isExpanded
? [...list, panelId]
: list.filter((id) => id !== panelId);
});
}
};
return (
<div>
{items.map((item, idx) => {
const panelId = `panel${idx}`;
const isOpen = exclusive
? expanded === panelId
: Array.isArray(expanded) && expanded.includes(panelId);
const bg =
idx % 2 === 0
? theme.palette.action.hover
: alpha(theme.palette.action.hover, 0.6);
return (
<Accordion
key={panelId}
expanded={isOpen}
onChange={handleChange(panelId)}
>
<AccordionSummary sx={{ backgroundColor: bg }}>
<Typography>{item.header}</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography>{item.body}</Typography>
</AccordionDetails>
</Accordion>
);
})}
</div>
);
}
This component renders a themed, accessibility-friendly accordion list whose items (header + body) are provided at run-time. It supports an exclusive mode (only one panel open at once) and applies a subtle striped background to alternate rows for visual scanning. The right-facing arrow icon flips down on expansion, consistent with our design language, and all colours pull from the active theme so it meshes seamlessly with any of our branded palettes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment