From 1334a601766871ab87ad2dcc17ef03cfbc879234 Mon Sep 17 00:00:00 2001 From: Mika Pons Date: Mon, 7 Sep 2026 10:52:44 +0200 Subject: [PATCH 1/9] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20search=20sh?= =?UTF-8?q?ortcuts,=20styling=20and=20lifecycle=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../graphs/overview/overview.component.ts | 34 ++- .../questions-chooser.component.html | 15 +- .../questions-chooser.component.scss | 65 +++--- .../questions-chooser.component.ts | 201 ++++++++++++------ 4 files changed, 217 insertions(+), 98 deletions(-) diff --git a/src/app/components/graphs/overview/overview.component.ts b/src/app/components/graphs/overview/overview.component.ts index a4f950e..7a29d8f 100644 --- a/src/app/components/graphs/overview/overview.component.ts +++ b/src/app/components/graphs/overview/overview.component.ts @@ -269,8 +269,10 @@ export class OverviewComponent @HostListener("document:keydown", ["$event"]) handleGlobalShortcuts(event: KeyboardEvent) { - if (document.body.classList.contains("modal-open")) return; - if (OsUtils.isTypingInInput(event)) { + if ( + document.body.classList.contains("modal-open") || + document.querySelector(".modal.show, .custom-modal-backdrop") !== null + ) { return; } @@ -280,7 +282,27 @@ export class OverviewComponent event.preventDefault(); this.clearQuestionsFilter(); this.triggerShortcut("escape"); - } else if (key === "r") { + return; + } + + if (OsUtils.isTypingInInput(event)) { + return; + } + + if ( + key === "f" && + !event.ctrlKey && + !event.metaKey && + !event.altKey + ) { + event.preventDefault(); + if (this.questionsChooser && typeof this.questionsChooser.focus === "function") { + this.questionsChooser.focus(); + } + return; + } + + if (key === "r") { event.preventDefault(); this.loadGraph(this.dataService.startDate, this.dataService.endDate); this.triggerShortcut("r"); @@ -2720,7 +2742,11 @@ export class OverviewComponent clearQuestionsFilter() { if (this.questionsChooser) { - this.questionsChooser.clearAll(); + if (typeof this.questionsChooser.handleEscape === "function") { + this.questionsChooser.handleEscape(); + } else { + this.questionsChooser.clearAll(); + } } } diff --git a/src/app/components/questions-chooser/questions-chooser.component.html b/src/app/components/questions-chooser/questions-chooser.component.html index 06e6c29..d437729 100644 --- a/src/app/components/questions-chooser/questions-chooser.component.html +++ b/src/app/components/questions-chooser/questions-chooser.component.html @@ -10,8 +10,7 @@ *ngIf="items.length > 0" (dragover)="onContainerDragOver($event)" (dragleave)="onContainerDragLeave($event)" - (drop)="onContainerDrop($event)" - style="position: relative;"> + (drop)="onContainerDrop($event)"> @@ -182,8 +181,8 @@ (mousedown)="$event.stopPropagation(); $event.preventDefault()" (click)="$event.stopPropagation()">
-
- +
+ @@ -346,18 +345,18 @@
-
-
+
+
! {{ 'QUESTIONS-CHOOSER-CHEAT-EXCLUDE' | translate }}
-
+
⇥ Tab {{ 'QUESTIONS-CHOOSER-CHEAT-SUGGESTIONS' | translate }}
-
+
? {{ 'QUESTIONS-CHOOSER-CHEAT-HELP' | translate }}
diff --git a/src/app/components/questions-chooser/questions-chooser.component.scss b/src/app/components/questions-chooser/questions-chooser.component.scss index 99c8f75..c1b6881 100644 --- a/src/app/components/questions-chooser/questions-chooser.component.scss +++ b/src/app/components/questions-chooser/questions-chooser.component.scss @@ -19,7 +19,7 @@ position: relative; &.disabled { - background-color: #e9ecef; + background-color: var(--color-hover); cursor: not-allowed; } } @@ -39,6 +39,7 @@ height: 100%; min-width: 0; /* Prevents intrinsic content size from overriding flex constraints */ box-sizing: border-box; + position: relative; /* Hide scrollbar for Chrome, Safari and Opera */ &::-webkit-scrollbar { @@ -52,7 +53,7 @@ .ghost-preview { opacity: 0.5; pointer-events: none; - box-shadow: 0 0 0 1px #3b82f6 inset; + box-shadow: 0 0 0 1px var(--color-primary) inset; } // ====================== @@ -62,8 +63,8 @@ .pill { display: inline-flex; align-items: center; - background-color: #eff6ff; - color: #1d4ed8; + background-color: var(--color-primary-bg); + color: var(--color-primary); border-radius: 9999px; padding: 0.1rem 0.5rem; margin-right: 0.5rem; @@ -104,7 +105,7 @@ margin-left: 0.4rem; cursor: pointer; opacity: 0.5; - color: #1d4ed8; + color: var(--color-primary); transition: opacity 150ms ease-out, transform 150ms ease-out; position: relative; @@ -144,8 +145,8 @@ } &.selected-pill { - border-color: #1d4ed8; - box-shadow: 0 0 0 1px #1d4ed8; + border-color: var(--color-primary); + box-shadow: 0 0 0 1px var(--color-primary); &.commit-pill { border-color: var(--color-text-secondary, #475569); @@ -217,7 +218,7 @@ opacity: 0.85; &.question-icon { - color: #1d4ed8; + color: var(--color-primary); } &.commit-icon { @@ -388,6 +389,14 @@ border-bottom: 1px solid var(--color-border); margin-bottom: 10px; + .quick-help-title-wrap { + gap: 6px; + + .quick-help-icon { + color: var(--color-primary); + } + } + .quick-help-title { font-size: 0.82rem; color: var(--color-text-primary); @@ -573,6 +582,11 @@ color: var(--color-text-muted); pointer-events: none; background: transparent; + gap: 8px; + + .cheat-group { + gap: 4px; + } .cheat-label { font-size: 0.68rem; @@ -592,40 +606,40 @@ :host-context(body.dark-theme) { .pill { - background-color: rgba(96, 165, 250, 0.15); - color: #60a5fa; + background-color: var(--color-primary-bg); + color: var(--color-primary); - .close-icon { color: #60a5fa; } + .close-icon { color: var(--color-primary); } &.selected-pill { - border-color: #60a5fa; - box-shadow: 0 0 0 1px #60a5fa; + border-color: var(--color-primary); + box-shadow: 0 0 0 1px var(--color-primary); } &.commit-pill { background-color: transparent; - color: var(--color-text-secondary, #94a3b8); - border-color: var(--color-border, #475569); + color: var(--color-text-secondary); + border-color: var(--color-border); &.selected-pill { - border-color: var(--color-text-secondary, #94a3b8); - box-shadow: 0 0 0 1px var(--color-text-secondary, #94a3b8); + border-color: var(--color-text-secondary); + box-shadow: 0 0 0 1px var(--color-text-secondary); } - .close-icon { color: var(--color-text-secondary, #94a3b8); } + .close-icon { color: var(--color-text-secondary); } } } .pill-icon.question-icon { - color: #60a5fa; + color: var(--color-primary); } .pill-icon.commit-icon { - color: var(--color-text-secondary, #94a3b8); + color: var(--color-text-secondary); } .connector-icon { - background: #3b82f6; + background: var(--color-primary); } .connector-icon.unlink-icon { @@ -633,7 +647,7 @@ } .pill.merged-right:not(.commit-pill) { - border-right-color: var(--color-border, #475569); + border-right-color: var(--color-border); } .pill-connector.merged { @@ -658,10 +672,9 @@ .quick-help-popover { .help-feature-card { - background: rgba(255, 255, 255, 0.04); - border-color: rgba(255, 255, 255, 0.08); + background: var(--color-hover); + border-color: var(--color-border); } - } .transparent-input { @@ -680,7 +693,7 @@ min-width: 50px; padding: 0; margin: 0; - color: #495057; + color: var(--color-text-primary); width: 100%; &:focus { diff --git a/src/app/components/questions-chooser/questions-chooser.component.ts b/src/app/components/questions-chooser/questions-chooser.component.ts index 7bc00e8..67fff58 100644 --- a/src/app/components/questions-chooser/questions-chooser.component.ts +++ b/src/app/components/questions-chooser/questions-chooser.component.ts @@ -10,13 +10,15 @@ import { QueryList, ElementRef, HostListener, + ChangeDetectorRef, + Optional, } from "@angular/core"; -import { ChangeDetectorRef } from "@angular/core"; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms"; import { NgbTypeahead } from "@ng-bootstrap/ng-bootstrap"; import { merge, Observable, Subject } from "rxjs"; import { filter, map } from "rxjs/operators"; import { OsUtils } from "@utils/os.utils"; +import { CustomModalService } from "@shared/ui/custom-modal/custom-modal.service"; export interface FilterGroup { criteria: { @@ -71,7 +73,6 @@ export class QuestionsChooserComponent disabled: boolean; - pressedShortcut: string = null; showQuickHelp = false; helpShiftX = 0; helpShiftY = 0; @@ -133,13 +134,54 @@ export class QuestionsChooserComponent } } + private isModalOpen(): boolean { + if (this.customModalService && this.customModalService.hasOpenModals()) { + return true; + } + if (document.body.classList.contains("modal-open")) { + return true; + } + return !!document.querySelector(".modal.show, .custom-modal-backdrop"); + } + + handleEscape(event?: KeyboardEvent) { + if (this.isModalOpen()) return; + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + + if (this.showQuickHelp) { + this.closeQuickHelp(); + } + if (this.instance && this.instance.isPopupOpen()) { + this.instance.dismissPopup(); + } + this.clearAll(); + if (this.inputField && this.inputField.nativeElement) { + this.inputField.nativeElement.blur(); + } + this.selectedPillIndex = null; + this.editingPillIndex = null; + this.cdr.markForCheck(); + } + + private getTypeaheadElement(): HTMLElement | null { + const ownsId = this.inputField?.nativeElement?.getAttribute("aria-owns"); + if (ownsId) { + const el = document.getElementById(ownsId); + if (el) return el; + } + return document.querySelector("ngb-typeahead-window") as HTMLElement; + } + updateHelpPosition() { if (!this.showQuickHelp) { this.resetHelpPosition(); return; } - const typeaheadEl = document.querySelector("ngb-typeahead-window") as HTMLElement; + const typeaheadEl = this.getTypeaheadElement(); const isTypeaheadVisible = typeaheadEl && ((this.instance && this.instance.isPopupOpen()) || @@ -156,7 +198,8 @@ export class QuestionsChooserComponent const typeaheadRect = typeaheadEl.getBoundingClientRect(); const containerRect = containerEl.getBoundingClientRect(); - const helpWidth = 360; + const helpEl = this.quickHelpPopover ? this.quickHelpPopover.nativeElement : null; + const helpWidth = helpEl && helpEl.offsetWidth > 0 ? helpEl.offsetWidth : Math.min(360, window.innerWidth - 32); // Resting position of quick-help-popover relative to viewport (top: calc(100% + 8px), right: 0 relative to pseudo-input-container): const restingTop = containerRect.bottom + 8; @@ -247,14 +290,18 @@ export class QuestionsChooserComponent }); } - const typeaheadEl = document.querySelector("ngb-typeahead-window") as HTMLElement; + const typeaheadEl = this.getTypeaheadElement(); if (typeaheadEl) { this.attachTypeaheadObservers(typeaheadEl); } this.updateHelpPosition(); - requestAnimationFrame(() => this.updateHelpPosition()); - setTimeout(() => this.updateHelpPosition(), 50); + requestAnimationFrame(() => { + this.updateHelpPosition(); + requestAnimationFrame(() => { + this.updateHelpPosition(); + }); + }); } private stopObservingTypeahead() { @@ -281,30 +328,33 @@ export class QuestionsChooserComponent return OsUtils.modifierKey; } + public focus() { + if (this.inputField && this.inputField.nativeElement) { + this.inputField.nativeElement.focus(); + } + } + @HostListener("document:keydown", ["$event"]) handleGlobalKeyDown(event: KeyboardEvent) { if (this.mode !== "search") return; - if (this.showQuickHelp && event.key === "Escape") { - this.showQuickHelp = false; - this.stopObservingTypeahead(); - this.resetHelpPosition(); - event.preventDefault(); + if (this.isModalOpen()) return; + + if (event.key === "Escape") { + this.handleEscape(event); return; } - if (event.key.toLowerCase() === "f") { + + if ( + event.key.toLowerCase() === "f" && + !event.ctrlKey && + !event.metaKey && + !event.altKey + ) { if (OsUtils.isTypingInInput(event)) { return; } event.preventDefault(); - - this.pressedShortcut = "f"; - if (this.inputField && this.inputField.nativeElement) { - this.inputField.nativeElement.focus(); - } - - setTimeout(() => { - if (this.pressedShortcut === "f") this.pressedShortcut = null; - }, 150); + this.focus(); } } @@ -341,22 +391,28 @@ export class QuestionsChooserComponent } public clearAll() { + this.question = ""; this.items = []; this.questions = []; this.commitMessages = []; this.selectedPillIndex = null; this.editingPillIndex = null; + this.isTypingExclusion = false; this.onChange(this.questions); this.commitMessagesChange.emit(this.commitMessages); this.emitFilterGroups(); } - constructor(private elementRef: ElementRef, private cdr: ChangeDetectorRef) {} + constructor( + private elementRef: ElementRef, + private cdr: ChangeDetectorRef, + @Optional() private customModalService?: CustomModalService + ) {} @HostListener("document:click", ["$event"]) clickout(event) { if (!this.elementRef.nativeElement.contains(event.target)) { - const typeaheadWindow = document.querySelector("ngb-typeahead-window"); + const typeaheadWindow = this.getTypeaheadElement(); if (typeaheadWindow && typeaheadWindow.contains(event.target)) { return; } @@ -384,6 +440,17 @@ export class QuestionsChooserComponent return; // Let the editInput handle it } + if (event.key === "Escape") { + this.handleEscape(event); + return; + } + + if (event.key === "?" && this.mode === "search") { + event.preventDefault(); + this.toggleQuickHelp(); + return; + } + if (event.key === "ArrowLeft" && OsUtils.isModifierPressed(event)) { const currentGroup = this.getGroupRange(this.selectedPillIndex); if (currentGroup.start > 0) { @@ -480,6 +547,21 @@ export class QuestionsChooserComponent return results; } + private getMatchingQuestionSuggestions( + cleanSearch: string, + isEditing = false + ): string[] { + return this.questionSuggestions + .filter( + (question) => + (this.mode === "search" || + !this.questions.includes(question) || + (isEditing && question === this.editingOldValue)) && + question.toLowerCase().indexOf(cleanSearch.toLowerCase()) > -1 + ) + .slice(0, 8); + } + searchQuestions = (text: Observable) => { const clicksWithClosedPopup$ = this.click$.pipe( filter(() => !this.instance.isPopupOpen()) @@ -490,13 +572,7 @@ export class QuestionsChooserComponent const raw = (searchRaw || "").trim(); const isExclusion = this.isTypingExclusion || raw.startsWith("!"); const cleanSearch = raw.startsWith("!") ? raw.substring(1).trim() : raw; - const matchingQuestions = this.questionSuggestions - .filter( - (question) => - (this.mode === "search" || !this.questions.includes(question)) && - question.toLowerCase().indexOf(cleanSearch.toLowerCase()) > -1 - ) - .slice(0, 8); + const matchingQuestions = this.getMatchingQuestionSuggestions(cleanSearch, false); if (this.mode !== "search") { return matchingQuestions; @@ -517,15 +593,11 @@ export class QuestionsChooserComponent const raw = (searchRaw || "").trim(); const isExclusion = this.editingIsExclusion || raw.startsWith("!"); const cleanSearch = raw.startsWith("!") ? raw.substring(1).trim() : raw; - const matchingQuestions = this.questionSuggestions - .filter( - (question) => - (this.mode === "search" || - !this.questions.includes(question) || - question === this.editingOldValue) && - question.toLowerCase().indexOf(cleanSearch.toLowerCase()) > -1 - ) - .slice(0, 8); + const matchingQuestions = this.getMatchingQuestionSuggestions(cleanSearch, true); + + if (this.mode !== "search") { + return matchingQuestions; + } const results = this.buildTypeaheadResults(cleanSearch, matchingQuestions, isExclusion); if (this.showQuickHelp) { @@ -536,6 +608,18 @@ export class QuestionsChooserComponent ); }; + private onInputCaptureKeyDown = (event: KeyboardEvent) => { + if (event.key === "Tab") { + event.preventDefault(); + event.stopPropagation(); // Stop ngbTypeahead from processing the Tab key + if (this.instance.isPopupOpen()) { + this.instance.dismissPopup(); + } else { + this.click$.next(this.question || ""); + } + } + }; + ngOnInit(): void { this.disabled = false; this.questions = [...this.questions]; @@ -543,27 +627,27 @@ export class QuestionsChooserComponent this.commitMessages = [...this.commitMessages]; this.syncItemsFromInputs(); - this.inputField.nativeElement.addEventListener( - "keydown", - (event: KeyboardEvent) => { - if (event.key === "Tab") { - event.preventDefault(); - event.stopPropagation(); // Stop ngbTypeahead from processing the Tab key - if (this.instance.isPopupOpen()) { - this.instance.dismissPopup(); - } else { - this.click$.next(this.question || ""); - } - } - }, - true - ); // Use capture phase + if (this.inputField && this.inputField.nativeElement) { + this.inputField.nativeElement.addEventListener( + "keydown", + this.onInputCaptureKeyDown, + true + ); // Use capture phase + } } ngOnDestroy(): void { this.stopObservingTypeahead(); + if (this.inputField && this.inputField.nativeElement) { + this.inputField.nativeElement.removeEventListener( + "keydown", + this.onInputCaptureKeyDown, + true + ); + } this.focus$.unsubscribe(); this.click$.unsubscribe(); + this.editFocus$.unsubscribe(); } syncItemsFromInputs() { @@ -696,11 +780,8 @@ export class QuestionsChooserComponent } onMainInputKeyDown(event: KeyboardEvent) { - if (this.showQuickHelp && event.key === "Escape") { - this.showQuickHelp = false; - this.stopObservingTypeahead(); - this.resetHelpPosition(); - event.preventDefault(); + if (event.key === "Escape") { + this.handleEscape(event); return; } if ( From 6f8f3afcd7d3e441e42829c7e2141f49037425a2 Mon Sep 17 00:00:00 2001 From: Mika Pons Date: Mon, 7 Sep 2026 11:13:44 +0200 Subject: [PATCH 2/9] =?UTF-8?q?=F0=9F=8E=A8=20Replace=20question=20tag=20i?= =?UTF-8?q?con=20with=20question=20mark=20in=20questions=20chooser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../questions-chooser.component.html | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/app/components/questions-chooser/questions-chooser.component.html b/src/app/components/questions-chooser/questions-chooser.component.html index d437729..42fabb8 100644 --- a/src/app/components/questions-chooser/questions-chooser.component.html +++ b/src/app/components/questions-chooser/questions-chooser.component.html @@ -38,8 +38,9 @@ - - + + + @@ -104,8 +105,9 @@ [class.merged-right]="di < getDraggedItems().length - 1" [class.merged-left]="di > 0"> - - + + + @@ -134,8 +136,9 @@ - - + + + @@ -202,7 +205,7 @@
- + #1
@@ -239,7 +242,7 @@
- + #1
@@ -302,8 +305,9 @@
- - + + + {{ 'QUESTIONS-CHOOSER-QUESTIONS-HEADER' | translate }}
@@ -332,8 +336,9 @@
- - + + + {{ 'QUESTIONS-CHOOSER-EXCLUDE-QUESTION' | translate }} From 3058a5e54c9fc3915e1588fbbbad3005ddd82191 Mon Sep 17 00:00:00 2001 From: Mika Pons Date: Mon, 7 Sep 2026 16:23:38 +0200 Subject: [PATCH 3/9] =?UTF-8?q?=F0=9F=90=9B=20Fix=20popovers=20auto-close?= =?UTF-8?q?=20on=20overview=20graph=20interaction=20and=20keep=20search=20?= =?UTF-8?q?input=20focus=20on=20enter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../graphs/overview/overview.component.html | 2 +- .../graphs/overview/overview.component.ts | 58 ++++++++++++++++--- ...verview-graph-contextual-menu.component.ts | 24 ++++++-- .../questions-chooser.component.ts | 44 +++++++++----- 4 files changed, 99 insertions(+), 29 deletions(-) diff --git a/src/app/components/graphs/overview/overview.component.html b/src/app/components/graphs/overview/overview.component.html index 46cbe71..3169e4f 100644 --- a/src/app/components/graphs/overview/overview.component.html +++ b/src/app/components/graphs/overview/overview.component.html @@ -19,7 +19,7 @@
-