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

Add d2l-expand-collapse component to create collapsible areas #540

Merged
merged 14 commits into from
May 19, 2020
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ npm install @brightspace-ui/core
* [Colors](components/colors/): color palette
* [Dialogs](components/dialog/): generic and confirmation dialogs
* [Dropdowns](components/dropdown/): dropdown openers and content containers
* [Expand Collapse](components/expand-collapse): component to create expandable and collapsible content
* [Focus Trap](components/focus-trap/): generic container that traps focus
* [Icons](components/icons/): iconography SVGs and web components
* [Inputs](components/inputs/): text, search, select, checkbox and radio inputs
Expand Down
34 changes: 34 additions & 0 deletions components/expand-collapse/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Expand Collapse

## Expand Collapse Content

The `d2l-expand-collapse-content` element can be used to used to create expandable and collapsible content. This component only provides the logic to expand and collapse the content; controlling when and how it expands or collapses is the responsibility of the user.

![Expand Collapse Content](./screenshots/expand-collapse-content.gif?raw=true)

```html
<script type="module">
import '@brightspace-ui/core/components/expand-collapse/expand-collapse-content.js';
</script>

<d2l-expand-collapse-content expanded>
<p>My expand collapse content.</p>
</d2l-expand-collapse-content>
```

**Properties:**

- `expanded` (Boolean, default: `false`): Specifies the expanded/collapsed state of the content

**Events:**

- `d2l-expand-collapse-content-expand`: dispatched when the content starts to expand. The `detail` contains an `expandComplete` promise that can be waited on to determine when the content has finished expanding.
- `d2l-expand-collapse-content-collapse`: dispatched when the content starts to collapse. The `detail` contains a `collapseComplete` promise that can be waited on to determine when the content has finished collapsing.

**Accessibility:**

