| /** |
| * @license |
| * Copyright 2022 Google LLC |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| import {LitElement, css, html} from 'lit'; |
| import {customElement, property} from 'lit/decorators.js'; |
| import {sharedStyles} from '../../../styles/shared-styles'; |
| import {getAppContext} from '../../../services/app-context'; |
| import {Category, RunStatus} from '../../../api/checks'; |
| import { |
| ChecksIcon, |
| iconFor, |
| isStatus, |
| labelFor, |
| } from '../../../models/checks/checks-util'; |
| import {fontStyles} from '../../../styles/gr-font-styles'; |
| import {Interaction} from '../../../constants/reporting'; |
| |
| @customElement('gr-checks-chip') |
| export class GrChecksChip extends LitElement { |
| @property() |
| statusOrCategory?: Category | RunStatus; |
| |
| @property() |
| text = ''; |
| |
| @property({type: Array}) |
| links: string[] = []; |
| |
| private readonly reporting = getAppContext().reportingService; |
| |
| static override get styles() { |
| return [ |
| fontStyles, |
| sharedStyles, |
| css` |
| :host { |
| display: inline-block; |
| position: relative; |
| white-space: nowrap; |
| } |
| .checksChip { |
| color: var(--chip-color); |
| cursor: pointer; |
| display: inline-block; |
| margin-right: var(--spacing-s); |
| padding: var(--spacing-xxs) var(--spacing-m) var(--spacing-xxs) |
| var(--spacing-s); |
| border-radius: 12px; |
| border: 1px solid gray; |
| /* centered position of 20px chips in 24px line-height inline flow */ |
| vertical-align: top; |
| position: relative; |
| top: 2px; |
| } |
| .checksChip.hoverFullLength { |
| position: absolute; |
| z-index: 1; |
| display: none; |
| } |
| .checksChip.hoverFullLength .text { |
| max-width: 500px; |
| } |
| :host(:hover) .checksChip.hoverFullLength { |
| display: inline-block; |
| } |
| .checksChip .text { |
| display: inline-block; |
| max-width: 120px; |
| white-space: nowrap; |
| overflow: hidden; |
| text-overflow: ellipsis; |
| vertical-align: top; |
| } |
| gr-icon { |
| font-size: var(--line-height-small); |
| } |
| .checksChip a gr-icon.launch { |
| color: var(--link-color); |
| } |
| .checksChip.error { |
| color: var(--error-foreground); |
| border-color: var(--error-foreground); |
| background: var(--error-background); |
| } |
| .checksChip.error:hover { |
| background: var(--error-background-hover); |
| box-shadow: var(--elevation-level-1); |
| } |
| .checksChip.error:focus-within { |
| background: var(--error-background-focus); |
| } |
| .checksChip.error gr-icon { |
| color: var(--error-foreground); |
| } |
| .checksChip.warning { |
| border-color: var(--warning-foreground); |
| background: var(--warning-background); |
| } |
| .checksChip.warning:hover { |
| background: var(--warning-background-hover); |
| box-shadow: var(--elevation-level-1); |
| } |
| .checksChip.warning:focus-within { |
| background: var(--warning-background-focus); |
| } |
| .checksChip.warning gr-icon { |
| color: var(--warning-foreground); |
| } |
| .checksChip.info { |
| border-color: var(--info-foreground); |
| background: var(--info-background); |
| } |
| .checksChip.info:hover { |
| background: var(--info-background-hover); |
| box-shadow: var(--elevation-level-1); |
| } |
| .checksChip.info:focus-within { |
| background: var(--info-background-focus); |
| } |
| .checksChip.info gr-icon { |
| color: var(--info-foreground); |
| } |
| .checksChip.check_circle { |
| border-color: var(--success-foreground); |
| background: var(--success-background); |
| } |
| .checksChip.check_circle:hover { |
| background: var(--success-background-hover); |
| box-shadow: var(--elevation-level-1); |
| } |
| .checksChip.check_circle:focus-within { |
| background: var(--success-background-focus); |
| } |
| .checksChip.check_circle gr-icon { |
| color: var(--success-foreground); |
| } |
| .checksChip.timelapse, |
| .checksChip.scheduled { |
| border-color: var(--gray-foreground); |
| background: var(--gray-background); |
| } |
| .checksChip.timelapse:hover, |
| .checksChip.pending_actions:hover { |
| background: var(--gray-background-hover); |
| box-shadow: var(--elevation-level-1); |
| } |
| .checksChip.timelapse:focus-within, |
| .checksChip.pending_actions:focus-within { |
| background: var(--gray-background-focus); |
| } |
| .checksChip.timelapse gr-icon, |
| .checksChip.pending_actions gr-icon { |
| color: var(--gray-foreground); |
| } |
| `, |
| ]; |
| } |
| |
| override render() { |
| if (!this.text) return; |
| if (!this.statusOrCategory) return; |
| const icon = iconFor(this.statusOrCategory); |
| const ariaLabel = this.computeAriaLabel(); |
| const chipClass = `checksChip font-small ${icon.name}`; |
| const chipClassFullLength = `${chipClass} hoverFullLength`; |
| // 15 is roughly the number of chars for the chip exceeding its 120px width. |
| return html` |
| ${this.text.length > 15 |
| ? html` ${this.renderChip(chipClassFullLength, ariaLabel, icon)}` |
| : ''} |
| ${this.renderChip(chipClass, ariaLabel, icon)} |
| `; |
| } |
| |
| private computeAriaLabel() { |
| if (!this.statusOrCategory) return ''; |
| const label = labelFor(this.statusOrCategory); |
| const type = isStatus(this.statusOrCategory) ? 'run' : 'result'; |
| const count = Number(this.text); |
| const isCountChip = !isNaN(count); |
| if (isCountChip) { |
| const plural = count > 1 ? 's' : ''; |
| return `${this.text} ${label} ${type}${plural}`; |
| } |
| return `${label} for check ${this.text}`; |
| } |
| |
| private renderChip(clazz: string, ariaLabel: string, icon: ChecksIcon) { |
| return html` |
| <div class=${clazz} role="link" tabindex="0" aria-label=${ariaLabel}> |
| <gr-icon icon=${icon.name} ?filled=${icon.filled}></gr-icon> |
| ${this.renderLinks()} |
| <div class="text">${this.text}</div> |
| </div> |
| `; |
| } |
| |
| private renderLinks() { |
| return this.links.map( |
| link => html` |
| <a |
| href=${link} |
| target="_blank" |
| rel="noopener noreferrer" |
| @click=${this.onLinkClick} |
| @keydown=${this.onLinkKeyDown} |
| aria-label="Link to check details" |
| ><gr-icon icon="open_in_new" class="launch"></gr-icon |
| ></a> |
| ` |
| ); |
| } |
| |
| private onLinkKeyDown(e: KeyboardEvent) { |
| // Prevents onChipKeyDown() from reacting to <a> link keyboard events. |
| e.stopPropagation(); |
| } |
| |
| private onLinkClick(e: MouseEvent) { |
| // Prevents onChipClick() from reacting to <a> link clicks. |
| e.stopPropagation(); |
| this.reporting.reportInteraction(Interaction.CHECKS_CHIP_LINK_CLICKED, { |
| text: this.text, |
| status: this.statusOrCategory, |
| }); |
| } |
| } |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| 'gr-checks-chip': GrChecksChip; |
| } |
| } |