Skip to content

Commit

Permalink
feat: rewrite the collapse
Browse files Browse the repository at this point in the history
  • Loading branch information
EduardoJM authored Jul 21, 2024
1 parent 1ec2578 commit 9232b7d
Show file tree
Hide file tree
Showing 15 changed files with 251 additions and 230 deletions.
3 changes: 2 additions & 1 deletion packages/vanilla/src/base/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './ComponentManager';
export * from './ComponentManager';
export * from './ClickTriggerComponent';
88 changes: 88 additions & 0 deletions packages/vanilla/src/components/Collapse/Collapse.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { CollapseManager } from './CollapseManager';

interface CollapseProps {
id: string;
triggerContent: string;
content: string;
horizontal?: boolean;
}

export default {
title: 'Vanilla JavaScript/Collapse',
render: ({
id,
triggerContent,
content,
horizontal = false,
}: CollapseProps) => {
const container = document.createElement('div');
container.style.display = 'flex';
container.style.alignItems = 'stretch';

if (!horizontal) {
container.style.flexDirection = 'column';
container.style.width = '300px';
} else {
container.style.height = '200px';
}

const triggerDiv = document.createElement('div');
triggerDiv.innerHTML = triggerContent;
triggerDiv.setAttribute('data-toggle', 'collapse')
triggerDiv.setAttribute('data-target', `#${id}`);
triggerDiv.setAttribute('aria-expanded', 'true');
triggerDiv.style.background = 'var(--color-primary-500)'
if (horizontal) {
triggerDiv.style.textOrientation = 'mixed'
triggerDiv.style.writingMode = 'vertical-rl';
}

container.appendChild(triggerDiv);

const contentDiv = document.createElement('div');
contentDiv.className = 'collapse show';
if (horizontal) {
contentDiv.classList.add('horizontal');

const childDiv = document.createElement('div');
childDiv.style.width = '300px';
childDiv.innerHTML = content;
contentDiv.append(childDiv);
} else {
contentDiv.innerHTML = content;
}

contentDiv.id = id;
contentDiv.style.background = 'var(--color-primary-100)'
container.appendChild(contentDiv);

const collapseManager = new CollapseManager();
container.addEventListener('click', (ev) => {
collapseManager.getInstance(ev.target as HTMLElement)?.executeByClick();
});

return container;
},
argTypes: {
id: { control: 'text', },
triggerContent: { control: 'text' },
content: { control: 'text', },
},
};

export const Default = {
args: {
id: 'example-default-collapse',
triggerContent: 'Click here to toggle',
content: 'John doe<br/>John doe<br/>John doe<br/>John doe<br/>John doe<br/>John doe<br/>'
}
};

export const Horizontal = {
args: {
id: 'example-horizontal-collapse',
triggerContent: 'Click here to toggle',
content: 'John doe<br/>John doe<br/>John doe<br/>John doe<br/>John doe<br/>John doe<br/>',
horizontal: true,
}
};
Original file line number Diff line number Diff line change
@@ -1,34 +1,39 @@
import { expect, vi } from 'vitest';
import userEvent from '@testing-library/user-event'
import { } from '@testing-library/dom';
import { getCollapseObjects, CLASS_NAME_COLLAPSING, CLASS_NAME_SHOW, CLASS_NAME_COLLAPSED } from './collapse.helpers';
import { createCollapse } from '../../../stories/Collapse/Collapse';
import './collapse.browser';
import userEvent from '@testing-library/user-event';
import { CLASS_NAME_COLLAPSED, CLASS_NAME_COLLAPSING, CLASS_NAME_SHOW, Collapse } from './Collapse'
import { CollapseManager } from './CollapseManager';

describe("Collapse", () => {
describe('Collapse', () => {
const renderCollapseBase = (horizontal: boolean = false) => {
const user = userEvent.setup();
const id = 'my-collapse-id';
const triggerContent = 'my-trigger';
const content = 'my-content';

const element = createCollapse({ id, triggerContent, content, horizontal });
document.body.appendChild(element);
const container = document.createElement('div');
container.innerHTML = `
<div data-toggle="collapse" data-target="#${id}" aria-expanded="true">
${triggerContent}
</div>
<div id="${id}" class="collapse show">${content}</div>
`;

document.dispatchEvent(new Event("DOMContentLoaded", {
bubbles: true,
cancelable: true
}));
document.body.appendChild(container);

return { user, id, triggerContent, content, horizontal, element };
const manager = new CollapseManager();
container.addEventListener('click', (ev) => manager.getInstance(ev.target as HTMLElement)?.executeByClick());

const collapse = manager.getInstance(container.querySelector('[data-toggle]') as HTMLElement) as Collapse;

return { user, id, triggerContent, content, horizontal, container, collapse };
};

test('should add and remove the correct classes when click on trigger', async () => {
const { user } = renderCollapseBase();
const { user, collapse } = renderCollapseBase();

const toggleSelector = '[data-toggle="collapse"]';
const toggle = document.querySelector<HTMLElement>(toggleSelector) as HTMLElement;
const { content: contentElement } = getCollapseObjects(toggle);
const { content: contentElement } = collapse.element;

await user.click(toggle);
expect(contentElement?.classList.contains(CLASS_NAME_COLLAPSING)).toBeTruthy();
Expand All @@ -48,4 +53,4 @@ describe("Collapse", () => {
expect(toggle.getAttribute('aria-expanded')).toEqual('true');
});
});
});
})
89 changes: 89 additions & 0 deletions packages/vanilla/src/components/Collapse/Collapse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { ClickTriggerComponent } from '../../base';
import { capFirst } from '../../utils/text';
import { executeAfterTransition } from '../../utils/transitions';
import { CollapseElement } from './CollapseElement';

