Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
b6791de
update orgs router to allow shared secret when determining org
emma-sg Dec 3, 2025
af7c30d
wip meters updates
emma-sg Nov 24, 2025
9583441
add options & get floating popover working
emma-sg Nov 24, 2025
00eec35
fix inconsistency with execution time in usage history table
emma-sg Nov 24, 2025
acd8ca9
wip
emma-sg Nov 25, 2025
e978f5c
reimplement execution minute meter from scratch
emma-sg Nov 26, 2025
42f64b7
clean up floating popover
emma-sg Nov 26, 2025
5b639be
update floating-ui dep
emma-sg Nov 27, 2025
e04bdf2
move tick to the right of value bar when value is 0
emma-sg Dec 1, 2025
446f173
move relevant styles to meter bar host element
emma-sg Dec 1, 2025
1363990
return "0%" when value is actually 0
emma-sg Dec 1, 2025
6869d3a
fix NaN issue
emma-sg Dec 1, 2025
e6d2505
(wip) redo execution minute meter layout & popovers
emma-sg Dec 1, 2025
e960cb7
[wip] disable virtual element with reference to real element
emma-sg Dec 1, 2025
52f8264
refactor meter implementations to shared structure
emma-sg Dec 2, 2025
90451e8
clean up execution time formatter & skip outdated tests
emma-sg Dec 2, 2025
3654cd7
fix rendering of usage history fields displaying partial minutes
emma-sg Dec 2, 2025
39cd1f3
fix remaining renames from execution second formatter cleanup
emma-sg Dec 2, 2025
58c3337
remove unused virtual element code and clean up floating popover code
emma-sg Dec 2, 2025
605d80b
unify tooltip content for execution minutes meter
emma-sg Dec 2, 2025
f6c0acb
revert & simplify execution minute formatter changes
emma-sg Dec 2, 2025
e3c0b88
update playwright version (was causing ci issues)
emma-sg Dec 2, 2025
4163259
update playwright test dep as well
emma-sg Dec 2, 2025
860edf8
wip 1
emma-sg Dec 2, 2025
d925dbd
reset package.json, yarn.lock, and web-test-runner config to contents
emma-sg Dec 3, 2025
075ff31
redo storage tooltips to match tooltips for exec minutes
emma-sg Dec 3, 2025
03b84fd
fix overlap issue & update meter bar hover styling
emma-sg Dec 3, 2025
97b4e2d
fix misaligned floating popover when locked to an axis of an element
emma-sg Dec 3, 2025
5e3878f
fix accidentally-changed org router dep
emma-sg Dec 3, 2025
1663d89
improve billing/usage skeletons
emma-sg Dec 3, 2025
c719b3e
fix tooltip content edge cases
emma-sg Dec 3, 2025
669c2d1
fix tooltip classes not being wrapped in `tw` helper
emma-sg Dec 3, 2025
ab0b65d
tweak monthly unused colour to match empty meter background
emma-sg Dec 3, 2025
3ef5ecf
remove unneeded empty strings
emma-sg Dec 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
244 changes: 244 additions & 0 deletions frontend/src/components/ui/floating-popover.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
import { type VirtualElement } from "@shoelace-style/shoelace/dist/components/popup/popup.component.js";
import SlTooltip from "@shoelace-style/shoelace/dist/components/tooltip/tooltip.component.js";
import slTooltipStyles from "@shoelace-style/shoelace/dist/components/tooltip/tooltip.styles.js";
import { css, html, type PropertyValues } from "lit";
import { customElement, property } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";

/** Re-implemented from Shoelace, since it's not exported */
function parseDuration(delay: number | string) {
delay = delay.toString().toLowerCase();

if (delay.indexOf("ms") > -1) {
return parseFloat(delay);
}

if (delay.indexOf("s") > -1) {
return parseFloat(delay) * 1000;
}

return parseFloat(delay);
}