To make your usage of `d2l-expand-collapse-content` accessible, the [`aria-expanded` attribute](https://www.w3.org/TR/wai-aria/#aria-expanded) should be added to the element that controls expanding and collapsing the content with `"true"` or `"false"` to indicate that the content is expanded or collapsed.

## Future Enhancements

Looking for an enhancement not listed here? Create a GitHub issue!
58 changes: 58 additions & 0 deletions components/expand-collapse/demo/expand-collapse-content.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta charset="UTF-8">
<link rel="stylesheet" href="../../demo/styles.css" type="text/css">
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js"></script>
<script type="module">
import '../../button/button.js';
import '../../demo/demo-page.js';
import '../expand-collapse-content.js';
</script>
</head>

<body unresolved>

<d2l-demo-page page-title="d2l-expand-collapse-content">

<h2>Default</h2>
<d2l-demo-snippet>
<template>
<d2l-button primary>Toggle</d2l-button>
<d2l-expand-collapse-content>
<p>
Yar Pirate Ipsum
Crow's nest chase guns coxswain belay coffer jib Shiver me timbers tackle piracy Buccaneer. Overhaul topsail Cat o'nine
tails lee wherry Sink me smartly ballast Sail ho hardtack. Bowsprit aft quarterdeck killick pirate black jack hands
crimp interloper yawl.
</p>
<ul>
<li>Coffee</li>
<li>Tea</li>
<li>Milk</li>
</ul>
<p>
Me trysail gangplank Plate Fleet Sink me hang the jib lanyard parrel square-rigged stern. Gangplank chandler brigantine
spyglass scurvy rope's end plunder lugger topmast trysail. Admiral of the Black cackle fruit hearties maroon bounty
Blimey yo-ho-ho sutler pillage boom.
</p>
</d2l-expand-collapse-content>
<script type="module">
const button = document.querySelector('d2l-button');
button.addEventListener('click', () => {
const section = document.querySelector('d2l-expand-collapse-content');
section.expanded = !section.expanded;
button.setAttribute('aria-expanded', section.expanded ? 'true' : 'false');
});
</script>
</template>
</d2l-demo-snippet>

</d2l-demo-page>

</body>

</html>
151 changes: 151 additions & 0 deletions components/expand-collapse/expand-collapse-content.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { css, html, LitElement } from 'lit-element/lit-element.js';
import { styleMap } from 'lit-html/directives/style-map.js';

const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;

const states = {
PRECOLLAPSING: 'precollapsing', // setting up the styles so the collapse transition will run
COLLAPSING: 'collapsing', // in the process of collapsing
COLLAPSED: 'collapsed', // fully collapsed
PREEXPANDING: 'preexpanding', // setting up the styles so the expand transition will run
EXPANDING: 'expanding', // in the process of expanding
EXPANDED: 'expanded', // fully expanded
};

class ExpandCollapseContent extends LitElement {

static get properties() {
return {
expanded: { type: Boolean, reflect: true },
_height: { type: String },
_state: { type: String }
};
}

static get styles() {
return css`
:host {
display: block;
}

:host([hidden]) {
display: none;
}

.d2l-expand-collapse-content-container {
display: none;
overflow: hidden;
transition: height 400ms cubic-bezier(0, 0.7, 0.5, 1);
}

.d2l-expand-collapse-content-container:not([data-state="collapsed"]) {
display: block;
}

.d2l-expand-collapse-content-container[data-state="expanded"] {
overflow: visible;
}

/* prevent margin colapse on slotted children */
.d2l-expand-collapse-content-inner:before,
.d2l-expand-collapse-content-inner:after {
content: ' ';
display: table;
}

@media (prefers-reduced-motion: reduce) {
.d2l-expand-collapse-content-container {
transition: none;
}
}
`;
}

constructor() {
super();
this.expanded = false;
this._height = '0';
this._isFirstUpdate = true;
this._state = states.COLLAPSED;
}

updated(changedProperties) {
super.updated(changedProperties);
if (changedProperties.has('expanded')) {
this._expandedChanged(this.expanded, this._isFirstUpdate);
this._isFirstUpdate = false;
}
}

render() {
const styles = { height: this._height };
return html`
<div class="d2l-expand-collapse-content-container" data-state="${this._state}" @transitionend=${this._onTransitionEnd} style=${styleMap(styles)}>
<div class="d2l-expand-collapse-content-inner">
<slot></slot>
</div>
</div>
`;
}

async _expandedChanged(val, firstUpdate) {
const eventPromise = new Promise(resolve => this._eventPromiseResolve = resolve);
if (val) {
if (!firstUpdate) {
this.dispatchEvent(new CustomEvent(
'd2l-expand-collapse-content-expand',
{ bubbles: true, detail: { expandComplete: eventPromise } }
));
}
if (reduceMotion || firstUpdate) {
this._state = states.EXPANDED;
this._height = 'auto';
this._eventPromiseResolve();
} else {
this._state = states.PREEXPANDING;
await this.updateComplete;
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
if (this._state === states.PREEXPANDING) {
this._state = states.EXPANDING;
const content = this.shadowRoot.querySelector('.d2l-expand-collapse-content-inner');
this._height = `${content.scrollHeight}px`;
}
}
} else {
if (!firstUpdate) {
this.dispatchEvent(new CustomEvent(
'd2l-expand-collapse-content-collapse',
{ bubbles: true, detail: { collapseComplete: eventPromise } }
));
}
if (reduceMotion || firstUpdate) {
this._state = states.COLLAPSED;
this._height = '0';
this._eventPromiseResolve();
} else {
this._state = states.PRECOLLAPSING;
const content = this.shadowRoot.querySelector('.d2l-expand-collapse-content-inner');
this._height = `${content.scrollHeight}px`;
await this.updateComplete;
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
if (this._state === states.PRECOLLAPSING) {
this._state = states.COLLAPSING;
this._height = '0';
}
}
}
}

_onTransitionEnd() {
if (this._state === states.EXPANDING) {
this._state = states.EXPANDED;
this._height = 'auto';
this._eventPromiseResolve();
} else if (this._state === states.COLLAPSING) {
this._state = states.COLLAPSED;
this._eventPromiseResolve();
}
}

}
customElements.define('d2l-expand-collapse-content', ExpandCollapseContent);
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions components/expand-collapse/test/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "brightspace/open-wc-testing-config"
}
40 changes: 40 additions & 0 deletions components/expand-collapse/test/expand-collapse-content.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import '../expand-collapse-content.js';
import { fixture, html } from '@open-wc/testing';
import { runConstructor } from '../../../tools/constructor-test-helper.js';

