Browse Source

Adds helper for graph search filters

main
Keith Daulton 2 years ago
parent
commit
858f5b89b4
1 changed files with 141 additions and 16 deletions
  1. +141
    -16
      src/webviews/apps/shared/components/search/search-input.ts

+ 141
- 16
src/webviews/apps/shared/components/search/search-input.ts View File

@ -5,8 +5,13 @@ import '../codicon';
// match case is disabled unless regex is true // match case is disabled unless regex is true
const template = html<SearchInput>` const template = html<SearchInput>`
<template role="search"> <template role="search">
<label htmlFor="search">
<label
for="search"
class="${x => (x.showHelp ? 'has-helper' : '')}"
@click="${(x, c) => x.handleShowHelper(c.event)}"
>
<code-icon icon="search" aria-label="${x => x.label}" title="${x => x.label}"></code-icon> <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> </label>
<div class="field"> <div class="field">
<input <input
@ -21,31 +26,36 @@ const template = html`
aria-describedby="${x => (!x.errorMessage ? '' : 'error')}" aria-describedby="${x => (!x.errorMessage ? '' : 'error')}"
@input="${(x, c) => x.handleInput(c.event)}" @input="${(x, c) => x.handleInput(c.event)}"
@keydown="${(x, c) => x.handleShortcutKeys(c.event as KeyboardEvent)}" @keydown="${(x, c) => x.handleShortcutKeys(c.event as KeyboardEvent)}"
@focus="${(x, c) => x.handleFocus(c.event)}"
/> />
<div class="message" id="error" aria-live="polite">${x => x.errorMessage}</div> <div class="message" id="error" aria-live="polite">${x => x.errorMessage}</div>
</div> </div>
<div class="controls"> <div class="controls">
<button <button
class="clear-button${x => (x.value ? '' : ' clear-button__hidden')}"
class="control${x => (x.value ? '' : ' is-hidden')}"
type="button" type="button"
role="button" role="button"
aria-label="Clear" aria-label="Clear"
title="Clear" title="Clear"
@click="${(x, c) => x.handleClear(c.event)}" @click="${(x, c) => x.handleClear(c.event)}"
@focus="${(x, c) => x.handleFocus(c.event)}"
> >
<code-icon icon="close"></code-icon> <code-icon icon="close"></code-icon>
</button> </button>
<button <button
class="control"
type="button" type="button"
role="checkbox" role="checkbox"
aria-label="Match All" aria-label="Match All"
title="Match All" title="Match All"
aria-checked="${x => x.matchAll}" aria-checked="${x => x.matchAll}"
@click="${(x, c) => x.handleMatchAll(c.event)}" @click="${(x, c) => x.handleMatchAll(c.event)}"
@focus="${(x, c) => x.handleFocus(c.event)}"
> >
<code-icon icon="whole-word"></code-icon> <code-icon icon="whole-word"></code-icon>
</button> </button>
<button <button
class="control"
type="button" type="button"
role="checkbox" role="checkbox"
aria-label="Match Case${x => aria-label="Match Case${x =>
@ -55,20 +65,40 @@ const template = html`
?disabled="${x => !x.matchRegex}" ?disabled="${x => !x.matchRegex}"
aria-checked="${x => x.matchCaseOverride}" aria-checked="${x => x.matchCaseOverride}"
@click="${(x, c) => x.handleMatchCase(c.event)}" @click="${(x, c) => x.handleMatchCase(c.event)}"
@focus="${(x, c) => x.handleFocus(c.event)}"
> >
<code-icon icon="case-sensitive"></code-icon> <code-icon icon="case-sensitive"></code-icon>
</button> </button>
<button <button
class="control"
type="button" type="button"
role="checkbox" role="checkbox"
aria-label="Use Regular Expression" aria-label="Use Regular Expression"
title="Use Regular Expression" title="Use Regular Expression"
aria-checked="${x => x.matchRegex}" aria-checked="${x => x.matchRegex}"
@click="${(x, c) => x.handleMatchRegex(c.event)}" @click="${(x, c) => x.handleMatchRegex(c.event)}"
@focus="${(x, c) => x.handleFocus(c.event)}"
> >
<code-icon icon="regex"></code-icon> <code-icon icon="regex"></code-icon>
</button> </button>
</div> </div>
<div class="helper" tabindex="-1" ${ref('helper')}>
<button class="helper-button" type="button" @click="${(x, c) => x.handleInsertToken('message:')}">
Search by Message <small>message: or =:</small>
</button>
<button class="helper-button" type="button" @click="${(x, c) => x.handleInsertToken('author:')}">
Search by Author <small>author: or @:</small>
</button>
<button class="helper-button" type="button" @click="${(x, c) => x.handleInsertToken('sha:')}">
Search by Commit SHA <small>sha: or #:</small>
</button>
<button class="helper-button" type="button" @click="${(x, c) => x.handleInsertToken('file:')}">
Search by File <small>file: or ?:</small>
</button>
<button class="helper-button" type="button" @click="${(x, c) => x.handleInsertToken('change:')}">
Search by Changes <small>change: or ~:</small>
</button>
</div>
</template> </template>
`; `;
@ -81,14 +111,25 @@ const styles = css`
display: inline-flex; display: inline-flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
gap: 0.8rem;
gap: 0.4rem;
position: relative; position: relative;
flex: auto 1 1; flex: auto 1 1;
} }
label { label {
display: flex;
justify-content: center;
align-items: center;
gap: 0.2rem;
width: 3.2rem;
height: 2.4rem;
color: var(--vscode-input-foreground); color: var(--vscode-input-foreground);
cursor: pointer;
}
.icon-small {
font-size: 1rem;
} }
.field { .field {
@ -158,43 +199,86 @@ const styles = css`
} }
button { button {
display: inline-flex;
justify-content: center;
align-items: center;
width: 2rem;
height: 2rem;
padding: 0; padding: 0;
color: var(--vscode-input-foreground); color: var(--vscode-input-foreground);
border: 1px solid transparent; border: 1px solid transparent;
background: none; background: none;
text-align: center;
border-radius: 0.25rem;
} }
button[role='checkbox']:focus:not([disabled]) {
button:focus:not([disabled]) {
outline: 1px solid var(--vscode-focusBorder); outline: 1px solid var(--vscode-focusBorder);
outline-offset: -1px; outline-offset: -1px;
} }
button:not([disabled]) { button:not([disabled]) {
cursor: pointer; cursor: pointer;
} }
button:hover:not([disabled]):not([aria-checked='true']) {
.control {
display: inline-flex;
justify-content: center;
align-items: center;
width: 2rem;
height: 2rem;
text-align: center;
border-radius: 0.25rem;
}
.control:hover:not([disabled]):not([aria-checked='true']) {
background-color: var(--vscode-inputOption-hoverBackground); background-color: var(--vscode-inputOption-hoverBackground);
} }
button[disabled] {
.control[disabled] {
opacity: 0.5; opacity: 0.5;
} }
button[disabled][aria-checked='true'] {
.control[disabled][aria-checked='true'] {
opacity: 0.8; opacity: 0.8;
} }
button[aria-checked='true'] {
.control[aria-checked='true'] {
background-color: var(--vscode-inputOption-activeBackground); background-color: var(--vscode-inputOption-activeBackground);
color: var(--vscode-inputOption-activeForeground); color: var(--vscode-inputOption-activeForeground);
border-color: var(--vscode-inputOption-activeBorder); border-color: var(--vscode-inputOption-activeBorder);
} }
.clear-button__hidden {
.control.is-hidden {
display: none; display: none;
} }
.has-helper {
background-color: var(--vscode-input-background);
border-radius: 0.3rem 0.3rem 0 0;
}
.helper {
display: none;
position: absolute;
top: 100%;
left: 0;
z-index: 5000;
width: fit-content;
background-color: var(--vscode-input-background);
border-radius: 0 0.3rem 0.3rem 0.3rem;
outline: none;
}
.has-helper ~ .helper {
display: block;
}
.helper-button {
display: block;
width: 100%;
padding: 0.3rem 0.6rem;
text-align: left;
}
.helper-button:hover {
background-color: var(--vscode-inputOption-hoverBackground);
}
.helper-button:first-child {
border-top-right-radius: 0.3rem;
}
.helper-button:last-child {
border-bottom-left-radius: 0.3rem;
border-bottom-right-radius: 0.3rem;
}
.helper-button small {
opacity: 0.5;
}
`; `;
@customElement({ @customElement({
@ -204,6 +288,9 @@ const styles = css`
}) })
export class SearchInput extends FASTElement { export class SearchInput extends FASTElement {
@observable @observable
showHelp = false;
@observable
errorMessage = ''; errorMessage = '';
@attr @attr
@ -230,11 +317,35 @@ export class SearchInput extends FASTElement {
} }
input!: HTMLInputElement; 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 { override focus(options?: FocusOptions): void {
this.input.focus(options); this.input.focus(options);
} }
handleDocumentClick(e: MouseEvent) {
if (this.showHelp === false) return;
const composedPath = e.composedPath();
if (!composedPath.includes(this)) {
this.showHelp = false;
}
}
handleFocus(_e: Event) {
this.showHelp = false;
}
handleClear(_e: Event) { handleClear(_e: Event) {
this.value = ''; this.value = '';
this.emitSearch(); this.emitSearch();
@ -273,6 +384,20 @@ export class SearchInput extends FASTElement {
return false; 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}`;
this.input.focus();
}
private emitSearch() { private emitSearch() {
const search: SearchQuery = { const search: SearchQuery = {
query: this.value, query: this.value,

Loading…
Cancel
Save