diff --git a/src/webviews/apps/plus/graph/GraphWrapper.tsx b/src/webviews/apps/plus/graph/GraphWrapper.tsx index 58b66dc..ef4150a 100644 --- a/src/webviews/apps/plus/graph/GraphWrapper.tsx +++ b/src/webviews/apps/plus/graph/GraphWrapper.tsx @@ -28,7 +28,7 @@ import type { Subscription } from '../../../../subscription'; import { getSubscriptionTimeRemaining, SubscriptionState } from '../../../../subscription'; import { debounce } from '../../../../system/function'; import { pluralize } from '../../../../system/string'; -import { SearchField, SearchNav } from '../../shared/components/search/react'; +import { SearchBox } from '../../shared/components/search/react'; import type { DateTimeFormat } from '../../shared/date'; import { formatDate, fromNow } from '../../shared/date'; @@ -646,18 +646,13 @@ export function GraphWrapper({ {isAllowed && (
- handleSearchInput(e as CustomEvent)} - onPrevious={() => handleSearchNavigation(false)} - onNext={() => handleSearchNavigation(true)} - /> - 2)} more={hasMoreSearchResults} + value={searchQuery?.query ?? ''} + onChange={e => handleSearchInput(e as CustomEvent)} onPrevious={() => handleSearchNavigation(false)} onNext={() => handleSearchNavigation(true)} /> diff --git a/src/webviews/apps/plus/graph/graph.scss b/src/webviews/apps/plus/graph/graph.scss index a4c5097..80378fc 100644 --- a/src/webviews/apps/plus/graph/graph.scss +++ b/src/webviews/apps/plus/graph/graph.scss @@ -435,6 +435,10 @@ a { margin: 0; } } + + &__group { + flex: auto 1 1; + } } .graph-app { diff --git a/src/webviews/apps/shared/components/search/react.tsx b/src/webviews/apps/shared/components/search/react.tsx index 63606b4..405aafa 100644 --- a/src/webviews/apps/shared/components/search/react.tsx +++ b/src/webviews/apps/shared/components/search/react.tsx @@ -1,21 +1,13 @@ import { provideReactWrapper } from '@microsoft/fast-react-wrapper'; import React from 'react'; -import { SearchField as fieldComponent } from './search-field'; -import { SearchNav as navComponent } from './search-nav'; +import { SearchBox as searchBoxComponent } from './search-box'; const { wrap } = provideReactWrapper(React); -export const SearchField = wrap(fieldComponent, { +export const SearchBox = wrap(searchBoxComponent, { events: { onChange: 'change', onPrevious: 'previous', onNext: 'next', }, }); - -export const SearchNav = wrap(navComponent, { - events: { - onPrevious: 'previous', - onNext: 'next', - }, -}); diff --git a/src/webviews/apps/shared/components/search/search-box.ts b/src/webviews/apps/shared/components/search/search-box.ts new file mode 100644 index 0000000..8e3379b --- /dev/null +++ b/src/webviews/apps/shared/components/search/search-box.ts @@ -0,0 +1,217 @@ +import { attr, css, customElement, FASTElement, html, observable, volatile, when } from '@microsoft/fast-element'; +import { isMac } from '@env/platform'; +import { pluralize } from '../../../../../system/string'; +import type { Disposable } from '../../dom'; +import { DOM } from '../../dom'; +import { numberConverter } from '../converters/number-converter'; +import '../codicon'; +import './search-input'; + +const template = html``; + +const styles = css` + :host { + display: inline-flex; + flex-direction: row; + align-items: center; + gap: 0.8rem; + color: var(--vscode-titleBar-inactiveForeground); + flex: auto 1 1; + } + :host(:focus) { + outline: 0; + } + + .search-navigation { + display: inline-flex; + flex-direction: row; + align-items: center; + gap: 0.8rem; + color: var(--vscode-titleBar-inactiveForeground); + } + .search-navigation:focus { + outline: 0; + } + + .count { + flex: none; + margin-right: 0.4rem; + font-size: 1.2rem; + min-width: 10ch; + } + + .count.error { + color: var(--vscode-errorForeground); + } + + .button { + width: 2.4rem; + height: 2.4rem; + padding: 0; + color: inherit; + border: none; + background: none; + text-align: center; + } + .button:focus { + outline: 1px solid var(--vscode-focusBorder); + outline-offset: -1px; + } + .button:not([disabled]) { + cursor: pointer; + } + .button:hover:not([disabled]) { + background-color: var(--vscode-titleBar-activeBackground); + } + .button > code-icon[icon='arrow-up'] { + transform: translateX(-0.1rem); + } + + .sr-only { + clip: rect(0 0 0 0); + clip-path: inset(50%); + height: 1px; + overflow: hidden; + position: absolute; + white-space: nowrap; + width: 1px; + } +`; + +@customElement({ name: 'search-box', template: template, styles: styles }) +export class SearchBox extends FASTElement { + @observable + errorMessage = ''; + + @attr + label = 'Search'; + + @attr + placeholder = 'Search commits, e.g. "Updates dependencies" author:eamodio'; + + @attr + value = ''; + + @attr({ mode: 'boolean' }) + matchAll = false; + + @attr({ mode: 'boolean' }) + matchCase = false; + + @attr({ mode: 'boolean' }) + matchRegex = true; + + @attr({ converter: numberConverter }) + total = 0; + + @attr({ converter: numberConverter }) + step = 0; + + @attr({ mode: 'boolean' }) + more = false; + + @attr({ mode: 'boolean' }) + valid = false; + + @attr + resultsLabel = 'result'; + + @volatile + get formattedLabel() { + return pluralize(this.resultsLabel, this.total, { zero: 'No' }); + } + + @volatile + get hasPrevious() { + return this.total !== 0; + } + + @volatile + get hasNext() { + return this.total !== 0; + } + + private _disposable: Disposable | undefined; + override connectedCallback(): void { + super.connectedCallback(); + + this._disposable = DOM.on(window, 'keyup', e => this.handleShortcutKeys(e)); + } + + override disconnectedCallback(): void { + super.disconnectedCallback(); + + this._disposable?.dispose(); + } + + next() { + this.$emit('next'); + } + + previous() { + this.$emit('previous'); + } + + handleShortcutKeys(e: KeyboardEvent) { + if ( + (e.key !== 'F3' && e.key !== 'g') || + (e.key !== 'g' && (e.ctrlKey || e.metaKey || e.altKey)) || + (e.key === 'g' && (!e.metaKey || !isMac)) + ) { + return; + } + + if (e.shiftKey) { + this.previous(); + } else { + this.next(); + } + } + + handlePrevious(e: Event) { + e.stopImmediatePropagation(); + this.previous(); + } + + handleNext(e: Event) { + e.stopImmediatePropagation(); + this.next(); + } +} diff --git a/src/webviews/apps/shared/components/search/search-field.ts b/src/webviews/apps/shared/components/search/search-input.ts similarity index 70% rename from src/webviews/apps/shared/components/search/search-field.ts rename to src/webviews/apps/shared/components/search/search-input.ts index e95c57b..37a158b 100644 --- a/src/webviews/apps/shared/components/search/search-field.ts +++ b/src/webviews/apps/shared/components/search/search-input.ts @@ -1,9 +1,9 @@ -import { attr, css, customElement, FASTElement, html, observable } from '@microsoft/fast-element'; +import { attr, css, customElement, FASTElement, html, observable, volatile } from '@microsoft/fast-element'; import type { SearchQuery } from '../../../../../git/search'; import '../codicon'; // match case is disabled unless regex is true -const template = html` +const template = html`