const collapsedContentFixture = html`<d2l-expand-collapse-content>A message.</d2l-expand-collapse-content>`;
const expandedContentFixture = html`<d2l-expand-collapse-content expanded>A message.</d2l-expand-collapse-content>`;

describe('d2l-expand-collapse-content', () => {

describe('constructor', () => {

it('should construct', () => {
runConstructor('d2l-expand-collapse-content');
});

});

describe('events', () => {

it('should fire d2l-expand-collapse-content-expand event with complete promise', async() => {
const content = await fixture(collapsedContentFixture);
setTimeout(() => content.expanded = true);
const e = await new Promise(resolve => {
content.addEventListener('d2l-expand-collapse-content-expand', (e) => resolve(e), { once: true });
});
await e.detail.expandComplete;
});

it('should fire d2l-expand-collapse-content-collapse event with complete promise', async() => {
const content = await fixture(expandedContentFixture);
setTimeout(() => content.expanded = false);
const e = await new Promise(resolve => {
content.addEventListener('d2l-expand-collapse-content-collapse', (e) => resolve(e), { once: true });
});
await e.detail.collapseComplete;
});

});

});
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="en">

<head>
<link rel="stylesheet" href="../../../test/styles.css" type="text/css">
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js"></script>
<script type="module">
import '../../typography/typography.js';
import '../expand-collapse-content.js';
</script>
<title>d2l-expand-collapse-content</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta charset="UTF-8">
<style>
ul {
border: 4px solid green;
}
d2l-expand-collapse-content {
border: 4px solid blue;
}
</style>
</head>

<body class="d2l-typography">
<div class="visual-diff">
<d2l-expand-collapse-content id="collapsed">
<ul>
<li>Coffee</li>
<li>Tea</li>
<li>Milk</li>
</ul>
</d2l-expand-collapse-content>
</div>
<div class="visual-diff">
<d2l-expand-collapse-content id="expanded" expanded>
<ul>
<li>Coffee</li>
<li>Tea</li>
<li>Milk</li>
</ul>
</d2l-expand-collapse-content>
</div>
</body>

</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const puppeteer = require('puppeteer');
const VisualDiff = require('@brightspace-ui/visual-diff');

describe('d2l-expand-collapse-content', () => {

const visualDiff = new VisualDiff('expand-collapse-content', __dirname);

let browser, page;

before(async() => {
browser = await puppeteer.launch();
page = await visualDiff.createPage(browser, { viewport: { width: 400, height: 400 } });
await page.goto(`${visualDiff.getBaseUrl()}/components/expand-collapse/test/expand-collapse-content.visual-diff.html`, { waitUntil: ['networkidle0', 'load'] });
await page.bringToFront();
});

after(async() => await browser.close());

[
'collapsed',
'expanded'
].forEach((testName) => {
it(testName, async function() {
const selector = `#${testName}`;
const rect = await visualDiff.getRect(page, selector);
await visualDiff.screenshotAndCompare(page, this.test.fullTitle(), { clip: rect });
});
});

});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ <h2 class="d2l-heading-3">Components</h2>
<li><a href="components/dropdown/demo/dropdown-tabs.html">d2l-dropdown-tabs</a></li>
</ul>
</li>
<li><a href="components/expand-collapse/demo/expand-collapse-content.html">d2l-expand-collapse-content</a></li>
<li><a href="components/focus-trap/demo/focus-trap.html">d2l-focus-trap</a></li>
<li><a href="components/hierarchical-view/demo/hierarchical-view.html">d2l-hierarchical-view</a></li>
<li><a href="components/icons/demo/icon.html">d2l-icon</a></li>
Expand Down