Skip to content

Commit

Permalink
Adds a reusable expansion panel element to LIT
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 427235792
  • Loading branch information
RyanMullins authored and LIT team committed Feb 8, 2022
1 parent 0b86054 commit 2d670ce
Show file tree
Hide file tree
Showing 18 changed files with 447 additions and 237 deletions.
4 changes: 2 additions & 2 deletions lit_nlp/client/core/faceting_control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export class FacetingControl extends ReactiveElement {
}

private updateBins() {
const configs: FacetingConfig[] = this.features.sort()
const configs: FacetingConfig[] = this.features
.filter(f => this.groupService.numericalFeatureNames.includes(f))
.map(f => this.featureConfigs.get(f) as FacetingConfig);

Expand Down Expand Up @@ -330,7 +330,7 @@ export class FacetingControl extends ReactiveElement {
}

private renderFeatureOptions() {
return this.groupService.denseFeatureNames.sort().map(
return this.groupService.denseFeatureNames.map(
feature => this.renderFeatureOption(feature));
}

Expand Down
44 changes: 44 additions & 0 deletions lit_nlp/client/elements/expansion_panel.css
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;
}
86 changes: 86 additions & 0 deletions lit_nlp/client/elements/expansion_panel.ts
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;
}
}
193 changes: 193 additions & 0 deletions lit_nlp/client/elements/expansion_panel_test.ts
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();
}
});
});
17 changes: 8 additions & 9 deletions lit_nlp/client/elements/interpreter_controls.css
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
.bordered {
border: 1px solid #e8eaed;
}

.description {
font-size: 8pt;
color: rgb(60, 64, 67);
padding-top: 10px;
}

.collapsible {
cursor: pointer;
}

.header {
padding: 8px;
width: 100%;
Expand All @@ -25,11 +17,18 @@
align-items: center;
}

.header .title {
color: var(--lit-majtonal-nv-800);
font-family: Roboto;
font-size: 13px;
font-weight: 700;
line-height: 18px;
}

.content {
padding: 0 8px;
overflow: hidden;
transition: max-height 0.2s ease-out;
background-color: #F8F9FA;
box-sizing: border-box;
}

Expand Down

0 comments on commit 2d670ce

Please sign in to comment.