/**
* Floating popovers are used to show labels and additional details in data visualizations.
* They're hidden until hover, and follow the cursor within the anchor element.
*
* Importantly, they are not interactive and do not respond to user input via keyboard.
* Their content will not be accessible to screen readers or other assistive technologies.
*
* @attr {String} content
* @attr {String} placement
* @attr {String} distance
* @attr {String} trigger
* @attr {Boolean} open
* @attr {Boolean} disabled
*/
@customElement("btrix-floating-popover")
export class FloatingPopover extends SlTooltip {
@property({ type: Boolean, reflect: true })
hoist = true;

@property({ type: String, reflect: true })
placement: SlTooltip["placement"] = "bottom";

@property({ type: String, reflect: true })
lock: "x" | "y" | "x y" | "" = "y";

clientX: number | null = 0;
clientY: number | null = 0;

isHovered = false;

private get slottedChildren() {
const slot = this.shadowRoot!.querySelector("slot");
return slot?.assignedElements({ flatten: true });
}

get anchor(): VirtualElement {
let originalRect: DOMRect | undefined;
if (this.lock !== "") {
originalRect = this.slottedChildren?.[0].getBoundingClientRect();
}
return {
getBoundingClientRect: () => {
return new DOMRect(
(this.hasLock("x") ? originalRect?.x : this.clientX) ?? 0,
(this.hasLock("y") ? originalRect?.y : this.clientY) ?? 0,
this.hasLock("x") ? originalRect?.width : 0,
this.hasLock("y") ? originalRect?.height : 0,
);
},
};
}

static styles = [
slTooltipStyles,
css`
:host {
--btrix-border: 1px solid var(--sl-color-neutral-300);
--sl-tooltip-border-radius: var(--sl-border-radius-large);
--sl-tooltip-background-color: var(--sl-color-neutral-50);
--sl-tooltip-color: var(--sl-color-neutral-700);
--sl-tooltip-font-size: var(--sl-font-size-x-small);
--sl-tooltip-padding: var(--sl-spacing-small);
--sl-tooltip-line-height: var(--sl-line-height-dense);
}

.tooltip__body {
border: var(--btrix-border);
box-shadow: var(--sl-shadow-small), var(--sl-shadow-large);
}

::part(popup) {
pointer-events: none;
}

::part(arrow) {
z-index: 1;
}

[data-current-placement^="bottom"]::part(arrow),
[data-current-placement^="left"]::part(arrow) {
border-top: var(--btrix-border);
}

[data-current-placement^="bottom"]::part(arrow),
[data-current-placement^="right"]::part(arrow) {
border-left: var(--btrix-border);
}

[data-current-placement^="top"]::part(arrow),
[data-current-placement^="right"]::part(arrow) {
border-bottom: var(--btrix-border);
}

[data-current-placement^="top"]::part(arrow),
[data-current-placement^="left"]::part(arrow) {
border-right: var(--btrix-border);
}
`,
];

constructor() {
super();
this.addEventListener("mouseover", this.overrideHandleMouseOver);
this.addEventListener("mouseout", this.overrideHandleMouseOut);
}

override render() {
return html`
<sl-popup
part="base"
exportparts="
popup:base__popup,
arrow:base__arrow
"
class=${classMap({
tooltip: true,
"tooltip--open": this.open,
})}
placement=${this.placement}
distance=${this.distance}
skidding=${this.skidding}
strategy=${this.hoist ? "fixed" : "absolute"}
flip
shift
arrow
.anchor=${this.anchor}
>
<slot slot="anchor" aria-describedby="tooltip"></slot>

<div
part="body"
id="tooltip"
class="tooltip__body"
role="tooltip"
aria-live=${this.open ? "polite" : "off"}
>
<slot name="content">${this.content}</slot>
</div>
</sl-popup>
`;
}

connectedCallback(): void {
super.connectedCallback();
}

disconnectedCallback(): void {
super.disconnectedCallback();
document.body.removeEventListener("mousemove", this.handleMouseMove);
}

async handleOptionsChange() {
if (this.hasUpdated) {
await this.updateComplete;
this.popup.reposition();
}
}

hasChanged(changedProps: PropertyValues<this>) {
if (
(
[
"content",
"distance",
"hoist",
"placement",
"skidding",
] as (keyof FloatingPopover)[]
).some(changedProps.has)
) {
void this.handleOptionsChange();
}
}

handleMouseMove = (event: MouseEvent) => {
if (this.isHovered) {
this.clientX = event.clientX;
this.clientY = event.clientY;
this.popup.reposition();
}
};

private readonly overrideHandleMouseOver = (event: MouseEvent) => {
if (this.overrideHasTrigger("hover")) {
this.isHovered = true;
this.clientX = event.clientX;
this.clientY = event.clientY;
document.body.addEventListener("mousemove", this.handleMouseMove);
const delay = parseDuration(
getComputedStyle(this).getPropertyValue("--show-delay"),
);
// @ts-expect-error need to access SlTooltip's hoverTimeout
clearTimeout(this.hoverTimeout as number | undefined);
// @ts-expect-error need to access SlTooltip's hoverTimeout
this.hoverTimeout = window.setTimeout(async () => this.show(), delay);
}
};

private readonly overrideHandleMouseOut = () => {
if (this.overrideHasTrigger("hover")) {
this.isHovered = false;
document.body.removeEventListener("mousemove", this.handleMouseMove);
const delay = parseDuration(
getComputedStyle(this).getPropertyValue("--hide-delay"),
);
// @ts-expect-error need to access SlTooltip's hoverTimeout
clearTimeout(this.hoverTimeout as number | undefined);
// @ts-expect-error need to access SlTooltip's hoverTimeout
this.hoverTimeout = window.setTimeout(async () => this.hide(), delay);
}
};

private readonly overrideHasTrigger = (triggerType: string) => {
const triggers = this.trigger.split(" ");
return triggers.includes(triggerType);
};

private readonly hasLock = (lockType: "x" | "y") => {
const locks = this.lock.split(" ");
return locks.includes(lockType);
};
}
1 change: 1 addition & 0 deletions frontend/src/components/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import("./details");
import("./file-input");
import("./file-list");
import("./filter-chip");
import("./floating-popover");
import("./format-date");
import("./inline-input");
import("./language-select");
Expand Down
Loading
Loading