diff --git a/.changeset/five-pillows-fail.md b/.changeset/five-pillows-fail.md index 531e2191a1a..36a2d2bb204 100644 --- a/.changeset/five-pillows-fail.md +++ b/.changeset/five-pillows-fail.md @@ -6,5 +6,5 @@ Add new components: - UI components (`Dropdown`, `Spinner`, `UnStyledButton` and lots of icon components) - Editor components (`QueryEditor`, `VariableEditor`, `HeaderEditor` and `ResponseEditor`) - Toolbar components (`ExecuteButton` and `ToolbarButton`) -- Docs components (`Argument`, `DefaultValue`, `Directive`, `FieldLink` and `TypeLink`) +- Docs components (`Argument`, `DefaultValue`, `Directive`, `FieldLink`, `Search` and `TypeLink`) - `History` component diff --git a/custom-words.txt b/custom-words.txt index 832bd54c18d..94ce626729c 100644 --- a/custom-words.txt +++ b/custom-words.txt @@ -66,6 +66,7 @@ browserslist changesets codemirror codesandbox +combobox commitlint cosmicconfig dompurify diff --git a/packages/graphiql-react/package.json b/packages/graphiql-react/package.json index b8775f44678..a307d9e8847 100644 --- a/packages/graphiql-react/package.json +++ b/packages/graphiql-react/package.json @@ -38,6 +38,7 @@ }, "dependencies": { "@graphiql/toolkit": "^1.0.0-next.0", + "@reach/combobox": "^0.17.0", "codemirror": "^5.65.3", "codemirror-graphql": "^2.0.0-next.0", "copy-to-clipboard": "^3.2.0", diff --git a/packages/graphiql-react/src/explorer/components/__tests__/test-utils.ts b/packages/graphiql-react/src/explorer/components/__tests__/test-utils.ts index 25a0b185af5..7e6362f8a52 100644 --- a/packages/graphiql-react/src/explorer/components/__tests__/test-utils.ts +++ b/packages/graphiql-react/src/explorer/components/__tests__/test-utils.ts @@ -13,7 +13,6 @@ export function mockExplorerContextValue( push() {}, reset() {}, show() {}, - showSearch() {}, }; } diff --git a/packages/graphiql-react/src/explorer/components/search.css b/packages/graphiql-react/src/explorer/components/search.css new file mode 100644 index 00000000000..022930e75b5 --- /dev/null +++ b/packages/graphiql-react/src/explorer/components/search.css @@ -0,0 +1,102 @@ +@import url('@reach/combobox/styles.css'); + +[data-reach-combobox] { + color: var(--color-neutral-60); + + &:not([data-state='idle']) { + border-radius: var(--border-radius-4); + box-shadow: var(--box-shadow); + color: var(--color-neutral-100); + + & .graphiql-doc-explorer-search-input { + background: var(--color-neutral-0); + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + } +} + +.graphiql-doc-explorer-search-input { + align-items: center; + background-color: var(--color-neutral-7); + border-radius: var(--border-radius-4); + display: flex; + padding: var(--px-8) var(--px-12); +} + +[data-reach-combobox-input] { + border: none; + background-color: transparent; + margin-left: var(--px-4); + width: 100%; + + &:focus { + outline: none; + } +} + +[data-reach-combobox-popover] { + border: none; + border-bottom-left-radius: var(--border-radius-4); + border-bottom-right-radius: var(--border-radius-4); + border-top: 1px solid var(--color-neutral-15); + max-height: 400px; + overflow-y: auto; + + /** + * This makes sure that the logic for auto-scrolling the search results when + * using keyboard navigation works properly (we use `offsetTop` there). + */ + position: relative; +} + +[data-reach-combobox-list] { + font-size: var(--font-size-body); + padding: var(--px-4); +} + +[data-reach-combobox-option] { + border-radius: var(--border-radius-4); + color: var(--color-neutral-60); + overflow-x: hidden; + padding: var(--px-8) var(--px-12); + text-overflow: ellipsis; + white-space: nowrap; + + &[data-highlighted] { + background-color: var(--color-neutral-7); + } + + &:hover { + background-color: var(--color-neutral-10); + } + + & + & { + margin-top: var(--px-4); + } +} + +.graphiql-doc-explorer-search-type { + color: var(--color-blue); +} + +.graphiql-doc-explorer-search-field { + color: var(--color-orche); +} + +.graphiql-doc-explorer-search-argument { + color: var(--color-purple); +} + +.graphiql-doc-explorer-search-divider { + color: var(--color-neutral-60); + font-size: var(--font-size-hint); + font-weight: var(--font-weight-medium); + margin-top: var(--px-8); + padding: var(--px-8) var(--px-12); +} + +.graphiql-doc-explorer-search-empty { + color: var(--color-neutral-60); + padding: var(--px-8) var(--px-12); +} diff --git a/packages/graphiql-react/src/explorer/components/search.tsx b/packages/graphiql-react/src/explorer/components/search.tsx new file mode 100644 index 00000000000..71ed9eaaa5a --- /dev/null +++ b/packages/graphiql-react/src/explorer/components/search.tsx @@ -0,0 +1,323 @@ +import { + Combobox, + ComboboxInput, + ComboboxPopover, + ComboboxList, + ComboboxOption, +} from '@reach/combobox'; +import { + GraphQLArgument, + GraphQLField, + GraphQLInputField, + GraphQLNamedType, + isInputObjectType, + isInterfaceType, + isObjectType, +} from 'graphql'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { MagnifyingGlassIcon } from '../../icons'; +import { useSchemaContext } from '../../schema'; +import debounce from '../../utility/debounce'; + +import { useExplorerContext } from '../context'; + +import './search.css'; +import { renderType } from './utils'; + +export function Search() { + const { explorerNavStack, push } = useExplorerContext({ + nonNull: true, + caller: Search, + }); + + const inputRef = useRef(null); + const popoverRef = useRef(null); + + const getSearchResults = useSearchResults(); + const [searchValue, setSearchValue] = useState(''); + + const [results, setResults] = useState(getSearchResults(searchValue)); + const debouncedGetSearchResults = useMemo( + () => + debounce(200, (search: string) => { + setResults(getSearchResults(search)); + }), + [getSearchResults], + ); + useEffect(() => { + debouncedGetSearchResults(searchValue); + }, [debouncedGetSearchResults, searchValue]); + + useEffect(() => { + function handleKeyDown(event: KeyboardEvent) { + if (event.metaKey && event.keyCode === 75 && inputRef.current) { + inputRef.current.focus(); + } + } + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, []); + + const navItem = explorerNavStack[explorerNavStack.length - 1]; + + const shouldSearchBoxAppear = + explorerNavStack.length === 1 || + isObjectType(navItem.def) || + isInterfaceType(navItem.def) || + isInputObjectType(navItem.def); + + return shouldSearchBoxAppear ? ( + { + const def = value as unknown as TypeMatch | FieldMatch; + push( + 'field' in def + ? { name: def.field.name, def: def.field } + : { name: def.type.name, def: def.type }, + ); + }} + > +
{ + if (inputRef.current) { + inputRef.current.focus(); + } + }} + > + + { + setSearchValue(event.target.value); + }} + onKeyDown={event => { + if (!event.isDefaultPrevented()) { + const container = popoverRef.current; + if (!container) { + return; + } + + window.requestAnimationFrame(() => { + const element = container.querySelector('[aria-selected=true]'); + if (!(element instanceof HTMLElement)) { + return; + } + const top = element.offsetTop - container.scrollTop; + const bottom = + container.scrollTop + + container.clientHeight - + (element.offsetTop + element.clientHeight); + if (bottom < 0) { + container.scrollTop -= bottom; + } + if (top < 0) { + container.scrollTop += top; + } + }); + } + }} + placeholder="⌘ K" + ref={inputRef} + value={searchValue} + /> +
+ + + {/** + * Setting the `index` prop explicitly on the `ComboboxOption` solves + * buggy behavior of the internal ordering of the combobox items. + * (Sometimes this results in weird jumps when using the keyboard to + * navigate search results.) + */} + {results.within.map((result, i) => ( + + + + ))} + {results.within.length > 0 && + results.types.length + results.fields.length > 0 ? ( +
+ Other results +
+ ) : null} + {results.types.map((result, i) => ( + + + + ))} + {results.fields.map((result, i) => ( + + . + + + ))} + {results.within.length + + results.types.length + + results.fields.length === + 0 ? ( +
+ No results found +
+ ) : null} +
+
+
+ ) : null; +} + +type TypeMatch = { type: GraphQLNamedType }; + +type FieldMatch = { + type: GraphQLNamedType; + field: GraphQLField | GraphQLInputField; + argument?: GraphQLArgument; +}; + +export function useSearchResults(caller?: Function) { + const { explorerNavStack } = useExplorerContext({ + nonNull: true, + caller: caller || useSearchResults, + }); + const { schema } = useSchemaContext({ + nonNull: true, + caller: caller || useSearchResults, + }); + + const navItem = explorerNavStack[explorerNavStack.length - 1]; + + return useCallback( + (searchValue: string) => { + const matches: { + within: FieldMatch[]; + types: TypeMatch[]; + fields: FieldMatch[]; + } = { + within: [], + types: [], + fields: [], + }; + + if (!schema) { + return matches; + } + + const withinType = navItem.def; + + const typeMap = schema.getTypeMap(); + let typeNames = Object.keys(typeMap); + + // Move the within type name to be the first searched. + if (withinType) { + typeNames = typeNames.filter(n => n !== withinType.name); + typeNames.unshift(withinType.name); + } + + for (const typeName of typeNames) { + if ( + matches.within.length + + matches.types.length + + matches.fields.length >= + 100 + ) { + break; + } + + const type = typeMap[typeName]; + if (withinType !== type && isMatch(typeName, searchValue)) { + matches.types.push({ type }); + } + + if ( + !isObjectType(type) && + !isInterfaceType(type) && + !isInputObjectType(type) + ) { + continue; + } + + const fields = type.getFields(); + for (const fieldName in fields) { + const field = fields[fieldName]; + let matchingArgs: GraphQLArgument[] | undefined; + + if (!isMatch(fieldName, searchValue)) { + if ('args' in field) { + matchingArgs = field.args.filter(arg => + isMatch(arg.name, searchValue), + ); + if (matchingArgs.length === 0) { + continue; + } + } else { + continue; + } + } + + matches[withinType === type ? 'within' : 'fields'].push( + ...(matchingArgs + ? matchingArgs.map(argument => ({ type, field, argument })) + : [{ type, field }]), + ); + } + } + + return matches; + }, + [navItem.def, schema], + ); +} + +function isMatch(sourceText: string, searchValue: string) { + try { + const escaped = searchValue.replace(/[^_0-9A-Za-z]/g, ch => '\\' + ch); + return sourceText.search(new RegExp(escaped, 'i')) !== -1; + } catch (e) { + return sourceText.toLowerCase().indexOf(searchValue.toLowerCase()) !== -1; + } +} + +type TypeProps = { type: GraphQLNamedType }; + +function Type(props: TypeProps) { + return ( + {props.type.name} + ); +} + +type FieldProps = { + field: GraphQLField | GraphQLInputField; + argument?: GraphQLArgument; +}; + +function Field(props: FieldProps) { + return ( + <> + + {props.field.name} + + {props.argument ? ( + <> + ( + + {props.argument.name} + + :{' '} + {renderType(props.argument.type, namedType => ( + + ))} + ) + + ) : null} + + ); +} diff --git a/packages/graphiql-react/src/explorer/components/type-link.tsx b/packages/graphiql-react/src/explorer/components/type-link.tsx index 2f07ef9a9da..78c75d2c4cf 100644 --- a/packages/graphiql-react/src/explorer/components/type-link.tsx +++ b/packages/graphiql-react/src/explorer/components/type-link.tsx @@ -1,6 +1,7 @@ -import { GraphQLType, isListType, isNonNullType } from 'graphql'; +import { GraphQLType } from 'graphql'; import { useExplorerContext } from '../context'; +import { renderType } from './utils'; import './type-link.css'; @@ -15,31 +16,16 @@ export function TypeLink(props: TypeLinkProps) { return null; } - const type = props.type; - if (isNonNullType(type)) { - return ( - <> - ! - - ); - } - if (isListType(type)) { - return ( - <> - [] - - ); - } - return ( + return renderType(props.type, namedType => ( { event.preventDefault(); - push({ name: type.name, def: type }); + push({ name: namedType.name, def: namedType }); }} href="#" > - {type.name} + {namedType.name} - ); + )); } diff --git a/packages/graphiql-react/src/explorer/components/utils.tsx b/packages/graphiql-react/src/explorer/components/utils.tsx new file mode 100644 index 00000000000..6bef3892d7e --- /dev/null +++ b/packages/graphiql-react/src/explorer/components/utils.tsx @@ -0,0 +1,19 @@ +import { + GraphQLNamedType, + GraphQLType, + isListType, + isNonNullType, +} from 'graphql'; + +export function renderType( + type: GraphQLType, + renderNamedType: (namedType: GraphQLNamedType) => JSX.Element, +): JSX.Element { + if (isNonNullType(type)) { + return <>{renderType(type.ofType, renderNamedType)}!; + } + if (isListType(type)) { + return <>[{renderType(type.ofType, renderNamedType)}]; + } + return renderNamedType(type); +} diff --git a/packages/graphiql-react/src/explorer/context.tsx b/packages/graphiql-react/src/explorer/context.tsx index 703f6c0f4df..13747f6ece7 100644 --- a/packages/graphiql-react/src/explorer/context.tsx +++ b/packages/graphiql-react/src/explorer/context.tsx @@ -25,7 +25,6 @@ export type ExplorerFieldDef = export type ExplorerNavStackItem = { name: string; title?: string; - search?: string; def?: GraphQLNamedType | ExplorerFieldDef; }; @@ -48,7 +47,6 @@ export type ExplorerContextType = { pop(): void; reset(): void; show(): void; - showSearch(search: string): void; }; export const ExplorerContext = @@ -121,14 +119,6 @@ export function ExplorerContextProvider(props: ExplorerContextProviderProps) { setIsVisible(true); }, [onToggleVisibility, storage]); - const showSearch = useCallback((search: string) => { - setNavStack(currentState => { - const lastItem = currentState[currentState.length - 1]; - const allButLastItem = currentState.slice(0, -1) as ExplorerNavStack; - return [...allButLastItem, { ...lastItem, search }] as ExplorerNavStack; - }); - }, []); - useEffect(() => { if (isFetching) { reset(); @@ -144,9 +134,8 @@ export function ExplorerContextProvider(props: ExplorerContextProviderProps) { pop, reset, show, - showSearch, }), - [hide, isVisible, navStack, push, pop, reset, show, showSearch], + [hide, isVisible, navStack, push, pop, reset, show], ); return ( diff --git a/packages/graphiql-react/src/explorer/index.ts b/packages/graphiql-react/src/explorer/index.ts index 4816ac6ddd8..dc36b0a19f4 100644 --- a/packages/graphiql-react/src/explorer/index.ts +++ b/packages/graphiql-react/src/explorer/index.ts @@ -2,6 +2,7 @@ export { Argument } from './components/argument'; export { DefaultValue } from './components/default-value'; export { Directive } from './components/directive'; export { FieldLink } from './components/field-link'; +export { Search } from './components/search'; export { TypeLink } from './components/type-link'; export { ExplorerContext, diff --git a/packages/graphiql-react/src/icons/index.tsx b/packages/graphiql-react/src/icons/index.tsx index bd8cc0ea406..53857f87213 100644 --- a/packages/graphiql-react/src/icons/index.tsx +++ b/packages/graphiql-react/src/icons/index.tsx @@ -5,6 +5,7 @@ import _CopyIcon from './copy.svg'; import _DocsIcon from './docs.svg'; import _HistoryIcon from './history.svg'; import _KeyboardShortcutIcon from './keyboard-shortcut.svg'; +import _MagnifyingGlassIcon from './magnifying-glass.svg'; import _MergeIcon from './merge.svg'; import _PenIcon from './pen.svg'; import _PlayIcon from './play.svg'; @@ -28,6 +29,10 @@ export const KeyboardShortcutIcon = generateIcon( _KeyboardShortcutIcon, 'keyboard shortcut icon', ); +export const MagnifyingGlassIcon = generateIcon( + _MagnifyingGlassIcon, + 'magnifying glass icon', +); export const MergeIcon = generateIcon(_MergeIcon, 'merge icon'); export const PenIcon = generateIcon(_PenIcon, 'pen icon'); export const PlayIcon = generateIcon(_PlayIcon, 'play icon'); diff --git a/packages/graphiql-react/src/icons/magnifying-glass.svg b/packages/graphiql-react/src/icons/magnifying-glass.svg new file mode 100644 index 00000000000..b2593871633 --- /dev/null +++ b/packages/graphiql-react/src/icons/magnifying-glass.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/graphiql-react/src/index.ts b/packages/graphiql-react/src/index.ts index 38b6373ee4c..24a53cf3c42 100644 --- a/packages/graphiql-react/src/index.ts +++ b/packages/graphiql-react/src/index.ts @@ -29,6 +29,7 @@ export { ExplorerContext, ExplorerContextProvider, FieldLink, + Search, TypeLink, useExplorerContext, } from './explorer'; diff --git a/packages/graphiql/__mocks__/@graphiql/react.tsx b/packages/graphiql/__mocks__/@graphiql/react.tsx index cd86c034a11..e8ec104aa7b 100644 --- a/packages/graphiql/__mocks__/@graphiql/react.tsx +++ b/packages/graphiql/__mocks__/@graphiql/react.tsx @@ -44,6 +44,7 @@ export { ReloadIcon, SchemaContext, SchemaContextProvider, + Search, SettingsIcon, Spinner, StarFilledIcon, diff --git a/packages/graphiql/cypress/integration/docs.spec.ts b/packages/graphiql/cypress/integration/docs.spec.ts index 768948cce49..7d17ab0df48 100644 --- a/packages/graphiql/cypress/integration/docs.spec.ts +++ b/packages/graphiql/cypress/integration/docs.spec.ts @@ -23,42 +23,38 @@ describe('GraphiQL DocExplorer - search', () => { }); it('Searches docs for values', () => { - cy.get('label.search-box input').type('test'); - cy.get('.doc-category-item').should('have.length', 7); + cy.get('[data-reach-combobox-input]').type('test'); + cy.get('[data-reach-combobox-popover]').should('not.have.attr', 'hidden'); + cy.get('[data-reach-combobox-option]').should('have.length', 7); }); it('Navigates to a docs entry on selecting a search result', () => { - cy.get('.doc-search-items>.doc-category-item').eq(4).children().click(); + cy.get('[data-reach-combobox-option]').eq(4).children().click(); cy.get('.doc-explorer-title').should('have.text', 'TestInput'); }); it('Allows searching fields within a type', () => { - cy.get('label.search-box input').type('list'); - cy.get('.doc-category-item').should('have.length', 8); + cy.get('[data-reach-combobox-input]').type('list'); + cy.get('[data-reach-combobox-option]').should('have.length', 14); }); it('Shows "other results" section', () => { - cy.get('.doc-category-title').should('have.text', 'other results'); - cy.get('.doc-category .graphiql-doc-explorer-field-name').should( - 'have.text', - 'hasArgs', - ); + cy.get( + '[data-reach-combobox-popover] .graphiql-doc-explorer-search-divider', + ).should('have.text', 'Other results'); + cy.get('[data-reach-combobox-option]').contains('hasArgs'); }); - it('Navigates back to search results when existing', () => { + it('Navigates back and closes popover', () => { cy.get('.doc-explorer-back').click(); cy.get('.doc-explorer-title').should('have.text', 'Docs'); - }); - - it('Retains the parent search value', () => { - cy.get('label.search-box input').should('have.value', 'test'); + cy.get('[data-reach-combobox-popover]').should('have.attr', 'hidden'); }); it('Type fields link to their own docs entry', () => { - cy.get('.doc-search-items>.doc-category-item') - .last() - .find('a:nth-child(2)') - .click(); + cy.get('[data-reach-combobox-input]').type('test'); + cy.wait(250); + cy.get('[data-reach-combobox-option]').last().click(); cy.get('.doc-explorer-title').should('have.text', 'isTest'); cy.get('.graphiql-markdown-description').should( @@ -66,16 +62,6 @@ describe('GraphiQL DocExplorer - search', () => { 'Is this a test schema? Sure it is.\n', ); }); - - it('Allows clearing the search', () => { - cy.visit(`/`); - cy.get('.graphiql-sidebar button').eq(0).click(); - cy.get('label.search-box input').type('test'); - cy.get('.doc-category-item').should('have.length', 7); - cy.get('.search-box-clear').click(); - cy.get('.doc-category-title').should('have.text', 'root types'); - cy.get('label.search-box input').should('have.value', ''); - }); }); describe('GraphQL DocExplorer - deprecated fields', () => { diff --git a/packages/graphiql/src/components/DocExplorer.tsx b/packages/graphiql/src/components/DocExplorer.tsx index 4546a262836..46d66e43812 100644 --- a/packages/graphiql/src/components/DocExplorer.tsx +++ b/packages/graphiql/src/components/DocExplorer.tsx @@ -5,14 +5,17 @@ * LICENSE file in the root directory of this source tree. */ -import { Spinner, useExplorerContext, useSchemaContext } from '@graphiql/react'; +import { + Search, + Spinner, + useExplorerContext, + useSchemaContext, +} from '@graphiql/react'; import { GraphQLSchema, isType } from 'graphql'; import React, { ReactNode } from 'react'; import FieldDoc from './DocExplorer/FieldDoc'; import SchemaDoc from './DocExplorer/SchemaDoc'; -import SearchBox from './DocExplorer/SearchBox'; -import SearchResults from './DocExplorer/SearchResults'; import TypeDoc from './DocExplorer/TypeDoc'; type DocExplorerProps = { @@ -40,7 +43,7 @@ export function DocExplorer(props: DocExplorerProps) { schema: schemaFromContext, validationErrors, } = useSchemaContext({ nonNull: true }); - const { explorerNavStack, hide, pop, showSearch } = useExplorerContext({ + const { explorerNavStack, hide, pop } = useExplorerContext({ nonNull: true, }); @@ -65,8 +68,6 @@ export function DocExplorer(props: DocExplorerProps) { // Schema is null when it explicitly does not exist, typically due to // an error during introspection. content =
No Schema Available
; - } else if (navItem.search) { - content = ; } else if (explorerNavStack.length === 1) { content = ; } else if (isType(navItem.def)) { @@ -75,10 +76,6 @@ export function DocExplorer(props: DocExplorerProps) { content = ; } - const shouldSearchBoxAppear = - explorerNavStack.length === 1 || - (isType(navItem.def) && 'getFields' in navItem.def); - let prevName; if (explorerNavStack.length > 1) { prevName = explorerNavStack[explorerNavStack.length - 2].name; @@ -119,13 +116,7 @@ export function DocExplorer(props: DocExplorerProps) {
- {shouldSearchBoxAppear && ( - - )} + {content}
diff --git a/packages/graphiql/src/components/DocExplorer/SearchBox.tsx b/packages/graphiql/src/components/DocExplorer/SearchBox.tsx deleted file mode 100644 index 135764e9375..00000000000 --- a/packages/graphiql/src/components/DocExplorer/SearchBox.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright (c) 2021 GraphQL Contributors. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import React, { ChangeEventHandler } from 'react'; - -import debounce from '../../utility/debounce'; - -type OnSearchFn = (value: string) => void; - -type SearchBoxProps = { - value?: string; - placeholder: string; - onSearch: OnSearchFn; -}; - -type SearchBoxState = { - value: string; -}; - -export default class SearchBox extends React.Component< - SearchBoxProps, - SearchBoxState -> { - debouncedOnSearch: OnSearchFn; - - constructor(props: SearchBoxProps) { - super(props); - this.state = { value: props.value || '' }; - this.debouncedOnSearch = debounce(200, this.props.onSearch); - } - - render() { - return ( - - ); - } - - handleChange: ChangeEventHandler = event => { - const value = event.currentTarget.value; - this.setState({ value }); - this.debouncedOnSearch(value); - }; - - handleClear = () => { - this.setState({ value: '' }); - this.props.onSearch(''); - }; -} diff --git a/packages/graphiql/src/components/DocExplorer/SearchResults.tsx b/packages/graphiql/src/components/DocExplorer/SearchResults.tsx deleted file mode 100644 index b38e70fd466..00000000000 --- a/packages/graphiql/src/components/DocExplorer/SearchResults.tsx +++ /dev/null @@ -1,137 +0,0 @@ -/** - * Copyright (c) 2021 GraphQL Contributors. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { - Argument, - FieldLink, - TypeLink, - useExplorerContext, - useSchemaContext, -} from '@graphiql/react'; -import React, { ReactNode } from 'react'; - -export default function SearchResults() { - const { explorerNavStack } = useExplorerContext({ nonNull: true }); - const { schema } = useSchemaContext({ nonNull: true }); - - const navItem = explorerNavStack[explorerNavStack.length - 1]; - - if (!schema || !navItem.search) { - return null; - } - - const searchValue = navItem.search; - const withinType = navItem.def; - - const matchedWithin: ReactNode[] = []; - const matchedTypes: ReactNode[] = []; - const matchedFields: ReactNode[] = []; - - const typeMap = schema.getTypeMap(); - let typeNames = Object.keys(typeMap); - - // Move the within type name to be the first searched. - if (withinType) { - typeNames = typeNames.filter(n => n !== withinType.name); - typeNames.unshift(withinType.name); - } - - for (const typeName of typeNames) { - if ( - matchedWithin.length + matchedTypes.length + matchedFields.length >= - 100 - ) { - break; - } - - const type = typeMap[typeName]; - if (withinType !== type && isMatch(typeName, searchValue)) { - matchedTypes.push( -
- -
, - ); - } - - if (type && 'getFields' in type) { - const fields = type.getFields(); - Object.keys(fields).forEach(fieldName => { - const field = fields[fieldName]; - let matchingArgs; - - if (!isMatch(fieldName, searchValue)) { - if ('args' in field && field.args.length) { - matchingArgs = field.args.filter(arg => - isMatch(arg.name, searchValue), - ); - if (matchingArgs.length === 0) { - return; - } - } else { - return; - } - } - - const match = ( -
- {withinType !== type && [, '.']} - - {matchingArgs && [ - '(', - - {matchingArgs.map(arg => ( - - ))} - , - ')', - ]} -
- ); - - if (withinType === type) { - matchedWithin.push(match); - } else { - matchedFields.push(match); - } - }); - } - } - - if (matchedWithin.length + matchedTypes.length + matchedFields.length === 0) { - return No results found.; - } - - if (withinType && matchedTypes.length + matchedFields.length > 0) { - return ( -
- {matchedWithin} -
-
other results
- {matchedTypes} - {matchedFields} -
-
- ); - } - - return ( -
- {matchedWithin} - {matchedTypes} - {matchedFields} -
- ); -} - -function isMatch(sourceText: string, searchValue: string) { - try { - const escaped = searchValue.replace(/[^_0-9A-Za-z]/g, ch => '\\' + ch); - return sourceText.search(new RegExp(escaped, 'i')) !== -1; - } catch (e) { - return sourceText.toLowerCase().indexOf(searchValue.toLowerCase()) !== -1; - } -} diff --git a/yarn.lock b/yarn.lock index dc7d059a66f..d474fdb1c92 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4435,6 +4435,80 @@ tslib "^2.4.0" webcrypto-core "^1.7.4" +"@reach/auto-id@0.17.0": + version "0.17.0" + resolved "https://registry.yarnpkg.com/@reach/auto-id/-/auto-id-0.17.0.tgz#60cce65eb7a0d6de605820727f00dfe2b03b5f17" + integrity sha512-ud8iPwF52RVzEmkHq1twuqGuPA+moreumUHdtgvU3sr3/15BNhwp3KyDLrKKSz0LP1r3V4pSdyF9MbYM8BoSjA== + dependencies: + "@reach/utils" "0.17.0" + tslib "^2.3.0" + +"@reach/combobox@^0.17.0": + version "0.17.0" + resolved "https://registry.yarnpkg.com/@reach/combobox/-/combobox-0.17.0.tgz#fb9d71d2d5aff3b339dce0ec5e3b73628a51b009" + integrity sha512-2mYvU5agOBCQBMdlM4cri+P1BbNwp05P1OuDyc33xJSNiBG7BMy4+ZSHJ0X4fyle6rHwSgCAOCLOeWV1XUYjoQ== + dependencies: + "@reach/auto-id" "0.17.0" + "@reach/descendants" "0.17.0" + "@reach/popover" "0.17.0" + "@reach/portal" "0.17.0" + "@reach/utils" "0.17.0" + prop-types "^15.7.2" + tiny-warning "^1.0.3" + tslib "^2.3.0" + +"@reach/descendants@0.17.0": + version "0.17.0" + resolved "https://registry.yarnpkg.com/@reach/descendants/-/descendants-0.17.0.tgz#3fb087125a67870acd4dee1528449ed546829b67" + integrity sha512-c7lUaBfjgcmKFZiAWqhG+VnXDMEhPkI4kAav/82XKZD6NVvFjsQOTH+v3tUkskrAPV44Yuch0mFW/u5Ntifr7Q== + dependencies: + "@reach/utils" "0.17.0" + tslib "^2.3.0" + +"@reach/observe-rect@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@reach/observe-rect/-/observe-rect-1.2.0.tgz#d7a6013b8aafcc64c778a0ccb83355a11204d3b2" + integrity sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ== + +"@reach/popover@0.17.0": + version "0.17.0" + resolved "https://registry.yarnpkg.com/@reach/popover/-/popover-0.17.0.tgz#feda6961f37d17b8738d2d52af6bfc5c4584464f" + integrity sha512-yYbBF4fMz4Ml4LB3agobZjcZ/oPtPsNv70ZAd7lEC2h7cvhF453pA+zOBGYTPGupKaeBvgAnrMjj7RnxDU5hoQ== + dependencies: + "@reach/portal" "0.17.0" + "@reach/rect" "0.17.0" + "@reach/utils" "0.17.0" + tabbable "^4.0.0" + tslib "^2.3.0" + +"@reach/portal@0.17.0": + version "0.17.0" + resolved "https://registry.yarnpkg.com/@reach/portal/-/portal-0.17.0.tgz#1dd69ffc8ffc8ba3e26dd127bf1cc4b15f0c6bdc" + integrity sha512-+IxsgVycOj+WOeNPL2NdgooUdHPSY285wCtj/iWID6akyr4FgGUK7sMhRM9aGFyrGpx2vzr+eggbUmAVZwOz+A== + dependencies: + "@reach/utils" "0.17.0" + tiny-warning "^1.0.3" + tslib "^2.3.0" + +"@reach/rect@0.17.0": + version "0.17.0" + resolved "https://registry.yarnpkg.com/@reach/rect/-/rect-0.17.0.tgz#804f0cfb211e0beb81632c64d4532ec9d1d73c48" + integrity sha512-3YB7KA5cLjbLc20bmPkJ06DIfXSK06Cb5BbD2dHgKXjUkT9WjZaLYIbYCO8dVjwcyO3GCNfOmPxy62VsPmZwYA== + dependencies: + "@reach/observe-rect" "1.2.0" + "@reach/utils" "0.17.0" + prop-types "^15.7.2" + tiny-warning "^1.0.3" + tslib "^2.3.0" + +"@reach/utils@0.17.0": + version "0.17.0" + resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.17.0.tgz#3d1d2ec56d857f04fe092710d8faee2b2b121303" + integrity sha512-M5y8fCBbrWeIsxedgcSw6oDlAMQDkl5uv3VnMVJ7guwpf4E48Xlh1v66z/1BgN/WYe2y8mB/ilFD2nysEfdGeA== + dependencies: + tiny-warning "^1.0.3" + tslib "^2.3.0" + "@rollup/pluginutils@^4.1.1": version "4.1.1" resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.1.1.tgz#1d4da86dd4eded15656a57d933fda2b9a08d47ec" @@ -15345,7 +15419,7 @@ prop-types@15.7.2, prop-types@^15.6.1, prop-types@^15.6.2: object-assign "^4.1.1" react-is "^16.8.1" -prop-types@^15.8.1: +prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -17303,6 +17377,11 @@ symbol-tree@^3.2.4: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== +tabbable@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-4.0.0.tgz#5bff1d1135df1482cf0f0206434f15eadbeb9261" + integrity sha512-H1XoH1URcBOa/rZZWxLxHCtOdVUEev+9vo5YdYhC9tCY4wnybX+VQrCYuy9ubkg69fCBxCONJOSLGfw0DWMffQ== + tapable@^1.0.0, tapable@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" @@ -17432,6 +17511,11 @@ timsort@^0.3.0: resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= +tiny-warning@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" + integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== + tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -17641,7 +17725,7 @@ tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2, tslib@^2.0.0, tslib@^2.4.0, tslib@~2.4.0: +tslib@^2, tslib@^2.0.0, tslib@^2.3.0, tslib@^2.4.0, tslib@~2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==