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(fuselage)!: Lighter Accordion and AccordionItem components #1470

Merged
merged 6 commits into from
Oct 23, 2024
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
13 changes: 13 additions & 0 deletions .changeset/soft-islands-decide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@rocket.chat/fuselage": minor
---

Simplifies `Accordion` and `AccordionItem`

It removes an obsolete and not accessible toggle switch in `AccordionItem` and eases the internal usage of `Box` to
improve rendering performance.

Additionally, it adds a new `StylingBox` component that can be used as a wrapper for components that accept styling
props but don't need the weight of the `Box` component prop handling internally.

Also, it adds a new `cx` and `cxx` helpers to compose class names.
6 changes: 6 additions & 0 deletions packages/fuselage/jest-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,9 @@ window.ResizeObserver = jest.fn().mockImplementation(() => ({
unobserve: jest.fn(),
disconnect: jest.fn(),
}));

let uniqueIdCounter = 0;
jest.mock('@rocket.chat/fuselage-hooks', () => ({
...jest.requireActual('@rocket.chat/fuselage-hooks'),
useUniqueId: () => `unique-id-${uniqueIdCounter++}`,
}));
26 changes: 17 additions & 9 deletions packages/fuselage/src/components/Accordion/Accordion.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,25 @@ import { axe } from 'jest-axe';
import { render } from '../../testing';
import * as stories from './Accordion.stories';

const { Default } = composeStories(stories);
const testCases = Object.values(composeStories(stories)).map((Story) => [
Story.storyName || 'Story',
Story,
]);

