Skip to content

Commit

Permalink
feat: accordion: enable custom summary layout using full width (#799)
Browse files Browse the repository at this point in the history
* feat: accordion: enable custom summary layout using full width

* chore: accordion: adds ut
  • Loading branch information
dkilgore-eightfold authored Apr 16, 2024
1 parent 3cf3525 commit e66947d
Show file tree
Hide file tree
Showing 8 changed files with 436 additions and 29 deletions.
110 changes: 108 additions & 2 deletions src/components/Accordion/Accordion.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import React from 'react';
import { Stories } from '@storybook/addon-docs';
import { ComponentMeta, ComponentStory } from '@storybook/react';
import { List } from '../List';
import { IconName } from '../Icon';
import { Accordion, AccordionProps, AccordionSize, AccordionShape } from './';
import { Button, ButtonShape, ButtonVariant } from '../Button';
import { Badge } from '../Badge';
import { IconName } from '../Icon';
import Layout from '../Layout';
import { List } from '../List';
import { Stack } from '../Stack';

export default {
title: 'Accordion',
Expand Down Expand Up @@ -85,15 +89,27 @@ const listItems: AccordionProps[] = [
},
];

const buttons = [0, 1].map((i) => ({
ariaLabel: `Button ${i}`,
disruptive: i === 0 ? false : true,
icon: i === 0 ? IconName.mdiCogOutline : IconName.mdiDeleteOutline,
variant: i === 0 ? ButtonVariant.Neutral : ButtonVariant.Secondary,
}));

const Single_Story: ComponentStory<typeof Accordion> = (args) => (
<Accordion {...args} />
);

const List_Story: ComponentStory<typeof List> = (args) => <List {...args} />;

const Custom_Story: ComponentStory<typeof Accordion> = (args) => (
<Accordion {...args} />
);

export const Single = Single_Story.bind({});
export const List_Vertical = List_Story.bind({});
export const List_Horizontal = List_Story.bind({});
export const Custom = Custom_Story.bind({});

// Storybook 6.5 using Webpack >= 5.76.0 automatically alphabetizes exports,
// this line ensures they are exported in the desired order.
Expand All @@ -102,6 +118,7 @@ export const __namedExportsOrder = [
'Single',
'List_Vertical',
'List_Horizontal',
'Custom',
];

Single.args = {
Expand Down Expand Up @@ -179,3 +196,92 @@ List_Horizontal.args = {
padding: '8px',
},
};

Custom.args = {
children: (
<>
<div style={{ height: 'auto' }}>
Icons are optional for accordions. The body area in the expanded view is
like a modal or a slide-in panel. You can put any smaller components
inside to build a layout.
</div>
</>
),
id: 'myAccordionId',
expandButtonProps: null,
expandIconProps: {
path: IconName.mdiChevronDown,
},
configContextProps: {
noGradientContext: false,
noThemeContext: false,
},
theme: '',
themeContainerId: 'my-accordion-theme-container',
gradient: false,
headerProps: {
fullWidth: true,
style: { gap: '8px' },
},
summary: (
<Layout octupleStyles>
{' '}
{/* octupleStyles enables scoped Octuple BEM. */}
<Stack
fullWidth
direction="horizontal"
flexGap="m"
justify="space-between"
wrap="wrap"
>
<Stack direction="vertical" flexGap="xxxs">
<h4
className="octuple-h4"
style={{
alignSelf: 'center',
flexWrap: 'nowrap',
margin: 0,
whiteSpace: 'nowrap',
}}
>
Accordion Header <Badge style={{ margin: '0 8px' }}>2</Badge>
</h4>
<div
className="octuple-content"
style={{ color: 'var(--grey-tertiary-color)', fontWeight: 400 }}
>
Supporting text
</div>
</Stack>
<Stack
align="center"
direction="horizontal"
flexGap="m"
justify="flex-end"
style={{ width: 'min-content' }}
>
<List
items={buttons}
layout="horizontal"
listStyle={{ display: 'flex', gap: '8px' }}
renderItem={(item) => (
<Button
ariaLabel={item.ariaLabel}
disruptive={item.disruptive}
iconProps={{ path: item.icon }}
onClick={(e) => e.preventDefault()} // prevent accordion toggle, then apply your own logic.
shape={ButtonShape.Round}
variant={item.variant}
/>
)}
/>
</Stack>
</Stack>
</Layout>
),
bordered: true,
shape: AccordionShape.Pill,
size: AccordionSize.Large,
expanded: false,
disabled: false,
};
85 changes: 85 additions & 0 deletions src/components/Accordion/Accordion.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import Enzyme from 'enzyme';
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
import MatchMediaMock from 'jest-matchmedia-mock';
import { Accordion, AccordionProps, AccordionShape, AccordionSize } from './';
import { Button, ButtonShape, ButtonVariant } from '../Button';
import { Badge } from '../Badge';
import { IconName } from '../Icon';
import Layout from '../Layout';
import { List } from '../List';
import { Stack } from '../Stack';
import { fireEvent, render, waitFor } from '@testing-library/react';

Enzyme.configure({ adapter: new Adapter() });
Expand Down Expand Up @@ -54,6 +59,13 @@ const accordionProps: AccordionProps = {
'data-testid': 'test-accordion',
};

const buttons = [0, 1].map((i) => ({
ariaLabel: `Button ${i}`,
disruptive: i === 0 ? false : true,
icon: i === 0 ? IconName.mdiCogOutline : IconName.mdiDeleteOutline,
variant: i === 0 ? ButtonVariant.Neutral : ButtonVariant.Secondary,
}));

describe('Accordion', () => {
beforeAll(() => {
matchMedia = new MatchMediaMock();
Expand Down Expand Up @@ -135,4 +147,77 @@ describe('Accordion', () => {
expect(container.querySelector('.rectangle')).toBeTruthy();
expect(container).toMatchSnapshot();
});

test('Accordion renders custom content', () => {
const { container } = render(
<Accordion
{...accordionProps}
expanded={true}
headerProps={{
fullWidth: true,
style: { gap: '8px' },
}}
size={AccordionSize.Medium}
summary={
<Layout octupleStyles>
<Stack
fullWidth
direction="horizontal"
flexGap="m"
justify="space-between"
wrap="wrap"
>
<Stack direction="vertical" flexGap="xxxs">
<h4
className="octuple-h4"
style={{
alignSelf: 'center',
flexWrap: 'nowrap',
margin: 0,
whiteSpace: 'nowrap',
}}
>
Accordion Header <Badge style={{ margin: '0 8px' }}>2</Badge>
</h4>
<div
className="octuple-content"
style={{
color: 'var(--grey-tertiary-color)',
fontWeight: 400,
}}
>
Supporting text
</div>
</Stack>
<Stack
align="center"
direction="horizontal"
flexGap="m"
justify="flex-end"
style={{ width: 'min-content' }}
>
<List
items={buttons}
layout="horizontal"
listStyle={{ display: 'flex', gap: '8px' }}
renderItem={(item) => (
<Button
ariaLabel={item.ariaLabel}
disruptive={item.disruptive}
iconProps={{ path: item.icon }}
onClick={(e) => e.preventDefault()}
shape={ButtonShape.Round}
variant={item.variant}
/>
)}
/>
</Stack>
</Stack>
</Layout>
}
/>
);
expect(() => container).not.toThrowError();
expect(container).toMatchSnapshot();
});
});
32 changes: 21 additions & 11 deletions src/components/Accordion/Accordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,41 +29,47 @@ import styles from './accordion.module.scss';
import themedComponentStyles from './accordion.theme.module.scss';

export const AccordionSummary: FC<AccordionSummaryProps> = ({
badgeProps,
children,
classNames,
disabled,
expandButtonProps,
expandIconProps,
expanded,
onClick,
classNames,
expandIconProps,
fullWidth = false,
gradient,
id,
iconProps,
badgeProps,
id,
onIconButtonClick,
onClick,
size,
disabled,
...rest
}) => {
const headerClassnames = mergeClasses([
styles.accordionSummary,
classNames,
{
[styles.accordionSummaryFullWidth]: fullWidth,
[styles.medium]: size === AccordionSize.Medium,
[styles.large]: size === AccordionSize.Large,
[styles.accordionSummaryExpanded]: expanded,
[styles.disabled]: disabled,
},
]);

const iconStyles: string = mergeClasses([
styles.accordionIcon,
const iconButtonClassNames: string = mergeClasses([
styles.accordionIconButton,
// Conditional classes can also be handled as follows
{ [styles.expandedIcon]: expanded },
{ [styles.expandedIconButton]: expanded },
]);

// to handle enter press on accordion header
const handleKeyDown = useCallback(
(event) => {
event.key === eventKeys.ENTER && onClick?.(event);
if (event.key === eventKeys.ENTER || event.key === eventKeys.SPACE) {
event.preventDefault();
onClick?.(event);
}
},
[onClick]
);
Expand All @@ -89,9 +95,12 @@ export const AccordionSummary: FC<AccordionSummaryProps> = ({
{...expandButtonProps}
disabled={disabled}
gradient={gradient}
iconProps={{ classNames: iconStyles, ...expandIconProps }}
iconProps={{ classNames: iconButtonClassNames, ...expandIconProps }}
onClick={onIconButtonClick}
onKeyDown={handleKeyDown}
shape={ButtonShape.Round}
variant={gradient ? ButtonVariant.Secondary : ButtonVariant.Neutral}
{...expandButtonProps}
/>
</div>
);
Expand Down Expand Up @@ -215,6 +224,7 @@ export const Accordion: FC<AccordionProps> = React.forwardRef(
gradient={gradient}
iconProps={iconProps}
id={id}
onIconButtonClick={() => toggleAccordion(!isExpanded)}
onClick={() => toggleAccordion(!isExpanded)}
size={size}
{...headerProps}
Expand Down
20 changes: 15 additions & 5 deletions src/components/Accordion/Accordion.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ export enum AccordionSize {
}

interface AccordionBaseProps extends OcBaseProps<HTMLDivElement> {
/**
* If the accordion is bordered or not
* @default true
*/
bordered?: boolean;
/**
* Configure how contextual props are consumed
*/
Expand Down Expand Up @@ -43,16 +48,17 @@ interface AccordionBaseProps extends OcBaseProps<HTMLDivElement> {
* @default false
*/
gradient?: boolean;
/**
* The onClick callback for the accordion.
* @param event
* @returns
*/
onIconButtonClick?: React.MouseEventHandler<HTMLButtonElement>;
/**
* Shape of the accordion
* @default AccordionShape.Pill
*/
shape?: AccordionShape;
/**
* If the accordion is bordered or not
* @default true
*/
bordered?: boolean;
/**
* Size of the accordion
* @default AccordionSize.Large
Expand Down Expand Up @@ -108,6 +114,10 @@ export interface AccordionSummaryProps
* Badge props for the header badge
*/
badgeProps?: BadgeProps;
/**
* Whether the accordion summary is full width or not.
*/
fullWidth?: boolean;
}

export interface AccordionBodyProps
Expand Down
Loading

0 comments on commit e66947d

Please sign in to comment.