mirror of
https://github.com/we-promise/sure.git
synced 2026-09-02 13:21:17 +00:00
* fix(ui): resolve DS Drift Patrol findings from #2745 Replace hand-rolled queue status chips and the insights nav tooltip/badge with DS::Pill and DS::Tooltip. Co-authored-by: Cursor <cursoragent@cursor.com> * i18n: use a single key for background job queue pill labels * fix(ui): address gariasf review on insights badge and queue pills Keep the unread insights count on theme-aware inverse tokens (DS::Pill filled neutral is not dark-mode safe). Extend DS::Tooltip with html_class passthrough and a/summary focus ancestors so icon-only links keep full hover + keyboard reveal. Add queue_label locales (de/uk/zh-TW), drop the dead latency keys, and restore mono via DS::Pill mono: true. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
125 lines
3.8 KiB
JavaScript
125 lines
3.8 KiB
JavaScript
import {
|
|
autoUpdate,
|
|
computePosition,
|
|
flip,
|
|
offset,
|
|
shift,
|
|
} from "@floating-ui/dom";
|
|
import { Controller } from "@hotwired/stimulus";
|
|
|
|
export default class extends Controller {
|
|
static targets = ["tooltip"];
|
|
static values = {
|
|
placement: { type: String, default: "top" },
|
|
offset: { type: Number, default: 10 },
|
|
crossAxis: { type: Number, default: 0 },
|
|
};
|
|
|
|
connect() {
|
|
this._cleanup = null;
|
|
this.boundUpdate = this.update.bind(this);
|
|
this.addEventListeners();
|
|
}
|
|
|
|
disconnect() {
|
|
this.removeEventListeners();
|
|
this.stopAutoUpdate();
|
|
}
|
|
|
|
addEventListeners() {
|
|
this.element.addEventListener("mouseenter", this.show);
|
|
this.element.addEventListener("mouseleave", this.hide);
|
|
// Keyboard parity: keyboard users hit the trigger via Tab + focus,
|
|
// not hover. Without these the tooltip never appears for them.
|
|
this.element.addEventListener("focusin", this.show);
|
|
this.element.addEventListener("focusout", this.hide);
|
|
// Esc-to-dismiss matches the WAI-ARIA Authoring Practices for the
|
|
// tooltip pattern.
|
|
this.element.addEventListener("keydown", this.handleKeydown);
|
|
|
|
// `as: :span` renders a non-focusable trigger inside an
|
|
// already-focusable ancestor (`<summary>`, icon-only `<a>`, …).
|
|
// When the ancestor receives keyboard focus the `focusin` event
|
|
// fires on *it* and bubbles UP to the document — it never reaches
|
|
// a descendant span. Without a listener on the ancestor itself,
|
|
// the tooltip stays hidden for keyboard users. Bind the same
|
|
// handlers on the closest interactive ancestor (if any) so
|
|
// focusing it reveals the tooltip and Esc still dismisses it.
|
|
this.focusableAncestor = this.element.closest("summary, a");
|
|
if (this.focusableAncestor) {
|
|
this.focusableAncestor.addEventListener("focusin", this.show);
|
|
this.focusableAncestor.addEventListener("focusout", this.hide);
|
|
this.focusableAncestor.addEventListener("keydown", this.handleKeydown);
|
|
}
|
|
}
|
|
|
|
removeEventListeners() {
|
|
this.element.removeEventListener("mouseenter", this.show);
|
|
this.element.removeEventListener("mouseleave", this.hide);
|
|
this.element.removeEventListener("focusin", this.show);
|
|
this.element.removeEventListener("focusout", this.hide);
|
|
this.element.removeEventListener("keydown", this.handleKeydown);
|
|
|
|
if (this.focusableAncestor) {
|
|
this.focusableAncestor.removeEventListener("focusin", this.show);
|
|
this.focusableAncestor.removeEventListener("focusout", this.hide);
|
|
this.focusableAncestor.removeEventListener("keydown", this.handleKeydown);
|
|
this.focusableAncestor = null;
|
|
}
|
|
}
|
|
|
|
show = () => {
|
|
this.tooltipTarget.classList.remove("hidden");
|
|
this.startAutoUpdate();
|
|
this.update();
|
|
};
|
|
|
|
hide = () => {
|
|
this.tooltipTarget.classList.add("hidden");
|
|
this.stopAutoUpdate();
|
|
};
|
|
|
|
handleKeydown = (event) => {
|
|
if (event.key === "Escape" && !this.tooltipTarget.classList.contains("hidden")) {
|
|
this.hide();
|
|
}
|
|
};
|
|
|
|
startAutoUpdate() {
|
|
if (!this._cleanup) {
|
|
const reference = this.element.querySelector("[data-icon]");
|
|
this._cleanup = autoUpdate(
|
|
reference || this.element,
|
|
this.tooltipTarget,
|
|
this.boundUpdate
|
|
);
|
|
}
|
|
}
|
|
|
|
stopAutoUpdate() {
|
|
if (this._cleanup) {
|
|
this._cleanup();
|
|
this._cleanup = null;
|
|
}
|
|
}
|
|
|
|
update() {
|
|
const reference = this.element.querySelector("[data-icon]");
|
|
computePosition(reference || this.element, this.tooltipTarget, {
|
|
placement: this.placementValue,
|
|
middleware: [
|
|
offset({
|
|
mainAxis: this.offsetValue,
|
|
crossAxis: this.crossAxisValue,
|
|
}),
|
|
flip(),
|
|
shift({ padding: 5 }),
|
|
],
|
|
}).then(({ x, y }) => {
|
|
Object.assign(this.tooltipTarget.style, {
|
|
left: `${x}px`,
|
|
top: `${y}px`,
|
|
});
|
|
});
|
|
}
|
|
} |