go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/common/components/changelists_badge/changelists_badge.tsx (about) 1 // Copyright 2023 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 import { MobxLitElement } from '@adobe/lit-mobx'; 16 import createCache from '@emotion/cache'; 17 import { CacheProvider, EmotionCache } from '@emotion/react'; 18 import { Chip } from '@mui/material'; 19 import { html, render } from 'lit'; 20 import { customElement } from 'lit/decorators.js'; 21 import { makeObservable, observable } from 'mobx'; 22 import { createRef } from 'react'; 23 import { createRoot, Root } from 'react-dom/client'; 24 25 import './changelists_tooltip'; 26 import { 27 HideTooltipEventDetail, 28 ShowTooltipEventDetail, 29 } from '@/common/components/tooltip'; 30 import { Changelist } from '@/common/services/luci_analysis'; 31 import { commonStyles } from '@/common/styles/stylesheets'; 32 33 import { getClLabel, getClLink } from './changelists_tooltip'; 34 35 export interface ChangelistBadgeProps { 36 readonly changelists: readonly Changelist[]; 37 } 38 39 export function ChangelistsBadge({ changelists }: ChangelistBadgeProps) { 40 const badgeRef = createRef<HTMLAnchorElement>(); 41 const firstCl = changelists[0]; 42 if (!firstCl) { 43 return <></>; 44 } 45 46 const hasMultipleCls = changelists.length > 1; 47 48 return ( 49 <Chip 50 label={`${getClLabel(firstCl)}${hasMultipleCls ? ', ...' : ''}`} 51 size="small" 52 component="a" 53 target="_blank" 54 href={getClLink(firstCl)} 55 clickable 56 ref={badgeRef} 57 onMouseOver={() => { 58 if (!hasMultipleCls) { 59 return; 60 } 61 const tooltip = document.createElement('div'); 62 render( 63 html` 64 <milo-changelists-tooltip 65 .changelists=${changelists} 66 ></milo-changelists-tooltip> 67 `, 68 tooltip, 69 ); 70 window.dispatchEvent( 71 new CustomEvent<ShowTooltipEventDetail>('show-tooltip', { 72 detail: { 73 tooltip, 74 targetRect: badgeRef.current!.getBoundingClientRect(), 75 gapSize: 2, 76 }, 77 }), 78 ); 79 }} 80 onMouseOut={() => { 81 if (!hasMultipleCls) { 82 return; 83 } 84 window.dispatchEvent( 85 new CustomEvent<HideTooltipEventDetail>('hide-tooltip', { 86 detail: { delay: 50 }, 87 }), 88 ); 89 }} 90 onClick={(e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => 91 e.stopPropagation() 92 } 93 /> 94 ); 95 } 96 97 @customElement('milo-changelists-badge') 98 export class ChangelistsBadgeElement extends MobxLitElement { 99 @observable.ref changelists!: readonly Changelist[]; 100 101 private readonly cache: EmotionCache; 102 private readonly parent: HTMLSpanElement; 103 private readonly root: Root; 104 105 constructor() { 106 super(); 107 makeObservable(this); 108 this.parent = document.createElement('span'); 109 const child = document.createElement('span'); 110 this.root = createRoot(child); 111 this.parent.appendChild(child); 112 this.cache = createCache({ 113 key: 'milo-changelists-badge', 114 container: this.parent, 115 }); 116 } 117 118 protected render() { 119 this.root.render( 120 <CacheProvider value={this.cache}> 121 <ChangelistsBadge changelists={this.changelists} /> 122 </CacheProvider>, 123 ); 124 return this.parent; 125 } 126 127 static styles = [commonStyles]; 128 }