export interface CollapseContentSize {
property: 'width' | 'height';
value: string;
}

export const CLASS_NAME_SHOW = 'show';
export const CLASS_NAME_COLLAPSE = 'collapse';
export const CLASS_NAME_COLLAPSING = 'collapsing';
export const CLASS_NAME_COLLAPSED = 'collapsed';
export const CLASS_NAME_HORIZONTAL = 'horizontal';

export const DIMENSION_VERTICAL = 'VERTICAL'
export const DIMENSION_HORIZONTAL = 'HORIZONTAL'
export type DIMENSION_TYPE = 'VERTICAL' | 'HORIZONTAL';
export const DIMENSIONS: Record<DIMENSION_TYPE, 'width' | 'height'> = {
'VERTICAL': 'height',
'HORIZONTAL': 'width',
}

export class Collapse implements ClickTriggerComponent {
element: CollapseElement;

constructor(element: CollapseElement) {
this.element = element;
}

getContentSize(content: HTMLElement, isOpen: boolean): CollapseContentSize {
const dimension: DIMENSION_TYPE = content.classList.contains(CLASS_NAME_HORIZONTAL)
? DIMENSION_HORIZONTAL
: DIMENSION_VERTICAL;
const property = `scroll${capFirst(DIMENSIONS[dimension])}`;
if (!isOpen) {
return {
value: '',
property: DIMENSIONS[dimension],
};
}
return {
value: `${(content as any)[property]}px`,
property: DIMENSIONS[dimension],
};
}

toggle() {
const { toggle, content } = this.element;

if (content.classList.contains(CLASS_NAME_COLLAPSING)) {
return;
}

const isOpenAttr = toggle.getAttribute('aria-expanded');
let isOpen = true;
if (isOpenAttr) {
isOpen = Boolean(JSON.parse(isOpenAttr));
}

const { value, property } = this.getContentSize(content, isOpen);

content.style[property] = value;
content.offsetHeight; // reset animation

content.classList.add(CLASS_NAME_COLLAPSING);
content.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW)
content.classList.toggle(CLASS_NAME_COLLAPSED, isOpen);
toggle.setAttribute('aria-expanded', JSON.stringify(!isOpen));

const complete = () => {
content.classList.remove(CLASS_NAME_COLLAPSING);
content.classList.add(CLASS_NAME_COLLAPSE);
if (!isOpen) {
content.classList.add(CLASS_NAME_SHOW);
}
};
executeAfterTransition(complete, content);

const { value: destValue } = this.getContentSize(content, !isOpen);

content.style[property] = destValue;
}

executeByClick() {
this.toggle();
}
}
5 changes: 5 additions & 0 deletions packages/vanilla/src/components/Collapse/CollapseElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface CollapseElement {
id: string;
toggle: HTMLElement;
content: HTMLElement;
}
36 changes: 36 additions & 0 deletions packages/vanilla/src/components/Collapse/CollapseManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { ComponentManager, ClickTriggerComponentManager } from '../../base';
import { CollapseElement } from './CollapseElement';
import { Collapse } from './Collapse';

const TRIGGER_SELECTOR = '[data-toggle="collapse"]';

export class CollapseManager extends ComponentManager<CollapseElement, Collapse> {
protected getElement(target?: HTMLElement | null): CollapseElement | null {
if (!target) {
return null;
}
const toggleClosest = target.closest<HTMLElement>(TRIGGER_SELECTOR);
if (!toggleClosest) {
return null;
}
const contentQuery = toggleClosest.getAttribute('data-target');
if (!contentQuery) {
return null;
}
const contentEl = document.querySelector<HTMLElement>(contentQuery);
if (!contentEl) {
return null;
}
return {
id: contentEl.id,
toggle: toggleClosest,
content: contentEl,
};
}

protected createInstance(element: CollapseElement): Collapse {
return new Collapse(element);
}
}

ClickTriggerComponentManager.register(new CollapseManager());
3 changes: 3 additions & 0 deletions packages/vanilla/src/components/Collapse/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './CollapseElement';
export * from './CollapseManager';
export * from './Collapse';
89 changes: 0 additions & 89 deletions packages/vanilla/src/components/collapse/collapse.browser.ts

This file was deleted.

Loading

0 comments on commit 9232b7d

Please sign in to comment.