Improve crawl list rendering (#645)

* add load more button

* adjust height

* refactor to improve performance

* remove unused observable component

* contain status

* update dropdown animation
This commit is contained in:
sua yoo 2023-02-28 18:36:23 -08:00 committed by GitHub
parent a1f939ad29
commit f2b7946960
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 266 additions and 184 deletions

View File

@ -12,13 +12,19 @@
* ``` * ```
*/ */
import { LitElement, html, css } from "lit"; import { LitElement, html, css } from "lit";
import { property, queryAssignedElements } from "lit/decorators.js"; import {
property,
query,
queryAssignedElements,
state,
} from "lit/decorators.js";
import { when } from "lit/directives/when.js"; import { when } from "lit/directives/when.js";
import { msg, localized, str } from "@lit/localize"; import { msg, localized, str } from "@lit/localize";
import type { SlIconButton, SlMenu } from "@shoelace-style/shoelace";
import { RelativeDuration } from "./relative-duration"; import { RelativeDuration } from "./relative-duration";
import type { Crawl } from "../types/crawler"; import type { Crawl } from "../types/crawler";
import { srOnly, truncate } from "../utils/css"; import { srOnly, truncate, dropdown } from "../utils/css";
import type { NavigateEvent } from "../utils/LiteElement"; import type { NavigateEvent } from "../utils/LiteElement";
const largeBreakpointCss = css`60rem`; const largeBreakpointCss = css`60rem`;
@ -40,7 +46,7 @@ const rowCss = css`
} }
.col { .col {
grid-column: span 1; grid-column: span 1 / span 1;
} }
`; `;
const columnCss = css` const columnCss = css`
@ -64,6 +70,7 @@ const hostVars = css`
export class CrawlListItem extends LitElement { export class CrawlListItem extends LitElement {
static styles = [ static styles = [
truncate, truncate,
dropdown,
rowCss, rowCss,
columnCss, columnCss,
hostVars, hostVars,
@ -73,12 +80,26 @@ export class CrawlListItem extends LitElement {
} }
.item { .item {
contain: content;
content-visibility: auto;
contain-intrinsic-height: auto 4rem;
cursor: pointer; cursor: pointer;
transition-property: background-color, box-shadow, margin; transition-property: background-color, box-shadow, margin;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms; transition-duration: 150ms;
overflow: hidden;
} }
.item:hover,
.item:focus,
.item:focus-within {
background-color: var(--sl-color-neutral-50);
}
.dropdown {
contain: content;
position: absolute;
z-index: 99;
}
.item:hover { .item:hover {
background-color: var(--sl-color-neutral-50); background-color: var(--sl-color-neutral-50);
margin-left: calc(-1 * var(--row-offset)); margin-left: calc(-1 * var(--row-offset));
@ -115,10 +136,8 @@ export class CrawlListItem extends LitElement {
.detail { .detail {
color: var(--sl-color-neutral-700); color: var(--sl-color-neutral-700);
font-size: var(--sl-font-size-medium); font-size: var(--sl-font-size-medium);
line-height: 1.4;
margin-bottom: var(--sl-spacing-3x-small);
overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
height: 1.5rem;
} }
.desc { .desc {
@ -126,18 +145,13 @@ export class CrawlListItem extends LitElement {
font-size: var(--sl-font-size-x-small); font-size: var(--sl-font-size-x-small);
font-family: var(--font-monostyle-family); font-family: var(--font-monostyle-family);
font-variation-settings: var(--font-monostyle-variation); font-variation-settings: var(--font-monostyle-variation);
line-height: 1.4; height: 1rem;
} }
.unknownValue { .unknownValue {
color: var(--sl-color-neutral-500); color: var(--sl-color-neutral-500);
} }
.name {
overflow: hidden;
text-overflow: ellipsis;
}
.url { .url {
display: flex; display: flex;
} }
@ -181,13 +195,6 @@ export class CrawlListItem extends LitElement {
@media only screen and (min-width: ${largeBreakpointCss}) { @media only screen and (min-width: ${largeBreakpointCss}) {
.action { .action {
border-left: 1px solid var(--sl-panel-border-color); border-left: 1px solid var(--sl-panel-border-color);
display: flex;
align-items: stretch;
}
.action sl-dropdown {
display: flex;
align-items: center;
} }
} }
`, `,
@ -196,7 +203,36 @@ export class CrawlListItem extends LitElement {
@property({ type: Object }) @property({ type: Object })
crawl?: Crawl; crawl?: Crawl;
@query(".dropdown")
dropdown!: HTMLElement;
@query(".dropdownTrigger")
dropdownTrigger!: SlIconButton;
@queryAssignedElements({ selector: "sl-menu", slot: "menu" })
private menuArr!: Array<SlMenu>;
@state()
private dropdownIsOpen?: boolean;
// TODO localize
private numberFormatter = new Intl.NumberFormat();
willUpdate(changedProperties: Map<string, any>) {
if (changedProperties.has("dropdownIsOpen")) {
if (this.dropdownIsOpen) {
this.openDropdown();
} else {
this.closeDropdown();
}
}
}
render() { render() {
return html`${this.renderRow()}${this.renderDropdown()}`;
}
renderRow() {
const isActive = const isActive =
this.crawl && this.crawl &&
["starting", "running", "stopping"].includes(this.crawl.state); ["starting", "running", "stopping"].includes(this.crawl.state);
@ -219,7 +255,9 @@ export class CrawlListItem extends LitElement {
}} }}
> >
<div class="col"> <div class="col">
<div class="detail url">${this.safeRender(this.renderName)}</div> <div class="detail url truncate">
${this.safeRender(this.renderName)}
</div>
<div class="desc"> <div class="desc">
${this.safeRender( ${this.safeRender(
(crawl) => html` (crawl) => html`
@ -265,29 +303,35 @@ export class CrawlListItem extends LitElement {
</div> </div>
<div class="col"> <div class="col">
<div class="detail"> <div class="detail">
${this.safeRender((crawl) => ${this.safeRender(
isActive (crawl) => html`<sl-format-bytes
? html`<span class="unknownValue">${msg("In Progress")}</span>`
: html`<sl-format-bytes
value=${crawl.fileSize || 0} value=${crawl.fileSize || 0}
></sl-format-bytes>` ></sl-format-bytes>`
)} )}
</div> </div>
<div class="desc"> <div class="desc">
${this.safeRender((crawl) => { ${this.safeRender((crawl) => {
const pagesComplete = crawl.stats?.done || 0; const pagesComplete = +(crawl.stats?.done || 0);
if (isActive) { if (isActive) {
const pagesFound = crawl.stats?.found || 0; const pagesFound = +(crawl.stats?.found || 0);
return html` return html`
${+pagesFound === 1 ${pagesFound === 1
? msg(str`${pagesComplete} / ${pagesFound} page`) ? msg(
: msg(str`${pagesComplete} / ${pagesFound} pages`)} str`${this.numberFormatter.format(
pagesComplete
)} / ${this.numberFormatter.format(pagesFound)} page`
)
: msg(
str`${this.numberFormatter.format(
pagesComplete
)} / ${this.numberFormatter.format(pagesFound)} pages`
)}
`; `;
} }
return html` return html`
${+pagesComplete === 1 ${pagesComplete === 1
? msg(str`${pagesComplete} page`) ? msg(str`${this.numberFormatter.format(pagesComplete)} page`)
: msg(str`${pagesComplete} pages`)} : msg(str`${this.numberFormatter.format(pagesComplete)} pages`)}
`; `;
})} })}
</div> </div>
@ -305,27 +349,53 @@ export class CrawlListItem extends LitElement {
</div> </div>
</div> </div>
<div class="col action"> <div class="col action">
<sl-dropdown <sl-icon-button
distance="4" class="dropdownTrigger"
hoist slot="trigger"
name="three-dots-vertical"
label=${msg("More")}
@click=${(e: MouseEvent) => { @click=${(e: MouseEvent) => {
// Prevent anchor link default behavior // Prevent anchor link default behavior
e.preventDefault(); e.preventDefault();
// Stop prop to anchor link // Stop prop to anchor link
e.stopPropagation(); e.stopPropagation();
this.dropdownIsOpen = true;
}}
@focusout=${(e: FocusEvent) => {
const relatedTarget = e.relatedTarget as HTMLElement;
if (this.menuArr[0]?.contains(relatedTarget)) {
// Keep dropdown open if moving to menu selection
return;
}
this.dropdownIsOpen = false;
}} }}
>
<sl-icon-button
slot="trigger"
name="three-dots-vertical"
label=${msg("More")}
></sl-icon-button> ></sl-icon-button>
<slot name="menu"></slot>
</sl-dropdown>
</div> </div>
</a>`; </a>`;
} }
private renderDropdown() {
return html`<div
class="dropdown hidden"
aria-hidden=${!this.dropdownIsOpen}
@animationend=${(e: AnimationEvent) => {
const el = e.target as HTMLDivElement;
if (e.animationName === "dropdownShow") {
el.classList.remove("animateShow");
}
if (e.animationName === "dropdownHide") {
el.classList.add("hidden");
el.classList.remove("animateHide");
}
}}
>
<slot
name="menu"
@sl-select=${() => (this.dropdownIsOpen = false)}
></slot>
</div> `;
}
private safeRender(render: (crawl: Crawl) => any) { private safeRender(render: (crawl: Crawl) => any) {
if (!this.crawl) { if (!this.crawl) {
return html`<sl-skeleton></sl-skeleton>`; return html`<sl-skeleton></sl-skeleton>`;
@ -355,6 +425,22 @@ export class CrawlListItem extends LitElement {
<span class="primaryUrl truncate">${crawl.firstSeed}</span>${nameSuffix} <span class="primaryUrl truncate">${crawl.firstSeed}</span>${nameSuffix}
`; `;
} }
private repositionDropdown() {
const { x, y } = this.dropdownTrigger.getBoundingClientRect();
this.dropdown.style.left = `${x + window.scrollX}px`;
this.dropdown.style.top = `${y + window.scrollY - 8}px`;
}
private openDropdown() {
this.repositionDropdown();
this.dropdown.classList.add("animateShow");
this.dropdown.classList.remove("hidden");
}
private closeDropdown() {
this.dropdown.classList.add("animateHide");
}
} }
@localized() @localized()

View File

@ -16,16 +16,12 @@ export class CrawlStatus extends LitElement {
animatePulse, animatePulse,
css` css`
:host { :host {
contain: content;
display: inline-flex;
align-items: center;
color: var(--sl-color-neutral-700); color: var(--sl-color-neutral-700);
} }
sl-icon,
sl-skeleton,
span {
display: inline-block;
vertical-align: middle;
}
sl-icon { sl-icon {
font-size: 1rem; font-size: 1rem;
margin-right: var(--sl-spacing-x-small); margin-right: var(--sl-spacing-x-small);

View File

@ -11,6 +11,8 @@ import type {
import inputCss from "@shoelace-style/shoelace/dist/components/input/input.styles.js"; import inputCss from "@shoelace-style/shoelace/dist/components/input/input.styles.js";
import debounce from "lodash/fp/debounce"; import debounce from "lodash/fp/debounce";
import { dropdown } from "../utils/css";
export type Tags = string[]; export type Tags = string[];
export type TagsChangeEvent = CustomEvent<{ export type TagsChangeEvent = CustomEvent<{
tags: string[]; tags: string[];
@ -33,13 +35,14 @@ export type TagInputEvent = CustomEvent<{
*/ */
@localized() @localized()
export class TagInput extends LitElement { export class TagInput extends LitElement {
static styles = css` static styles = [
dropdown,
inputCss,
css`
:host { :host {
--tag-height: 1.5rem; --tag-height: 1.5rem;
} }
${inputCss}
.input { .input {
flex-wrap: wrap; flex-wrap: wrap;
height: auto; height: auto;
@ -69,48 +72,6 @@ export class TagInput extends LitElement {
z-index: 2; z-index: 2;
} }
.dropdown {
position: absolute;
transform-origin: top left;
}
.hidden {
display: none;
}
.animateShow {
animation: dropdownShow 100ms ease forwards;
}
.animateHide {
animation: dropdownHide 100ms ease forwards;
}
@keyframes dropdownShow {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes dropdownHide {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.9);
display: none;
}
}
.shake { .shake {
animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both; animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0);
@ -123,24 +84,22 @@ export class TagInput extends LitElement {
90% { 90% {
transform: translate3d(-1px, 0, 0); transform: translate3d(-1px, 0, 0);
} }
20%, 20%,
80% { 80% {
transform: translate3d(2px, 0, 0); transform: translate3d(2px, 0, 0);
} }
30%, 30%,
50%, 50%,
70% { 70% {
transform: translate3d(-3px, 0, 0); transform: translate3d(-3px, 0, 0);
} }
40%, 40%,
60% { 60% {
transform: translate3d(3px, 0, 0); transform: translate3d(3px, 0, 0);
} }
} }
`; `,
];
@property({ type: Array }) @property({ type: Array })
initialTags?: Tags; initialTags?: Tags;

View File

@ -136,9 +136,6 @@ export class CrawlsList extends LiteElement {
private timerId?: number; private timerId?: number;
// TODO localize
private numberFormatter = new Intl.NumberFormat();
private filterCrawls = (crawls: Crawl[]) => private filterCrawls = (crawls: Crawl[]) =>
this.filterByState.length this.filterByState.length
? crawls.filter((crawl) => ? crawls.filter((crawl) =>

View File

@ -43,3 +43,47 @@ export const animatePulse = css`
} }
} }
`; `;
export const dropdown = css`
.dropdown {
contain: content;
position: absolute;
transform-origin: top left;
}
.hidden {
display: none;
}
.animateShow {
animation: dropdownShow 100ms ease forwards;
}
.animateHide {
animation: dropdownHide 100ms ease forwards;
}
@keyframes dropdownShow {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes dropdownHide {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.9);
}
}
`;