-
Notifications
You must be signed in to change notification settings - Fork 346
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds a reusable expansion panel element to LIT
PiperOrigin-RevId: 427235792
- Loading branch information
1 parent
0b86054
commit 2d670ce
Showing
18 changed files
with
447 additions
and
237 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
:host { | ||
display: flex; | ||
flex-direction: column; | ||
align-items: center; | ||
justify-content: start; | ||
} | ||
|
||
.expansion-header { | ||
width: calc(100% - 16px); | ||
height: 30px; | ||
padding: 2px 8px; | ||
border-bottom: 1px solid var(--lit-neutral-300); | ||
|
||
display: flex; | ||
flex-direction: row; | ||
cursor: pointer; | ||
align-items: center; | ||
justify-content: space-between; | ||
|
||
background-color: var(--lit-neutral-100); | ||
color: var(--lit-majtonal-nv-800); | ||
font-weight: bold; | ||
} | ||
|
||
.expansion-label { | ||
max-width: calc(100% - 32px); | ||
|
||
overflow: hidden; | ||
text-overflow: ellipsis; | ||
white-space: nowrap; | ||
} | ||
|
||
.expansion-content { | ||
padding: 0; | ||
border-bottom: 1px solid var(--lit-neutral-300); | ||
} | ||
|
||
.expansion-content.pad-left { | ||
padding-left: 8px; | ||
} | ||
|
||
.expansion-content.pad-right { | ||
padding-right: 16px; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
/** | ||
* @fileoverview A reusable expansion panel element for LIT | ||
* | ||
* @license | ||
* Copyright 2022 Google LLC | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
// tslint:disable:no-new-decorators | ||
import {html} from 'lit'; | ||
import {customElement, property} from 'lit/decorators'; | ||
import {classMap} from 'lit/directives/class-map'; | ||
import {styleMap} from 'lit/directives/style-map'; | ||
import {observable} from 'mobx'; | ||
|
||
import {ReactiveElement} from '../lib/elements'; | ||
|
||
import {styles} from './expansion_panel.css'; | ||
import {styles as sharedStyles} from '../lib/shared_styles.css'; | ||
|
||
/** Custom expansion event interface for ExpansionPanel */ | ||
export interface ExpansionToggle { | ||
isExpanded: boolean; | ||
} | ||
|
||
/** | ||
* An element that displays a header with a label and a toggle to expand or | ||
* collapse the subordinate content. | ||
*/ | ||
@customElement('expansion-panel') | ||
export class ExpansionPanel extends ReactiveElement { | ||
static override get styles() { | ||
return [sharedStyles, styles]; | ||
} | ||
|
||
@observable @property({type: String}) label = ''; | ||
@observable @property({type: Boolean}) expanded = false; | ||
@observable @property({type: Boolean}) padLeft = false; | ||
@observable @property({type: Boolean}) padRight = false; | ||
|
||
override render() { | ||
const contentPadding = (this.padLeft ? 8 : 0) + (this.padRight ? 16 : 0); | ||
const styles = styleMap({width: `calc(100% - ${contentPadding}px)`}); | ||
const classes = classMap({ | ||
'expansion-content': true, | ||
'pad-left': this.padLeft, | ||
'pad-right': this.padRight | ||
}); | ||
|
||
const toggle = () => { | ||
this.expanded = !this.expanded; | ||
const event = new CustomEvent<ExpansionToggle>('expansion-toggle', { | ||
detail: {isExpanded: this.expanded} | ||
}); | ||
this.dispatchEvent(event); | ||
}; | ||
|
||
return html` | ||
<div class="expansion-header" @click=${toggle}> | ||
<div class="expansion-label">${this.label}</div> | ||
<mwc-icon class="icon-button min-button"> | ||
${this.expanded ? 'expand_less' : 'expand_more'} | ||
</mwc-icon> | ||
</div> | ||
${this.expanded ? | ||
html`<div class=${classes} style=${styles}><slot></slot></div>` : | ||
null}`; | ||
} | ||
} | ||
|
||
declare global { | ||
interface HTMLElementTagNameMap { | ||
'expansion-panel': ExpansionPanel; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
/** | ||
* @fileoverview A reusable expansion panel element for LIT | ||
* | ||
* @license | ||
* Copyright 2022 Google LLC | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import 'jasmine'; | ||
import {html, render, LitElement} from 'lit'; | ||
import {ExpansionPanel, ExpansionToggle} from './expansion_panel'; | ||
|
||
describe('expansion panel test', () => { | ||
let expansionPanel: ExpansionPanel; | ||
|
||
const expansionHandler = (event: Event) => { | ||
const customEvent = event as CustomEvent<ExpansionToggle>; | ||
expect(customEvent.detail.isExpanded).toBeDefined(); | ||
}; | ||
|
||
beforeEach(async () => { | ||
expansionPanel = new ExpansionPanel(); | ||
document.body.appendChild(expansionPanel); | ||
document.body.addEventListener('expansion-toggle', expansionHandler); | ||
await expansionPanel.updateComplete; | ||
}); | ||
|
||
afterEach(() => { | ||
document.body.removeEventListener('expansion-toggle', expansionHandler); | ||
}); | ||
|
||
it('should instantiate correctly', () => { | ||
expect(expansionPanel).toBeDefined(); | ||
expect(expansionPanel instanceof HTMLElement).toBeTrue(); | ||
expect(expansionPanel instanceof LitElement).toBeTrue(); | ||
}); | ||
|
||
it('is initially collapsed', () => { | ||
expect(expansionPanel.renderRoot.children.length).toEqual(1); | ||
const [firstChild] = expansionPanel.renderRoot.children; | ||
expect(firstChild instanceof HTMLDivElement).toBeTrue(); | ||
expect((firstChild as HTMLDivElement).className) | ||
.toEqual('expansion-header'); | ||
}); | ||
|
||
it('expands when you click the header and emits an event', async () => { | ||
const [firstChild] = expansionPanel.renderRoot.children; | ||
(firstChild as HTMLDivElement).click(); | ||
await expansionPanel.updateComplete; | ||
|
||
expect(expansionPanel.renderRoot.children.length).toEqual(2); | ||
const [, secondChild] = expansionPanel.renderRoot.children; | ||
expect(secondChild instanceof HTMLDivElement).toBeTrue(); | ||
expect((secondChild as HTMLDivElement).className) | ||
.toEqual(' expansion-content '); | ||
}); | ||
|
||
it('collapses when you click the header a second time and emits an event', | ||
async () => { | ||
const [firstChild] = expansionPanel.renderRoot.children; | ||
(firstChild as HTMLDivElement).click(); | ||
await expansionPanel.updateComplete; | ||
|
||
(firstChild as HTMLDivElement).click(); | ||
await expansionPanel.updateComplete; | ||
|
||
expect(expansionPanel.renderRoot.children.length).toEqual(1); | ||
}); | ||
|
||
it('respects the expanded flag', async () => { | ||
const template = html` | ||
<expansion-panel .label=${'test expansion panel'} expanded> | ||
</expansion-panel>`; | ||
render(template, document.body); | ||
const panels = document.body.querySelectorAll('expansion-panel'); | ||
const panel = panels[panels.length - 1]; | ||
await panel.updateComplete; | ||
|
||
const [, content] = panel.renderRoot.children; | ||
expect(panel.renderRoot.children.length).toEqual(2); | ||
expect((content as HTMLElement).className).toEqual(' expansion-content '); | ||
}); | ||
|
||
it('respects the padLeft flag', async () => { | ||
const template = html` | ||
<expansion-panel .label=${'test expansion panel'} expanded padLeft> | ||
</expansion-panel>`; | ||
await render(template, document.body); | ||
const panels = document.body.querySelectorAll('expansion-panel'); | ||
const panel = panels[panels.length - 1]; | ||
await panel.updateComplete; | ||
|
||
const [, content] = panel.renderRoot.children; | ||
expect(panel.renderRoot.children.length).toEqual(2); | ||
expect((content as HTMLElement).style.width).toEqual('calc(100% - 8px)'); | ||
expect((content as HTMLElement).className) | ||
.toEqual(' expansion-content pad-left '); | ||
}); | ||
|
||
it('respects the padRight flag', async () => { | ||
const template = html` | ||
<expansion-panel .label=${'test expansion panel'} expanded padRight> | ||
</expansion-panel>`; | ||
await render(template, document.body); | ||
const panels = document.body.querySelectorAll('expansion-panel'); | ||
const panel = panels[panels.length - 1]; | ||
await panel.updateComplete; | ||
|
||
|
||
const [, content] = panel.renderRoot.children; | ||
expect(panel.renderRoot.children.length).toEqual(2); | ||
expect((content as HTMLElement).style.width).toEqual('calc(100% - 16px)'); | ||
expect((content as HTMLElement).className) | ||
.toEqual(' expansion-content pad-right '); | ||
}); | ||
|
||
it('respects padLeft and padRight simultaneously', async () => { | ||
const template = html` | ||
<expansion-panel .label=${'test expansion panel'} | ||
expanded padLeft padRight> | ||
</expansion-panel>`; | ||
await render(template, document.body); | ||
const panels = document.body.querySelectorAll('expansion-panel'); | ||
const panel = panels[panels.length - 1]; | ||
await panel.updateComplete; | ||
|
||
|
||
const [, content] = panel.renderRoot.children; | ||
expect(panel.renderRoot.children.length).toEqual(2); | ||
expect((content as HTMLElement).style.width).toEqual('calc(100% - 24px)'); | ||
expect((content as HTMLElement).className) | ||
.toEqual(' expansion-content pad-left pad-right '); | ||
}); | ||
|
||
it('should render a single Element in its slot from a template', async () => { | ||
const template = html` | ||
<expansion-panel .label=${'test expansion panel'}> | ||
<div>This is a test div</div> | ||
</expansion-panel>`; | ||
render(template, document.body); | ||
const panels = document.body.querySelectorAll('expansion-panel'); | ||
const panel = panels[panels.length - 1]; | ||
await panel.updateComplete; | ||
|
||
const [firstChild] = panel.renderRoot.children; | ||
(firstChild as HTMLDivElement).click(); | ||
await panel.updateComplete; | ||
|
||
const slot = panel.shadowRoot!.querySelector('slot'); | ||
const slottedNodes = slot!.assignedNodes({flatten: true}) | ||
.filter(n => n instanceof HTMLDivElement); | ||
|
||
expect(slottedNodes.length).toEqual(1); | ||
expect(slottedNodes[0] instanceof HTMLDivElement).toBeTrue(); | ||
}); | ||
|
||
it('should render many Elements in its slot from a template', async () => { | ||
const template = html` | ||
<expansion-panel .label=${'test expansion panel'}> | ||
<div>This is a test div</div> | ||
<div>This is another test div</div> | ||
<div>This is a third test div</div> | ||
</expansion-panel>`; | ||
render(template, document.body); | ||
const panels = document.body.querySelectorAll('expansion-panel'); | ||
const panel = panels[panels.length - 1]; | ||
await panel.updateComplete; | ||
|
||
const [firstChild] = panel.renderRoot.children; | ||
(firstChild as HTMLDivElement).click(); | ||
await panel.updateComplete; | ||
|
||
const slot = panel.shadowRoot!.querySelector('slot'); | ||
const slottedNodes = slot!.assignedNodes({flatten: true}) | ||
.filter(n => n instanceof HTMLDivElement); | ||
|
||
expect(slottedNodes.length).toEqual(3); | ||
for (const node of slottedNodes) { | ||
expect(node instanceof HTMLDivElement).toBeTrue(); | ||
} | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.