Fix crawl list action menu positioning (#1399)
Refactors `btrix-crawl-list` dropdown action menu to use `sl-dropdown` auto-positioning to fix menu clipping
This commit is contained in:
		
							parent
							
								
									b15c5ccddd
								
							
						
					
					
						commit
						ffc8b75ea8
					
				| @ -11,23 +11,22 @@ | |||||||
|  * </btrix-crawl-list> |  * </btrix-crawl-list> | ||||||
|  * ``` |  * ``` | ||||||
|  */ |  */ | ||||||
|  | import type { TemplateResult } from "lit"; | ||||||
| import { LitElement, html, css } from "lit"; | import { LitElement, html, css } from "lit"; | ||||||
| import { | import { | ||||||
|   customElement, |   customElement, | ||||||
|   property, |   property, | ||||||
|   query, |   query, | ||||||
|   queryAssignedElements, |   queryAssignedElements, | ||||||
|   state, |  | ||||||
| } from "lit/decorators.js"; | } from "lit/decorators.js"; | ||||||
| import { msg, localized, str } from "@lit/localize"; | import { msg, localized, str } from "@lit/localize"; | ||||||
| import type { SlMenu } from "@shoelace-style/shoelace"; |  | ||||||
| import queryString from "query-string"; | import queryString from "query-string"; | ||||||
| 
 | 
 | ||||||
| import type { Button } from "./button"; |  | ||||||
| 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, dropdown } from "../utils/css"; | import { srOnly, truncate } from "../utils/css"; | ||||||
| import type { NavigateEvent } from "../utils/LiteElement"; | import type { NavigateEvent } from "../utils/LiteElement"; | ||||||
|  | import type { OverflowDropdown } from "./overflow-dropdown"; | ||||||
| 
 | 
 | ||||||
