add LazyQueryInputFormControl, remove duplicated common pattern (#63537)

Many places used a LazyQueryInput with a focus ring and implemented it
incorrectly:

- The text would overflow
- The tooltips in the query input would be chopped off

This cleans up duplicate code and fixes those bugs.

No visual changes.


## Test plan

View the query input and try overflowing it on:

- search context query input
- saved search query input
- code monitor query input
This commit is contained in:
Quinn Slack 2024-07-01 17:34:51 -07:00 committed by GitHub
parent aa87316d68
commit 1b9459cab7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 57 additions and 137 deletions

View File

@ -3,18 +3,20 @@
// Match query input styling for the intermediate input displayed while
// lazy-loading the query input component, to avoid unstable UI.
.intermediate-input {
height: 100%;
font-size: var(--code-font-size);
border: none;
padding-left: 0;
padding-right: 0;
padding: 0;
height: unset;
&:focus {
box-shadow: none;
}
}
@media (--xs-breakpoint-down) {
height: auto;
padding: 0;
.form-control {
cursor: text;
height: unset;
&:focus-within {
border: 1px solid var(--input-focus-border-color);
box-shadow: var(--search-box-focus-box-shadow);
}
}

View File

@ -57,3 +57,12 @@ export const LazyQueryInput: React.FunctionComponent<LazyQueryInputProps> = ({ .
<CodeMirrorQueryInput {...props} />
</Suspense>
)
/**
* A {@link LazyQueryInput} with a focus ring and other styles from the `.form-control` CSS class.
*/
export const LazyQueryInputFormControl: React.FunctionComponent<LazyQueryInputProps> = props => (
<div className={classNames('form-control', styles.formControl)}>
<LazyQueryInput {...props} />
</div>
)

View File

@ -1,64 +1,5 @@
@import 'wildcard/src/global-styles/breakpoints';
.query-input {
display: flex;
align-items: stretch;
@media (--xs-breakpoint-down) {
flex-direction: column;
align-items: flex-start;
}
&-preview-link {
white-space: nowrap;
font-size: 0.75rem;
background-color: var(--color-bg-2);
border: 1px solid var(--input-border-color);
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
display: flex;
align-items: center;
padding: 0 0.5rem;
&-icon {
max-height: 1rem;
max-width: 1rem;
}
@media (--xs-breakpoint-down) {
height: 2rem;
width: 100%;
border-top: none;
border-top-right-radius: 0;
border-bottom-left-radius: 2px;
}
}
&-field {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-right: none;
background-color: var(--color-bg-1);
/* stylelint-disable-next-line declaration-property-unit-allowed-list */
height: calc(1rem + 0.5rem * 2 + 1px * 2); /* 1rem for text, 0.5rem vertical padding, 1px border */
display: flex;
align-items: center;
flex-grow: 0;
min-width: 0;
&:focus-within {
border: 1px solid var(--input-focus-border-color);
box-shadow: var(--search-box-focus-box-shadow);
}
@media (--xs-breakpoint-down) {
border-right: 1px solid var(--input-border-color);
border-top-right-radius: 2px;
border-bottom-left-radius: 0;
}
}
}
.trigger-label {
color: var(--link-color);
}

View File

@ -4,7 +4,7 @@ import { mdiCheck, mdiHelpCircle, mdiOpenInNew, mdiRadioboxBlank } from '@mdi/js
import { VisuallyHidden } from '@reach/visually-hidden'
import classNames from 'classnames'
import { LazyQueryInput } from '@sourcegraph/branded'
import { LazyQueryInputFormControl } from '@sourcegraph/branded'
import type { QueryState } from '@sourcegraph/shared/src/search'
import { FilterType, resolveFilter, validateFilter } from '@sourcegraph/shared/src/search/query/filters'
import { scanSearchQuery } from '@sourcegraph/shared/src/search/query/scanner'
@ -12,7 +12,7 @@ import { useSettingsCascade } from '@sourcegraph/shared/src/settings/settings'
import { buildSearchURLQuery } from '@sourcegraph/shared/src/util/url'
import { Button, Card, Checkbox, Code, H3, Icon, Link, Tooltip } from '@sourcegraph/wildcard'
import { SearchPatternType } from '../../../graphql-operations'
import type { SearchPatternType } from '../../../graphql-operations'
import { defaultPatternTypeFromSettings } from '../../../util/settings'
import styles from './FormTriggerArea.module.scss'
@ -226,16 +226,12 @@ export const FormTriggerArea: React.FunctionComponent<React.PropsWithChildren<Tr
</span>
<span className="mt-4">Search query</span>
<div>
<div className={classNames(styles.queryInput, 'my-2')}>
<div className="my-2">
<div
className={classNames(
'form-control',
styles.queryInputField,
`test-${derivedInputClassName}`
)}
className={classNames(`test-${derivedInputClassName}`)}
data-testid="trigger-query-edit"
>
<LazyQueryInput
<LazyQueryInputFormControl
className="test-trigger-input"
patternType={defaultPatternType}
isSourcegraphDotCom={isSourcegraphDotCom}
@ -246,21 +242,15 @@ export const FormTriggerArea: React.FunctionComponent<React.PropsWithChildren<Tr
autoFocus={true}
/>
</div>
<div className={styles.queryInputPreviewLink}>
<Link
to={`/search?${buildSearchURLQuery(queryState.query, defaultPatternType, false)}`}
target="_blank"
rel="noopener noreferrer"
className="test-preview-link"
>
Preview results{' '}
<Icon
aria-label="Open in new window"
className={classNames('ml-1', styles.queryInputPreviewLinkIcon)}
svgPath={mdiOpenInNew}
/>
</Link>
</div>
<Link
to={`/search?${buildSearchURLQuery(queryState.query, defaultPatternType, false)}`}
target="_blank"
rel="noopener noreferrer"
className="test-preview-link d-flex align-items-center flex-gap-1 my-1"
>
Preview results{' '}
<Icon aria-label="Open in new window" className="ml-1" svgPath={mdiOpenInNew} />
</Link>
</div>
<ul className={classNames(styles.checklist, 'mb-4')}>

View File

@ -43,10 +43,6 @@
&__query {
margin-top: 0.75rem;
margin-left: 1.25rem;
border-radius: 0.25rem;
padding: 0.5rem;
background-color: var(--color-bg-1);
border: 1px solid var(--border-color-2);
}
&__query-label {

View File

@ -2,33 +2,33 @@ import React, { useCallback, useMemo, useState } from 'react'
import classNames from 'classnames'
import { useNavigate } from 'react-router-dom'
import { type Observable, of, throwError, from } from 'rxjs'
import { from, of, throwError, type Observable } from 'rxjs'
import { catchError, map, startWith, switchMap, tap } from 'rxjs/operators'
import { SyntaxHighlightedSearchQuery, LazyQueryInput } from '@sourcegraph/branded'
import { LazyQueryInputFormControl, SyntaxHighlightedSearchQuery } from '@sourcegraph/branded'
import { asError, createAggregateError, isErrorLike } from '@sourcegraph/common'
import {
SearchPatternType,
type Scalars,
type SearchContextFields,
type SearchContextInput,
type SearchContextRepositoryRevisionsInput,
SearchPatternType,
type SearchContextFields,
} from '@sourcegraph/shared/src/graphql-operations'
import type { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
import type { QueryState, SearchContextProps } from '@sourcegraph/shared/src/search'
import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { useIsLightTheme } from '@sourcegraph/shared/src/theme'
import {
Container,
Alert,
Button,
Code,
Container,
Form,
Input,
Link,
RadioButton,
TextArea,
useEventObservable,
Alert,
Link,
Code,
Input,
Form,
} from '@sourcegraph/wildcard'
import type { AuthenticatedUser } from '../../auth'
@ -431,7 +431,7 @@ export const SearchContextForm: React.FunctionComponent<React.PropsWithChildren<
label={<>Search query</>}
/>
<div className={styles.searchContextFormQuery} data-testid="search-context-dynamic-query">
<LazyQueryInput
<LazyQueryInputFormControl
patternType={SearchPatternType.regexp}
isSourcegraphDotCom={isSourcegraphDotCom}
caseSensitive={true}

View File

@ -7,21 +7,6 @@
font-weight: bold;
}
.query-input {
display: flex;
align-items: center;
cursor: text;
:global(.cm-editor) {
flex: 1;
}
&:focus-within {
border: 1px solid var(--input-focus-border-color);
box-shadow: var(--search-box-focus-box-shadow);
}
}
.submit-button {
margin-bottom: 1rem;
}

View File

@ -1,6 +1,6 @@
import { describe, expect, test, vi } from 'vitest'
import { LazyQueryInput } from '@sourcegraph/branded'
import { LazyQueryInputFormControl } from '@sourcegraph/branded'
import { renderWithBrandedContext } from '@sourcegraph/wildcard/src/testing'
import { SearchPatternType } from '../graphql-operations'
@ -10,9 +10,9 @@ import { SavedSearchForm } from './SavedSearchForm'
const DEFAULT_PATTERN_TYPE = SearchPatternType.regexp
describe('SavedSearchForm', () => {
test('renders LazyQueryInput with the default patternType', () => {
test('renders LazyQueryInputFormControl with the default patternType', () => {
vi.mock('@sourcegraph/branded', () => ({
LazyQueryInput: vi.fn(() => null),
LazyQueryInputFormControl: vi.fn(() => null),
}))
vi.mock('../util/settings', () => ({
defaultPatternTypeFromSettings: () => DEFAULT_PATTERN_TYPE,
@ -36,7 +36,7 @@ describe('SavedSearchForm', () => {
/>
)
expect(LazyQueryInput).toHaveBeenCalledWith(
expect(LazyQueryInputFormControl).toHaveBeenCalledWith(
expect.objectContaining({
patternType: DEFAULT_PATTERN_TYPE,
}),

View File

@ -3,27 +3,27 @@ import React, { useEffect, useMemo, useState } from 'react'
import classNames from 'classnames'
import type { Omit } from 'utility-types'
import { LazyQueryInput } from '@sourcegraph/branded'
import { LazyQueryInputFormControl } from '@sourcegraph/branded'
import type { QueryState } from '@sourcegraph/shared/src/search'
import { useSettingsCascade } from '@sourcegraph/shared/src/settings/settings'
import {
Container,
PageHeader,
ProductStatusBadge,
Button,
Link,
Alert,
Button,
Checkbox,
Input,
Code,
Label,
Container,
ErrorAlert,
Form,
Input,
Label,
Link,
PageHeader,
ProductStatusBadge,
} from '@sourcegraph/wildcard'
import type { AuthenticatedUser } from '../auth'
import { PageTitle } from '../components/PageTitle'
import { type Scalars, SearchPatternType } from '../graphql-operations'
import { type Scalars, type SearchPatternType } from '../graphql-operations'
import type { NamespaceProps } from '../namespaces'
import { defaultPatternTypeFromSettings } from '../util/settings'
@ -60,7 +60,6 @@ export const SavedSearchForm: React.FunctionComponent<React.PropsWithChildren<Sa
/**
* Returns an input change handler that updates the SavedQueryFields in the component's state
*
* @param key The key of saved query fields that a change of this input should update
*/
const createInputChangeHandler =
@ -124,9 +123,7 @@ export const SavedSearchForm: React.FunctionComponent<React.PropsWithChildren<Sa
/>
<Label className={classNames('w-100 form-group', styles.label)}>
<div className="mb-2">Query</div>
<LazyQueryInput
className={classNames('form-control', styles.queryInput)}
<LazyQueryInputFormControl
patternType={defaultPatternType}
isSourcegraphDotCom={props.isSourcegraphDotCom}
caseSensitive={false}