Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: accordion: enable custom summary layout using full width #799

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading