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 9 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 sections
* [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
37 changes: 37 additions & 0 deletions components/expand-collapse/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Expand Collapse

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

![More-Less](./screenshots/expand-collapse.gif?raw=true)

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

<button>Toggle</button>
<d2l-expand-collapse>
<p>My expand collapse content.</p>
</d2l-expand-collapse>

<script type="module">
AllanKerr marked this conversation as resolved.
Show resolved Hide resolved
const button = document.querySelector('button');
button.addEventListener('click', () => {
const section = document.querySelector('d2l-expand-collapse');
section.expanded = !section.expanded;
button.setAttribute('aria-expanded', section.expanded ? 'true' : 'false');
});
</script>
```

**Properties:**

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

**Accessibility:**

To make your usage of `d2l-expand-collapse` accessible, the `aria-expanded` attribute should be added to the element that controls expanding and collapsing section with `"true"` or `"false"` to indicate that the section is expanded or collapsed.
AllanKerr marked this conversation as resolved.
Show resolved Hide resolved

## 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.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.js';
</script>
</head>

<body unresolved>

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

<h2>Default</h2>
<d2l-demo-snippet>
<template>
<d2l-button primary>Toggle</d2l-button>
<d2l-expand-collapse>
AllanKerr marked this conversation as resolved.
Show resolved Hide resolved
<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>
<script type="module">
const button = document.querySelector('d2l-button');
button.addEventListener('click', () => {
const section = document.querySelector('d2l-expand-collapse');
section.expanded = !section.expanded;
button.setAttribute('aria-expanded', section.expanded ? 'true' : 'false');
});
</script>
</template>
</d2l-demo-snippet>

</d2l-demo-page>

</body>

</html>
144 changes: 144 additions & 0 deletions components/expand-collapse/expand-collapse.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
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
};
AllanKerr marked this conversation as resolved.
Show resolved Hide resolved

class ExpandCollapse 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-container {
display: none;
overflow: hidden;
transition: height 400ms cubic-bezier(0, 0.7, 0.5, 1);
}

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

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

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

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

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

updated(changedProperties) {
if (changedProperties.has('expanded')) {
this._expandedChanged(this.expanded);
}
}

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

async _expandedChanged(val) {
if (val) {
if (reduceMotion) {
this._state = states.EXPANDED;
this._height = 'auto';
} 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._getContent();
this._height = `${content.scrollHeight}px`;
}
}
} else {
if (reduceMotion) {
this._state = states.COLLAPSED;
this._height = '0';
} else {
this._state = states.PRECOLLAPSING;
const content = this._getContent();
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';
}
}
}
dlockhart marked this conversation as resolved.
Show resolved Hide resolved
AllanKerr marked this conversation as resolved.
Show resolved Hide resolved
}

_getContent() {
AllanKerr marked this conversation as resolved.
Show resolved Hide resolved
return this.shadowRoot.querySelector('.d2l-expand-collapse-content');
}

async _getUpdateComplete() {
const fontsPromise = document.fonts ? document.fonts.ready : Promise.resolve();
await super._getUpdateComplete();
/* wait for the fonts to load because browsers have a font block period
where they will render an invisible fallback font face that may result in
improper height calculations if the expand-collapse starts expanded */
AllanKerr marked this conversation as resolved.
Show resolved Hide resolved
await fontsPromise;
}

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

}
customElements.define('d2l-expand-collapse', ExpandCollapse);
AllanKerr marked this conversation as resolved.
Show resolved Hide resolved
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"
}
14 changes: 14 additions & 0 deletions components/expand-collapse/test/expand-collapse.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import '../expand-collapse.js';
import { runConstructor } from '../../../tools/constructor-test-helper.js';

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

describe('constructor', () => {

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

});

});
46 changes: 46 additions & 0 deletions components/expand-collapse/test/expand-collapse.visual-diff.html
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.js';
</script>
<title>d2l-expand-collapse</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 {
border: 4px solid blue;
}
</style>
</head>

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

@dlockhart dlockhart May 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting that the blue border still appears even when it's collapsed. It makes sense given it's the internal container that gets a display: none on it, but as a consumer of this I wouldn't have expected it. I can't think of a super clean way to fix this without reflecting that _state property up to the host. Hmm...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think having the border still show up will work well. It aligns with FACE's use of expand collapse where they have a line where the expand collapse section is when collapsed. Implementing it like this means it can be done with an attribute selector:
Capture

It will also make it a lot easier to use with d2l-more-less since that component doesn't fully collapse by default, it leaves 4 ems visibile.

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

</html>
30 changes: 30 additions & 0 deletions components/expand-collapse/test/expand-collapse.visual-diff.js
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', () => {

const visualDiff = new VisualDiff('expand-collapse', __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.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.html">d2l-expand-collapse</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