describe('[Accordion Component]', () => {
it('renders without crashing', () => {
render(<Default />);
});
test.each(testCases)(
`renders %s without crashing`,
async (_storyname, Story) => {
const tree = render(<Story />);
expect(tree.baseElement).toMatchSnapshot();
}
);

it('should have no a11y violations', async () => {
const { container } = render(<Default />);
test.each(testCases)(
'%s should have no a11y violations',
async (_storyname, Story) => {
const { container } = render(<Story />);

const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
}
);
54 changes: 42 additions & 12 deletions packages/fuselage/src/components/Accordion/Accordion.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,66 @@ import type { StoryFn, Meta } from '@storybook/react';
import type { ComponentType } from 'react';

import Box from '../Box';
import { Accordion } from './Accordion';
import { AccordionItem } from './AccordionItem';
import Accordion from './Accordion';
import AccordionItem from './AccordionItem';

export default {
title: 'Containers/Accordion',
component: Accordion,
subcomponents: {
'Accordion.Item': Accordion.Item as ComponentType<any>,
'AccordionItem': AccordionItem as ComponentType<any>,
AccordionItem: AccordionItem as ComponentType<any>,
},
} satisfies Meta<typeof Accordion>;

const Template: StoryFn<typeof Accordion> = () => (
export const Default: StoryFn<typeof Accordion> = () => (
<Accordion>
<Accordion.Item title='Item #1' defaultExpanded>
<AccordionItem title='Item #1'>
<Box color='default' fontScale='p2' marginBlockEnd={16}>
Content #1
</Box>
</Accordion.Item>
<Accordion.Item title='Item #2'>
</AccordionItem>
<AccordionItem title='Item #2'>
<Box color='default' fontScale='p2' marginBlockEnd={16}>
Content #2
</Box>
</Accordion.Item>
<Accordion.Item title='Item #3'>
</AccordionItem>
<AccordionItem title='Item #3'>
<Box color='default' fontScale='p2' marginBlockEnd={16}>
Content #3
</Box>
</Accordion.Item>
</AccordionItem>
</Accordion>
);

export const Default: StoryFn<typeof Accordion> = Template.bind({});
const ItemTemplate: StoryFn<typeof AccordionItem> = ({
title = 'Item #2',
...args
}) => (
<Accordion>
<AccordionItem title='Item #1'>
<Box color='default' fontScale='p2' marginBlockEnd={16}>
Content #1
</Box>
</AccordionItem>
<AccordionItem title={title} {...args}>
<Box color='default' fontScale='p2' marginBlockEnd={16}>
Content #2
</Box>
</AccordionItem>
<AccordionItem title='Item #3'>
<Box color='default' fontScale='p2' marginBlockEnd={16}>
Content #3
</Box>
</AccordionItem>
</Accordion>
);

export const ExpandedItemByDefault = ItemTemplate.bind({});
ExpandedItemByDefault.args = {
defaultExpanded: true,
};

export const DisabledItem = ItemTemplate.bind({});
DisabledItem.args = {
disabled: true,
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
display: flex;
flex-flow: column nowrap;
border-block-end-color: colors.stroke(extra-light);

border-block-end-width: lengths.border-width(default);
}

Expand Down Expand Up @@ -64,14 +63,6 @@
@include typography.use-font-scale(h4);
}

.rcx-accordion-item__toggle-switch {
display: flex;
align-items: center;
flex: 0 0 auto;

margin: lengths.margin(none) lengths.margin(24);
}

.rcx-accordion-item__panel {
visibility: hidden;

Expand Down
27 changes: 14 additions & 13 deletions packages/fuselage/src/components/Accordion/Accordion.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import type { ComponentProps, ReactElement, ReactNode } from 'react';
import type { ReactNode } from 'react';

import Box from '../Box';
import { AccordionItem } from './AccordionItem';
import { cx, cxx } from '../../helpers/composeClassNames';
import { StylingBox } from '../Box';
import { StylingProps } from '../Box/stylingProps';

type AccordionProps = ComponentProps<typeof Box> & {
animated?: boolean;
export type AccordionProps = {
children: ReactNode;
};
} & Partial<StylingProps>;

/**
* An `Accordion` allows users to toggle the display of sections of content.
*/
export function Accordion(props: AccordionProps): ReactElement<AccordionProps> {
return <Box animated rcx-accordion {...props} />;
}
const Accordion = ({ children, ...props }: AccordionProps) => (
<StylingBox {...props}>
<div className={cx(cxx('rcx-box')('full', 'animated'), 'rcx-accordion')}>
{children}
</div>
</StylingBox>
);

/**
* @deprecated use named import instead
*/
Accordion.Item = AccordionItem;
export default Accordion;
114 changes: 49 additions & 65 deletions packages/fuselage/src/components/Accordion/AccordionItem.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useToggle, useUniqueId } from '@rocket.chat/fuselage-hooks';
import type { FormEvent, KeyboardEvent, MouseEvent, ReactNode } from 'react';
import type { KeyboardEvent, MouseEvent, ReactNode } from 'react';

import Box from '../Box';
import { cx, cxx } from '../../helpers/composeClassNames';
import { StylingBox } from '../Box';
import { Chevron } from '../Chevron';
import { ToggleSwitch } from '../ToggleSwitch';

type AccordionItemProps = {
export type AccordionItemProps = {
children?: ReactNode;
className?: string;
defaultExpanded?: boolean;
Expand All @@ -14,33 +14,20 @@ type AccordionItemProps = {
tabIndex?: number;
title: ReactNode;
noncollapsible?: boolean;
onToggle?: (e: MouseEvent | KeyboardEvent) => void;
onToggleEnabled?: (e: FormEvent) => void;
};

export const AccordionItem = function Item({
const AccordionItem = ({
children,
className,
defaultExpanded,
disabled,
disabled = false,
expanded: propExpanded,
tabIndex = 0,
title,
noncollapsible = !title,
onToggle,
onToggleEnabled,
...props
}: AccordionItemProps) {
}: AccordionItemProps) => {
const [stateExpanded, toggleStateExpanded] = useToggle(defaultExpanded);
const expanded = propExpanded || stateExpanded;
const toggleExpanded = (event: MouseEvent | KeyboardEvent) => {
if (onToggle) {
onToggle.call(event.currentTarget, event);
return;
}

toggleStateExpanded();
};

const panelExpanded = noncollapsible || expanded;

Expand All @@ -52,27 +39,25 @@ export const AccordionItem = function Item({
return;
}
e.currentTarget?.blur();
toggleExpanded(e);
toggleStateExpanded();
};

const handleKeyDown = (event: KeyboardEvent) => {
if (disabled || event.currentTarget !== event.target) {
return;
}

if ([13, 32].includes(event.keyCode)) {
event.preventDefault();
if (![' ', 'Enter'].includes(event.key)) {
return;
}

if (event.repeat) {
return;
}
event.preventDefault();

toggleExpanded(event);
if (event.repeat) {
return;
}
};

const handleToggleClick = (event: MouseEvent) => {
event.stopPropagation();
toggleStateExpanded();
};

const collapsibleProps = {
Expand All @@ -92,42 +77,41 @@ export const AccordionItem = function Item({
const barProps = noncollapsible ? nonCollapsibleProps : collapsibleProps;

return (
<Box is='section' rcx-accordion-item className={className} {...props}>
{title && (
<Box
role='button'
animated
rcx-accordion-item__bar
rcx-accordion-item__bar--disabled={disabled}
{...barProps}
>
<Box is='h2' rcx-accordion-item__title id={titleId}>
{title}
</Box>
{!noncollapsible && (
<>
{(disabled || onToggleEnabled) && (
<Box rcx-accordion-item__toggle-switch>
<ToggleSwitch
checked={!disabled}
onClick={handleToggleClick}
onChange={onToggleEnabled}
/>
</Box>
<StylingBox {...props}>
<section className={cx(cxx('rcx-box')('full'), 'rcx-accordion-item')}>
{title && (
<div
role='button'
className={cx(
cxx('rcx-box')('full', 'animated'),
cxx('rcx-accordion-item__bar')({ disabled })
)}
{...barProps}
>
<h2
className={cx(
cxx('rcx-box')('full'),
'rcx-accordion-item__title'
)}
<Chevron size='x24' up={expanded} />
</>
id={titleId}
>
{title}
</h2>
{!noncollapsible && <Chevron size='x24' up={expanded} />}
</div>
)}
<div
className={cx(
cxx('rcx-box')('full', 'animated'),
cxx('rcx-accordion-item__panel')({ expanded: panelExpanded })
)}
</Box>
)}
<Box
animated
rcx-accordion-item__panel
rcx-accordion-item__panel--expanded={panelExpanded}
id={panelId}
>
{children}
</Box>
</Box>
id={panelId}
>
{children}
</div>
</section>
</StylingBox>
);
};

export default AccordionItem;
Loading
Loading