blob: b6ca9f5287a402c75bf185ac8029d40a2b4d8e9d [file] [log] [blame]
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import '@polymer/iron-dropdown/iron-dropdown';
import '@polymer/paper-item/paper-item';
import '@polymer/paper-listbox/paper-listbox';
import '../../../styles/shared-styles';
import '../gr-button/gr-button';
import '../gr-date-formatter/gr-date-formatter';
import '../gr-select/gr-select';
import '../gr-file-status/gr-file-status';
import {css, html, LitElement, PropertyValues} from 'lit';
import {customElement, property, query} from 'lit/decorators.js';
import {IronDropdownElement} from '@polymer/iron-dropdown/iron-dropdown';
import {Timestamp} from '../../../types/common';
import {NormalizedFileInfo} from '../../change/gr-file-list/gr-file-list';
import {GrButton} from '../gr-button/gr-button';
import {assertIsDefined} from '../../../utils/common-util';
import {sharedStyles} from '../../../styles/shared-styles';
import {ValueChangedEvent} from '../../../types/events';
import {incrementalRepeat} from '../../lit/incremental-repeat';
import {when} from 'lit/directives/when.js';
import {isMagicPath} from '../../../utils/path-list-util';
/**
* Required values are text and value. mobileText and triggerText will
* fall back to text if not provided.
*
* If bottomText is not provided, nothing will display on the second
* line.
*
* If date is not provided, nothing will be displayed in its place.
*/
export interface DropdownItem {
text: string;
value: string | number;
bottomText?: string;
triggerText?: string;
mobileText?: string;
date?: Timestamp;
disabled?: boolean;
file?: NormalizedFileInfo;
}
declare global {
interface HTMLElementEventMap {
'value-change': ValueChangedEvent<string>;
}
}
@customElement('gr-dropdown-list')
export class GrDropdownList extends LitElement {
@query('#dropdown')
dropdown?: IronDropdownElement;
@query('#trigger')
trigger?: GrButton;
/**
* Fired when the selected value changes
*
* @event value-change
*
* @property {string} value
*/
@property({type: Number})
initialCount = 75;
@property({type: Array})
items?: DropdownItem[];
@property({type: String})
text?: string;
@property({type: Boolean})
disabled = false;
@property({type: String})
value = '';
@property({type: Boolean, attribute: 'show-copy-for-trigger-text'})
showCopyForTriggerText = false;
static override get styles() {
return [
sharedStyles,
css`
:host {
display: inline-block;
}
#triggerText {
-moz-user-select: text;
-ms-user-select: text;
-webkit-user-select: text;
user-select: text;
}
.dropdown-trigger {
cursor: pointer;
padding: 0;
}
.dropdown-content {
background-color: var(--dropdown-background-color);
box-shadow: var(--elevation-level-2);
max-height: 70vh;
min-width: 266px;
}
paper-item:hover {
background-color: var(--hover-background-color);
}
paper-item:not(:last-of-type) {
border-bottom: 1px solid var(--border-color);
}
.bottomContent {
color: var(--deemphasized-text-color);
}
.bottomContent,
.topContent {
display: flex;
justify-content: space-between;
flex-direction: row;
width: 100%;
}
gr-button {
font-family: var(--trigger-style-font-family);
--gr-button-text-color: var(--trigger-style-text-color);
}
gr-date-formatter {
color: var(--deemphasized-text-color);
margin-left: var(--spacing-xxl);
white-space: nowrap;
}
gr-select {
display: none;
}
/* Because the iron dropdown 'area' includes the trigger, and the entire
width of the dropdown, we want to treat tapping the area above the
dropdown content as if it is tapping whatever content is underneath
it. The next two styles allow this to happen. */
iron-dropdown {
max-width: none;
pointer-events: none;
}
paper-listbox {
pointer-events: auto;
--paper-listbox_-_padding: 0;
}
paper-item {
cursor: pointer;
flex-direction: column;
font-size: inherit;
/* This variable was introduced in Dec 2019. We keep both min-height
* rules around, because --paper-item-min-height is not yet
* upstreamed.
*/
--paper-item-min-height: 0;
--paper-item_-_min-height: 0;
--paper-item_-_padding: 10px 16px;
--paper-item-focused-before_-_background-color: var(
--selection-background-color
);
--paper-item-focused_-_background-color: var(
--selection-background-color
);
}
@media only screen and (max-width: 50em) {
gr-select {
display: var(--gr-select-style-display, inline);
width: var(--gr-select-style-width);
}
gr-button,
iron-dropdown {
display: none;
}
select {
width: var(--native-select-style-width);
}
}
`,
];
}
protected override willUpdate(changedProperties: PropertyValues): void {
if (changedProperties.has('items') || changedProperties.has('value')) {
this.handleValueChange();
}
}
override render() {
return html`
<gr-button
id="trigger"
?disabled=${this.disabled}
down-arrow
link
class="dropdown-trigger"
slot="dropdown-trigger"
no-uppercase
@click=${this.showDropdownTapHandler}
>
<span id="triggerText">${this.text}</span>
<gr-copy-clipboard
?hidden=${!this.showCopyForTriggerText}
hideInput
.text=${this.text}
></gr-copy-clipboard>
</gr-button>
<iron-dropdown
id="dropdown"
.verticalAlign=${'top'}
.horizontalAlign=${'left'}
.dynamicAlign=${true}
.noOverlap=${true}
.allowOutsideScroll=${true}
@click=${this.handleDropdownClick}
>
<paper-listbox
class="dropdown-content"
slot="dropdown-content"
.attrForSelected=${'data-value'}
.selected=${this.value}
@selected-changed=${this.selectedChanged}
>
${incrementalRepeat({
values: this.items ?? [],
initialCount: this.initialCount,
mapFn: item => this.renderPaperItem(item as DropdownItem),
})}
</paper-listbox>
</iron-dropdown>
<gr-select
.bindValue=${this.value}
@bind-value-changed=${this.selectedChanged}
>
<select>
${this.items?.map(
item => html`
<option ?disabled=${item.disabled} value=${`${item.value}`}>
${this.computeMobileText(item)}
</option>
`
)}
</select>
</gr-select>
`;
}
private renderPaperItem(item: DropdownItem) {
return html`
<paper-item ?disabled=${item.disabled} data-value=${item.value}>
<div class="topContent">
<div>${item.text}</div>
${when(
item.date,
() => html`
<gr-date-formatter .dateStr=${item.date}></gr-date-formatter>
`
)}
${when(
item.file?.status && !isMagicPath(item.file?.__path),
() => html`
<gr-file-status .status=${item.file?.status}></gr-file-status>
`
)}
</div>
${when(
item.bottomText,
() => html`
<div class="bottomContent">
<div>${item.bottomText}</div>
</div>
`
)}
</paper-item>
`;
}
private selectedChanged(e: ValueChangedEvent<string>) {
this.value = e.detail.value;
}
/**
* Handle a click on the iron-dropdown element.
*/
private handleDropdownClick() {
// async is needed so that that the click event is fired before the
// dropdown closes (This was a bug for touch devices).
setTimeout(() => {
assertIsDefined(this.dropdown);
this.dropdown.close();
}, 1);
}
private handleValueChange() {
if (this.value === undefined || this.items === undefined) {
return;
}
const selectedObj = this.items.find(item => `${item.value}` === this.value);
if (!selectedObj) {
return;
}
this.text = selectedObj.triggerText
? selectedObj.triggerText
: selectedObj.text;
this.dispatchEvent(
new CustomEvent('value-change', {
detail: {value: this.value},
bubbles: false,
})
);
}
/**
* Handle a click on the button to open the dropdown.
*/
private showDropdownTapHandler() {
this.open();
}
/**
* Open the dropdown.
*/
open() {
assertIsDefined(this.dropdown);
this.dropdown.open();
}
// Private but used in tests.
computeMobileText(item: DropdownItem) {
return item.mobileText ? item.mobileText : item.text;
}
}
declare global {
interface HTMLElementTagNameMap {
'gr-dropdown-list': GrDropdownList;
}
}