Browse Source

Adds webview focus/blur events

Converts search-input to use pop-menu
Closes pop-menu's on webview blur
main
Eric Amodio 1 year ago
parent
commit
d18f05b64b
7 changed files with 197 additions and 140 deletions
  1. +12
    -0
      src/plus/webviews/graph/graphWebview.ts
  2. +5
    -0
      src/plus/webviews/graph/protocol.ts
  3. +1
    -1
      src/webviews/apps/plus/graph/GraphWrapper.tsx
  4. +15
    -11
      src/webviews/apps/plus/graph/graph.scss
  5. +6
    -0
      src/webviews/apps/plus/graph/graph.tsx
  6. +45
    -10
      src/webviews/apps/shared/components/overlays/pop-menu/pop-menu.ts
  7. +113
    -118
      src/webviews/apps/shared/components/search/search-input.ts

+ 12
- 0
src/plus/webviews/graph/graphWebview.ts View File

@ -142,6 +142,7 @@ import {
ChooseRepositoryCommandType,
DidChangeAvatarsNotificationType,
DidChangeColumnsNotificationType,
DidChangeFocusNotificationType,
DidChangeGraphConfigurationNotificationType,
DidChangeNotificationType,
DidChangeRefsMetadataNotificationType,
@ -419,6 +420,8 @@ export class GraphWebviewProvider implements WebviewProvider {
}
onFocusChanged(focused: boolean): void {
void this.notifyDidChangeFocus(focused);
if (!focused || this.activeSelection == null || !this.container.commitDetailsView.visible) {
this._showActiveSelectionDetailsDebounced?.cancel();
return;
@ -1174,6 +1177,15 @@ export class GraphWebviewProvider implements WebviewProvider {
}
@debug()
private async notifyDidChangeFocus(focused: boolean): Promise<boolean> {
if (!this.host.ready || !this.host.visible) return false;
return this.host.notify(DidChangeFocusNotificationType, {
focused: focused,
});
}
@debug()
private async notifyDidChangeWindowFocus(): Promise<boolean> {
if (!this.host.ready || !this.host.visible) {
this.host.addPendingIpcNotification(DidChangeWindowFocusNotificationType, this._ipcNotificationMap, this);

+ 5
- 0
src/plus/webviews/graph/protocol.ts View File

@ -356,6 +356,11 @@ export const DidChangeColumnsNotificationType = new IpcNotificationType
'graph/columns/didChange',
);
export interface DidChangeFocusParams {
focused: boolean;
}
export const DidChangeFocusNotificationType = new IpcNotificationType<DidChangeFocusParams>('graph/focus/didChange');
export interface DidChangeWindowFocusParams {
focused: boolean;
}

+ 1
- 1
src/webviews/apps/plus/graph/GraphWrapper.tsx View File

@ -1422,7 +1422,7 @@ export function GraphWrapper({
>
<span className="codicon codicon-graph-line action-button__icon"></span>
</button>
<PopMenu position="right" className="split-button-dropdown">
<PopMenu position="right">
<button
type="button"
className="action-button"

+ 15
- 11
src/webviews/apps/plus/graph/graph.scss View File

@ -227,6 +227,16 @@ button:not([disabled]),
text-decoration: none;
}
&[aria-checked] {
border: 1px solid transparent;
}
&[aria-checked='true'] {
background-color: var(--vscode-inputOption-activeBackground);
color: var(--vscode-inputOption-activeForeground);
border-color: var(--vscode-inputOption-activeBorder);
}
.codicon[class*='codicon-'],
.glicon[class*='glicon-'] {
line-height: 2.2rem;
@ -252,7 +262,11 @@ button:not([disabled]),
&__more,
&__more.codicon[class*='codicon-'] {
font-size: 1rem;
margin-right: -0.35rem;
margin-right: -0.25rem;
}
&__more.codicon[class*='codicon-']::before {
margin-left: -0.25rem;
}
&__indicator {
@ -320,16 +334,6 @@ button:not([disabled]),
}
}
.action-button[aria-checked] {
border: 1px solid transparent;
}
.action-button[aria-checked='true'] {
background-color: var(--vscode-inputOption-activeBackground);
color: var(--vscode-inputOption-activeForeground);
border-color: var(--vscode-inputOption-activeBorder);
}
.button-group {
display: flex;
flex-direction: row;

+ 6
- 0
src/webviews/apps/plus/graph/graph.tsx View File

@ -21,6 +21,7 @@ import {
ChooseRepositoryCommandType,
DidChangeAvatarsNotificationType,
DidChangeColumnsNotificationType,
DidChangeFocusNotificationType,
DidChangeGraphConfigurationNotificationType,
DidChangeNotificationType,
DidChangeRefsMetadataNotificationType,
@ -164,6 +165,11 @@ export class GraphApp extends App {
this.setState(this.state, type);
});
break;
case DidChangeFocusNotificationType.method:
onIpc(DidChangeFocusNotificationType, msg, params => {
window.dispatchEvent(new CustomEvent(params.focused ? 'webview-focus' : 'webview-blur'));
});
break;
case DidChangeWindowFocusNotificationType.method:
onIpc(DidChangeWindowFocusNotificationType, msg, (params, type) => {

+ 45
- 10
src/webviews/apps/shared/components/overlays/pop-menu/pop-menu.ts View File

@ -92,16 +92,30 @@ export class PopMenu extends FASTElement {
}
private isTrackingOutside = false;
private _toggleHandler: ((e: MouseEvent) => void) | undefined;
override connectedCallback() {
super.connectedCallback();
this.updateToggle();
this.addEventListener('click', this.handleToggle.bind(this), false);
if (this._toggleHandler == null) {
this._toggleHandler = this.handleToggle.bind(this);
}
this.addEventListener('click', this._toggleHandler, false);
}
override disconnectedCallback() {
super.disconnectedCallback();
this.removeEventListener('click', this.handleToggle.bind(this), false);
if (this._toggleHandler != null) {
this.removeEventListener('click', this._toggleHandler, false);
this._toggleHandler = undefined;
}
this.disposeTrackOutside();
}
close() {
this.open = false;
this.disposeTrackOutside();
}
@ -136,26 +150,47 @@ export class PopMenu extends FASTElement {
(e.type === 'click' &&
window.getComputedStyle(composedPath[0] as Element, '::before').content === '"\uEA76"')
) {
this.open = false;
this.disposeTrackOutside();
this.close();
}
}
private _documentEventHandler: ((e: MouseEvent | FocusEvent) => void) | undefined;
private _webviewBlurEventHandler: ((e: Event) => void) | undefined;
trackOutside() {
if (this.isTrackingOutside || !this.open) return;
this.isTrackingOutside = true;
const boundHandleDocumentEvent = this.handleDocumentEvent.bind(this);
document.addEventListener('click', boundHandleDocumentEvent, false);
document.addEventListener('focusin', boundHandleDocumentEvent, false);
if (this._documentEventHandler == null) {
this._documentEventHandler = this.handleDocumentEvent.bind(this);
}
document.addEventListener('click', this._documentEventHandler, false);
document.addEventListener('focusin', this._documentEventHandler, false);
if (this._webviewBlurEventHandler == null) {
this._webviewBlurEventHandler = () => this.close();
}
window.addEventListener('webview-blur', this._webviewBlurEventHandler, false);
}
disposeTrackOutside() {
if (!this.isTrackingOutside) return;
this.isTrackingOutside = false;
const boundHandleDocumentEvent = this.handleDocumentEvent.bind(this);
document.removeEventListener('click', boundHandleDocumentEvent, false);
window.removeEventListener('focusin', boundHandleDocumentEvent, false);
if (this._documentEventHandler != null) {
document.removeEventListener('click', this._documentEventHandler, false);
window.removeEventListener('focusin', this._documentEventHandler, false);
this._documentEventHandler = undefined;
}
if (this._webviewBlurEventHandler != null) {
window.removeEventListener('webview-blur', this._webviewBlurEventHandler, false);
this._webviewBlurEventHandler = undefined;
}
}
}

+ 113
- 118
src/webviews/apps/shared/components/search/search-input.ts View File

@ -2,6 +2,7 @@ import { attr, css, customElement, FASTElement, html, observable, ref, volatile,
import type { SearchQuery } from '../../../../../git/search';
import { debounce } from '../../../../../system/function';
import '../code-icon';
import type { PopMenu } from '../overlays/pop-menu';
export type SearchOperators =
| '=:'
@ -34,21 +35,56 @@ const operatorsHelpMap = new Map([
// match case is disabled unless regex is true
const template = html<SearchInput>`
<template role="search">
<label
for="search"
aria-controls="helper"
aria-expanded="${x => x.showHelp}"
@click="${(x, c) => x.handleShowHelper(c.event)}"
>
<code-icon icon="search" aria-label="${x => x.label}" title="${x => x.label}"></code-icon>
<code-icon class="icon-small" icon="chevron-down" aria-hidden="true"></code-icon>
</label>
<pop-menu ${ref('popmenu')} style="margin-left: -0.25rem;">
<button
type="button"
class="action-button"
slot="trigger"
aria-label="${x => x.label}"
title="${x => x.label}"
>
<code-icon icon="search" aria-hidden="true"></code-icon>
<code-icon class="action-button__more" icon="chevron-down" aria-hidden="true"></code-icon>
</button>
<menu-list slot="content">
<menu-label>Search by</menu-label>
<menu-item role="none">
<button class="menu-button" type="button" @click="${(x, _c) => x.handleInsertToken('@me')}">
My changes <small>@me</small>
</button>
</menu-item>
<menu-item role="none">
<button class="menu-button" type="button" @click="${(x, _c) => x.handleInsertToken('message:')}">
Message <small>message: or =:</small>
</button>
</menu-item>
<menu-item role="none">
<button class="menu-button" type="button" @click="${(x, _c) => x.handleInsertToken('author:')}">
Author <small>author: or @:</small>
</button>
</menu-item>
<menu-item role="none">
<button class="menu-button" type="button" @click="${(x, _c) => x.handleInsertToken('commit:')}">
Commit SHA <small>commit: or #:</small>
</button>
</menu-item>
<menu-item role="none">
<button class="menu-button" type="button" @click="${(x, _c) => x.handleInsertToken('file:')}">
File <small>file: or ?:</small>
</button>
</menu-item>
<menu-item role="none">
<button class="menu-button" type="button" @click="${(x, _c) => x.handleInsertToken('change:')}">
Change <small>change: or ~:</small>
</button>
</menu-item>
</menu-list>
</pop-menu>
<div class="field">
<input
${ref('input')}
id="search"
part="search"
class="${x => (x.showHelp ? 'has-helper' : '')}"
type="text"
spellcheck="false"
placeholder="${x => x.placeholder}"
@ -145,27 +181,6 @@ const template = html`
<code-icon icon="regex"></code-icon>
</button>
</div>
<div class="helper" id="helper" tabindex="-1" ${ref('helper')}>
<p class="helper-label">Search by</p>
<button class="helper-button" type="button" @click="${(x, _c) => x.handleInsertToken('@me')}">
My changes <small>@me</small>
</button>
<button class="helper-button" type="button" @click="${(x, _c) => x.handleInsertToken('message:')}">
Message <small>message: or =:</small>
</button>
<button class="helper-button" type="button" @click="${(x, _c) => x.handleInsertToken('author:')}">
Author <small>author: or @:</small>
</button>
<button class="helper-button" type="button" @click="${(x, _c) => x.handleInsertToken('commit:')}">
Commit SHA <small>commit: or #:</small>
</button>
<button class="helper-button" type="button" @click="${(x, _c) => x.handleInsertToken('file:')}">
File <small>file: or ?:</small>
</button>
<button class="helper-button" type="button" @click="${(x, _c) => x.handleInsertToken('change:')}">
Change <small>change: or ~:</small>
</button>
</div>
</template>
`;
@ -327,74 +342,90 @@ const styles = css`
display: none;
}
.helper {
display: none;
position: absolute;
top: 100%;
left: 0;
z-index: 5000;
width: fit-content;
background-color: var(--vscode-menu-background);
border: 1px solid var(--vscode-menu-border);
outline: none;
.action-button {
position: relative;
appearance: none;
font-family: inherit;
font-size: 1.2rem;
line-height: 2.2rem;
// background-color: var(--color-graph-actionbar-background);
background-color: transparent;
border: none;
color: inherit;
color: var(--color-foreground);
padding: 0 0.75rem;
cursor: pointer;
border-radius: 3px;
height: auto;
display: grid;
grid-auto-flow: column;
grid-gap: 0.5rem;
gap: 0.5rem;
max-width: fit-content;
}
label[aria-expanded='true'] ~ .helper {
display: block;
.action-button[disabled] {
pointer-events: none;
cursor: default;
opacity: 1;
}
.helper::before {
font: normal normal normal 14px/1 codicon;
display: inline-block;
.action-button:hover {
background-color: var(--color-graph-actionbar-selectedBackground);
color: var(--color-foreground);
text-decoration: none;
text-rendering: auto;
text-align: center;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
vertical-align: middle;
line-height: 2rem;
letter-spacing: normal;
.action-button[aria-checked] {
border: 1px solid transparent;
}
content: '\\ea76';
position: absolute;
top: 2px;
right: 5px;
cursor: pointer;
pointer-events: all;
z-index: 10001;
opacity: 0.6;
.action-button[aria-checked='true'] {
background-color: var(--vscode-inputOption-activeBackground);
color: var(--vscode-inputOption-activeForeground);
border-color: var(--vscode-inputOption-activeBorder);
}
.helper-label {
text-transform: uppercase;
font-size: 0.84em;
.action-button code-icon,
.action-button .codicon[class*='codicon-'],
.action-button .glicon[class*='glicon-'] {
line-height: 2.2rem;
padding-left: 0.6rem;
padding-right: 0.6rem;
margin: 0;
opacity: 0.6;
user-select: none;
vertical-align: bottom;
}
.action-button__more,
.action-button__more.codicon[class*='codicon-'] {
font-size: 1rem;
margin-right: -0.25rem;
}
.action-button__more::before {
margin-left: -0.25rem;
}
.helper-button {
menu-item {
padding: 0 0.5rem;
}
menu-list {
padding-bottom: 0.5rem;
}
.menu-button {
display: block;
width: 100%;
padding-left: 0.6rem;
padding-right: 0.6rem;
padding: 0.1rem 0.6rem 0 0.6rem;
line-height: 2.2rem;
text-align: left;
color: var(--vscode-menu-foreground);
border-radius: 3px;
}
.helper-button:hover {
.menu-button:hover {
color: var(--vscode-menu-selectionForeground);
background-color: var(--vscode-menu-selectionBackground);
}
.helper-button small {
opacity: 0.5;
}
`;
@customElement({
@ -403,8 +434,8 @@ const styles = css`
styles: styles,
})
export class SearchInput extends FASTElement {
@observable
showHelp = false;
input!: HTMLInputElement;
popmenu!: PopMenu;
@observable
errorMessage = '';
@ -435,39 +466,12 @@ export class SearchInput extends FASTElement {
return this.matchRegex ? this.matchCase : true;
}
input!: HTMLInputElement;
helper!: HTMLElement;
override connectedCallback() {
super.connectedCallback();
document.addEventListener('click', this.handleDocumentClick.bind(this));
}
override disconnectedCallback() {
super.disconnectedCallback();
document.removeEventListener('click', this.handleDocumentClick.bind(this));
}
override focus(options?: FocusOptions): void {
this.input.focus(options);
}
handleDocumentClick(e: MouseEvent) {
if (this.showHelp === false) return;
const composedPath = e.composedPath();
if (
!composedPath.includes(this) ||
// If the ::before element is clicked and is the close icon, close the menu
(e.type === 'click' &&
window.getComputedStyle(composedPath[0] as Element, '::before').content === '"\uEA76"')
) {
this.showHelp = false;
}
}
handleFocus(_e: Event) {
this.showHelp = false;
this.popmenu.close();
}
handleClear(_e: Event) {
@ -547,15 +551,6 @@ export class SearchInput extends FASTElement {
return false;
}
handleShowHelper(_e: Event) {
this.showHelp = !this.showHelp;
if (this.showHelp) {
window.requestAnimationFrame(() => {
this.helper.focus();
});
}
}
handleInsertToken(token: string) {
this.value += `${this.value.length > 0 ? ' ' : ''}${token}`;
window.requestAnimationFrame(() => {

Loading…
Cancel
Save