Skip to content

Commit

Permalink
[increment minor] Merge pull request #540 from BrightspaceUI/akerr/ex…
Browse files Browse the repository at this point in the history
…pand-collapse/dev

Add d2l-expand-collapse component to create collapsible areas
  • Loading branch information
AllanKerr authored May 19, 2020
2 parents cf22c10 + 0a76373 commit 5d60e00
Show file tree
Hide file tree
Showing 12 changed files with 364 additions and 0 deletions.
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

0 comments on commit 5d60e00

Please sign in to comment.