| const mediumBreakpointCss = css`30rem`; | const mediumBreakpointCss = css`30rem`; | ||||||
| const largeBreakpointCss = css`60rem`; | const largeBreakpointCss = css`60rem`; | ||||||
| @ -75,7 +74,6 @@ 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, | ||||||
| @ -96,12 +94,6 @@ export class CrawlListItem extends LitElement { | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       .dropdown { |  | ||||||
|         contain: content; |  | ||||||
|         position: absolute; |  | ||||||
|         z-index: 99; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       .col { |       .col { | ||||||
|         display: flex; |         display: flex; | ||||||
|         align-items: center; |         align-items: center; | ||||||
| @ -172,10 +164,6 @@ export class CrawlListItem extends LitElement { | |||||||
|         align-items: center; |         align-items: center; | ||||||
|         justify-content: center; |         justify-content: center; | ||||||
|       } |       } | ||||||
| 
 |  | ||||||
|       .action sl-icon-button { |  | ||||||
|         font-size: 1rem; |  | ||||||
|       } |  | ||||||
|     `,
 |     `,
 | ||||||
|   ]; |   ]; | ||||||
| 
 | 
 | ||||||
| @ -194,42 +182,15 @@ export class CrawlListItem extends LitElement { | |||||||
|   @query(".row") |   @query(".row") | ||||||
|   row!: HTMLElement; |   row!: HTMLElement; | ||||||
| 
 | 
 | ||||||
|   // TODO consolidate with btrix-combobox
 |   @query("btrix-overflow-dropdown") | ||||||
|   @query(".dropdown") |   dropdownMenu!: OverflowDropdown; | ||||||
|   dropdown!: HTMLElement; |  | ||||||
| 
 |  | ||||||
|   @query(".dropdownTrigger") |  | ||||||
|   dropdownTrigger!: Button; |  | ||||||
| 
 |  | ||||||
|   @queryAssignedElements({ selector: "sl-menu", slot: "menu" }) |  | ||||||
|   private menuArr!: Array<SlMenu>; |  | ||||||
| 
 |  | ||||||
|   @state() |  | ||||||
|   private dropdownIsOpen?: boolean; |  | ||||||
| 
 |  | ||||||
|   @state() |  | ||||||
|   private hasMenuItems?: boolean; |  | ||||||
| 
 | 
 | ||||||
|   // TODO localize
 |   // TODO localize
 | ||||||
|   private numberFormatter = new Intl.NumberFormat(undefined, { |   private numberFormatter = new Intl.NumberFormat(undefined, { | ||||||
|     notation: "compact", |     notation: "compact", | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   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 search = |     const search = | ||||||
|       this.collectionId || this.workflowId |       this.collectionId || this.workflowId | ||||||
|         ? `?${queryString.stringify( |         ? `?${queryString.stringify( | ||||||
| @ -240,15 +201,16 @@ export class CrawlListItem extends LitElement { | |||||||
|             { skipEmptyString: true } |             { skipEmptyString: true } | ||||||
|           )}` |           )}` | ||||||
|         : ""; |         : ""; | ||||||
|     return html`<a
 |     return html`<div
 | ||||||
|       class="item row" |       class="item row" | ||||||
|       role="button" |       role="button" | ||||||
|       href="/orgs/${this.orgSlug}/items/${this.crawl?.type}/${this.crawl |  | ||||||
|         ?.id}${search}" |  | ||||||
|       @click=${async (e: MouseEvent) => { |       @click=${async (e: MouseEvent) => { | ||||||
|  |         if (e.target === this.dropdownMenu) { | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|         e.preventDefault(); |         e.preventDefault(); | ||||||
|         await this.updateComplete; |         await this.updateComplete; | ||||||
|         const href = (e.currentTarget as HTMLAnchorElement).href; |         const href = `/orgs/${this.orgSlug}/items/${this.crawl?.type}/${this.crawl?.id}${search}`; | ||||||
|         // TODO consolidate with LiteElement navTo
 |         // TODO consolidate with LiteElement navTo
 | ||||||
|         const evt: NavigateEvent = new CustomEvent("navigate", { |         const evt: NavigateEvent = new CustomEvent("navigate", { | ||||||
|           detail: { url: href }, |           detail: { url: href }, | ||||||
| @ -361,33 +323,12 @@ export class CrawlListItem extends LitElement { | |||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|       ${this.renderActions()} |       ${this.renderActions()} | ||||||
|     </a>`;
 |     </div>`;
 | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private renderDropdown() { |   private safeRender( | ||||||
|     return html`<div
 |     render: (crawl: Crawl) => string | TemplateResult<1> | undefined | ||||||
|       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" |  | ||||||
|         @slotchange=${() => (this.hasMenuItems = this.menuArr.length > 0)} |  | ||||||
|         @sl-select=${() => (this.dropdownIsOpen = false)} |  | ||||||
|       ></slot> |  | ||||||
|     </div> `;
 |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   private safeRender(render: (crawl: Crawl) => any) { |  | ||||||
|     if (!this.crawl) { |     if (!this.crawl) { | ||||||
|       return html`<sl-skeleton></sl-skeleton>`; |       return html`<sl-skeleton></sl-skeleton>`; | ||||||
|     } |     } | ||||||
| @ -399,7 +340,7 @@ export class CrawlListItem extends LitElement { | |||||||
|     if (!crawl.firstSeed) |     if (!crawl.firstSeed) | ||||||
|       return html`<span class="truncate">${crawl.id}</span>`; |       return html`<span class="truncate">${crawl.id}</span>`; | ||||||
|     const remainder = crawl.seedCount - 1; |     const remainder = crawl.seedCount - 1; | ||||||
|     let nameSuffix: any = ""; |     let nameSuffix: string | TemplateResult<1> = ""; | ||||||
|     if (remainder) { |     if (remainder) { | ||||||
|       if (remainder === 1) { |       if (remainder === 1) { | ||||||
|         nameSuffix = html`<span class="additionalUrls"
 |         nameSuffix = html`<span class="additionalUrls"
 | ||||||
| @ -417,56 +358,19 @@ export class CrawlListItem extends LitElement { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private renderActions() { |   private renderActions() { | ||||||
|     if (!this.hasMenuItems) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return html` <div class="col action">
 |     return html` <div class="col action">
 | ||||||
|       <sl-icon-button |       <btrix-overflow-dropdown> | ||||||
|         class="dropdownTrigger" |         <slot | ||||||
|         label=${msg("Actions")} |           name="menu" | ||||||
|         name="three-dots-vertical" |           @click=${(e: MouseEvent) => { | ||||||
|         @click=${(e: MouseEvent) => { |             // Prevent navigation to detail view
 | ||||||
|           // Prevent anchor link default behavior
 |             e.preventDefault(); | ||||||
|           e.preventDefault(); |             e.stopPropagation(); | ||||||
|           // Stop prop to anchor link
 |           }} | ||||||
|           e.stopPropagation(); |         ></slot> | ||||||
|           this.dropdownIsOpen = !this.dropdownIsOpen; |       </btrix-overflow-dropdown> | ||||||
|         }} |  | ||||||
|         @focusout=${(e: FocusEvent) => { |  | ||||||
|           const relatedTarget = e.relatedTarget as HTMLElement; |  | ||||||
|           if (relatedTarget) { |  | ||||||
|             if (this.menuArr[0]?.contains(relatedTarget)) { |  | ||||||
|               // Keep dropdown open if moving to menu selection
 |  | ||||||
|               return; |  | ||||||
|             } |  | ||||||
|             if (this.row?.isEqualNode(relatedTarget)) { |  | ||||||
|               // Handle with click event
 |  | ||||||
|               return; |  | ||||||
|             } |  | ||||||
|           } |  | ||||||
|           this.dropdownIsOpen = false; |  | ||||||
|         }} |  | ||||||
|       > |  | ||||||
|       </sl-icon-button> |  | ||||||
|     </div>`;
 |     </div>`;
 | ||||||
|   } |   } | ||||||
| 
 |  | ||||||
|   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() | ||||||
|  | |||||||
| @ -48,3 +48,4 @@ import("./code"); | |||||||
| import("./pw-strength-alert"); | import("./pw-strength-alert"); | ||||||
| import("./search-combobox"); | import("./search-combobox"); | ||||||
| import("./meter"); | import("./meter"); | ||||||
|  | import("./overflow-dropdown"); | ||||||
|  | |||||||
							
								
								
									
										57
									
								
								frontend/src/components/overflow-dropdown.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								frontend/src/components/overflow-dropdown.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,57 @@ | |||||||
|  | import { LitElement, html, css } from "lit"; | ||||||
|  | import { customElement, state, queryAssignedElements } from "lit/decorators.js"; | ||||||
|  | import { msg, localized } from "@lit/localize"; | ||||||
|  | import type { SlMenu } from "@shoelace-style/shoelace"; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Dropdown for additional actions. | ||||||
|  |  * | ||||||
|  |  * Usage: | ||||||
|  |  * ```ts
 | ||||||
|  |  * <btrix-overflow-dropdown> | ||||||
|  |  *   <sl-menu> | ||||||
|  |  *     <sl-menu-item>Item 1</sl-menu-item> | ||||||
|  |  *     <sl-menu-item>Item 2</sl-menu-item> | ||||||
|  |  *   </sl-menu> | ||||||
|  |  *< /btrix-overflow-dropdown> | ||||||
|  |  * ``` | ||||||
|  |  */ | ||||||
|  | @localized() | ||||||
|  | @customElement("btrix-overflow-dropdown") | ||||||
|  | export class OverflowDropdown extends LitElement { | ||||||
|  |   static style = [ | ||||||
|  |     css` | ||||||
|  |       .trigger { | ||||||
|  |         font-size: 1rem; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       .trigger[disabled] { | ||||||
|  |         visibility: hidden; | ||||||
|  |       } | ||||||
|  |     `,
 | ||||||
|  |   ]; | ||||||
|  | 
 | ||||||
|  |   @state() | ||||||
|  |   private hasMenuItems?: boolean; | ||||||
|  | 
 | ||||||
|  |   @queryAssignedElements({ selector: "sl-menu", flatten: true }) | ||||||
|  |   private menu!: Array<SlMenu>; | ||||||
|  | 
 | ||||||
|  |   render() { | ||||||
|  |     return html` | ||||||
|  |       <sl-dropdown ?disabled=${!this.hasMenuItems}> | ||||||
|  |         <sl-icon-button | ||||||
|  |           slot="trigger" | ||||||
|  |           class="trigger" | ||||||
|  |           label=${msg("Actions")} | ||||||
|  |           name="three-dots-vertical" | ||||||
|  |           ?disabled=${!this.hasMenuItems} | ||||||
|  |         > | ||||||
|  |         </sl-icon-button> | ||||||
|  |         <slot | ||||||
|  |           @slotchange=${() => (this.hasMenuItems = this.menu.length > 0)} | ||||||
|  |         ></slot> | ||||||
|  |       </sl-dropdown> | ||||||
|  |     `;
 | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -11,23 +11,23 @@ | |||||||
|  * </btrix-workflow-list> |  * </btrix-workflow-list> | ||||||
|  * ``` |  * ``` | ||||||
|  */ |  */ | ||||||
|  | import type { TemplateResult } from "lit"; | ||||||
| import { LitElement, html, css } from "lit"; | import { LitElement, html, css } from "lit"; | ||||||
| import { | import { | ||||||
|   property, |   property, | ||||||
|   query, |   query, | ||||||
|   queryAssignedElements, |   queryAssignedElements, | ||||||
|   state, |  | ||||||
|   customElement, |   customElement, | ||||||
| } from "lit/decorators.js"; | } from "lit/decorators.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 { ListWorkflow } from "../types/crawler"; | import type { ListWorkflow } from "../types/crawler"; | ||||||
| import { srOnly, truncate, dropdown } from "../utils/css"; | import { srOnly, truncate } from "../utils/css"; | ||||||
| import type { NavigateEvent } from "../utils/LiteElement"; | import type { NavigateEvent } from "../utils/LiteElement"; | ||||||
| import { humanizeSchedule } from "../utils/cron"; | import { humanizeSchedule } from "../utils/cron"; | ||||||
| import { numberFormatter } from "../utils/number"; | import { numberFormatter } from "../utils/number"; | ||||||
|  | import type { OverflowDropdown } from "./overflow-dropdown"; | ||||||
| 
 | 
 | ||||||
| const mediumBreakpointCss = css`30rem`; | const mediumBreakpointCss = css`30rem`; | ||||||
| const largeBreakpointCss = css`60rem`; | const largeBreakpointCss = css`60rem`; | ||||||
| @ -74,7 +74,6 @@ const hostVars = css` | |||||||
| export class WorkflowListItem extends LitElement { | export class WorkflowListItem extends LitElement { | ||||||
|   static styles = [ |   static styles = [ | ||||||
|     truncate, |     truncate, | ||||||
|     dropdown, |  | ||||||
|     rowCss, |     rowCss, | ||||||
|     columnCss, |     columnCss, | ||||||
|     hostVars, |     hostVars, | ||||||
| @ -84,8 +83,7 @@ export class WorkflowListItem extends LitElement { | |||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       .item { |       .item { | ||||||
|         contain: content; |         contain: size; | ||||||
|         content-visibility: auto; |  | ||||||
|         contain-intrinsic-height: auto 4rem; |         contain-intrinsic-height: auto 4rem; | ||||||
|         cursor: pointer; |         cursor: pointer; | ||||||
|         transition-property: background-color, box-shadow, margin; |         transition-property: background-color, box-shadow, margin; | ||||||
| @ -99,11 +97,7 @@ export class WorkflowListItem extends LitElement { | |||||||
|       .item:focus-within { |       .item:focus-within { | ||||||
|         background-color: var(--sl-color-neutral-50); |         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)); | ||||||
| @ -196,10 +190,6 @@ export class WorkflowListItem extends LitElement { | |||||||
|         justify-content: center; |         justify-content: center; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       .action sl-icon-button { |  | ||||||
|         font-size: 1rem; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       @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); | ||||||
| @ -217,52 +207,30 @@ export class WorkflowListItem extends LitElement { | |||||||
|   @query(".row") |   @query(".row") | ||||||
|   row!: HTMLElement; |   row!: HTMLElement; | ||||||
| 
 | 
 | ||||||
|   // TODO consolidate with btrix-combobox
 |   @query("btrix-overflow-dropdown") | ||||||
|   @query(".dropdown") |   dropdownMenu!: OverflowDropdown; | ||||||
|   dropdown!: HTMLElement; |  | ||||||
| 
 |  | ||||||
|   @query(".dropdownTrigger") |  | ||||||
|   dropdownTrigger!: SlIconButton; |  | ||||||
| 
 |  | ||||||
|   @queryAssignedElements({ selector: "sl-menu", slot: "menu" }) |  | ||||||
|   private menuArr!: Array<SlMenu>; |  | ||||||
| 
 |  | ||||||
|   @state() |  | ||||||
|   private dropdownIsOpen?: boolean; |  | ||||||
| 
 | 
 | ||||||
|   private numberFormatter = numberFormatter(undefined, { |   private numberFormatter = numberFormatter(undefined, { | ||||||
|     notation: "compact", |     notation: "compact", | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   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 notSpecified = html`<span class="notSpecified" role="presentation"
 |     const notSpecified = html`<span class="notSpecified" role="presentation"
 | ||||||
|       >---</span |       >---</span | ||||||
|     >`;
 |     >`;
 | ||||||
| 
 | 
 | ||||||
|     return html`<a
 |     return html`<div
 | ||||||
|       class="item row" |       class="item row" | ||||||
|       role="button" |       role="button" | ||||||
|       href=${`/orgs/${this.orgSlug}/workflows/crawl/${this.workflow?.id}#${ |  | ||||||
|         this.workflow?.isCrawlRunning ? "watch" : "crawls" |  | ||||||
|       }`}
 |  | ||||||
|       @click=${async (e: MouseEvent) => { |       @click=${async (e: MouseEvent) => { | ||||||
|  |         if (e.target === this.dropdownMenu) { | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|         e.preventDefault(); |         e.preventDefault(); | ||||||
|         await this.updateComplete; |         await this.updateComplete; | ||||||
|         const href = (e.currentTarget as HTMLAnchorElement).href; |         const href = `/orgs/${this.orgSlug}/workflows/crawl/${ | ||||||
|  |           this.workflow?.id | ||||||
|  |         }#${this.workflow?.isCrawlRunning ? "watch" : "crawls"}`;
 | ||||||
|         // TODO consolidate with LiteElement navTo
 |         // TODO consolidate with LiteElement navTo
 | ||||||
|         const evt: NavigateEvent = new CustomEvent("navigate", { |         const evt: NavigateEvent = new CustomEvent("navigate", { | ||||||
|           detail: { url: href }, |           detail: { url: href }, | ||||||
| @ -422,59 +390,23 @@ export class WorkflowListItem extends LitElement { | |||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|       <div class="col action"> |       <div class="col action"> | ||||||
|         <sl-icon-button |         <btrix-overflow-dropdown> | ||||||
|           class="dropdownTrigger" |           <slot | ||||||
|           name="three-dots-vertical" |             name="menu" | ||||||
|           label=${msg("Actions")} |             @click=${(e: MouseEvent) => { | ||||||
|           @click=${(e: MouseEvent) => { |               // Prevent navigation to detail view
 | ||||||
|             // Prevent anchor link default behavior
 |               e.preventDefault(); | ||||||
|             e.preventDefault(); |               e.stopPropagation(); | ||||||
|             // Stop prop to anchor link
 |             }} | ||||||
|             e.stopPropagation(); |           ></slot> | ||||||
|             this.dropdownIsOpen = !this.dropdownIsOpen; |         </btrix-overflow-dropdown> | ||||||
|           }} |  | ||||||
|           @focusout=${(e: FocusEvent) => { |  | ||||||
|             const relatedTarget = e.relatedTarget as HTMLElement; |  | ||||||
|             if (relatedTarget) { |  | ||||||
|               if (this.menuArr[0]?.contains(relatedTarget)) { |  | ||||||
|                 // Keep dropdown open if moving to menu selection
 |  | ||||||
|                 return; |  | ||||||
|               } |  | ||||||
|               if (this.row?.isEqualNode(relatedTarget)) { |  | ||||||
|                 // Handle with click event
 |  | ||||||
|                 return; |  | ||||||
|               } |  | ||||||
|             } |  | ||||||
|             this.dropdownIsOpen = false; |  | ||||||
|           }} |  | ||||||
|         ></sl-icon-button> |  | ||||||
|       </div> |       </div> | ||||||
|     </a>`;
 |     </div>`;
 | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private renderDropdown() { |   private safeRender( | ||||||
|     return html`<div
 |     render: (workflow: ListWorkflow) => string | TemplateResult<1> | ||||||
|       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: (workflow: ListWorkflow) => any) { |  | ||||||
|     if (!this.workflow) { |     if (!this.workflow) { | ||||||
|       return html`<sl-skeleton></sl-skeleton>`; |       return html`<sl-skeleton></sl-skeleton>`; | ||||||
|     } |     } | ||||||
| @ -488,7 +420,7 @@ export class WorkflowListItem extends LitElement { | |||||||
|     if (!workflow.firstSeed) |     if (!workflow.firstSeed) | ||||||
|       return html`<span class="truncate">${workflow.id}</span>`; |       return html`<span class="truncate">${workflow.id}</span>`; | ||||||
|     const remainder = workflow.seedCount - 1; |     const remainder = workflow.seedCount - 1; | ||||||
|     let nameSuffix: any = ""; |     let nameSuffix: string | TemplateResult<1> = ""; | ||||||
|     if (remainder) { |     if (remainder) { | ||||||
|       if (remainder === 1) { |       if (remainder === 1) { | ||||||
|         nameSuffix = html`<span class="additionalUrls"
 |         nameSuffix = html`<span class="additionalUrls"
 | ||||||
| @ -505,22 +437,6 @@ export class WorkflowListItem extends LitElement { | |||||||
|       >${nameSuffix} |       >${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() | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user