From ce35fea95a3d408064eba698d47ac0c57bd58349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Vannicatte?= <20689156+shortcuts@users.noreply.github.com> Date: Fri, 5 Feb 2021 12:15:19 +0100 Subject: [PATCH] feat(sourceId): Add `sourceId` to provide `data-autocomplete-source-id` on `section` source container (#429) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Implements required source identifier `sourceId` used as value for the `data-autocomplete-source-id` attribute of the source `section` container * Update docs * Update tests * add reference to `sourceId` and `data-autocomplete-source-id` in autocomplete-js Co-authored-by: François Chalifour --- examples/js/app.tsx | 1 + examples/js/shortcutsPlugin.tsx | 1 + .../src/__tests__/concurrency.test.ts | 6 +- .../src/__tests__/getInputProps.test.ts | 1 + .../src/__tests__/getSources.test.ts | 4 ++ .../src/__tests__/stallThreshold.test.ts | 10 +-- .../src/types/AutocompleteSource.ts | 4 ++ .../__tests__/getNormalizedSources.test.ts | 65 +++++++++++++++++-- .../src/utils/getNormalizedSources.ts | 5 ++ .../src/__tests__/autocomplete.test.ts | 11 ++++ .../src/__tests__/positioning.test.ts | 1 + packages/autocomplete-js/src/render.tsx | 6 +- .../src/types/AutocompleteSource.ts | 12 +++- .../src/createQuerySuggestionsPlugin.ts | 1 + .../src/createRecentSearchesPlugin.ts | 1 + packages/website/docs/autocomplete-js.md | 1 + packages/website/docs/createAutocomplete.md | 1 + packages/website/docs/creating-a-renderer.md | 4 ++ packages/website/docs/keyboard-navigation.md | 1 + packages/website/docs/sources.md | 14 ++++ test/utils/createSource.ts | 1 + 21 files changed, 135 insertions(+), 16 deletions(-) diff --git a/examples/js/app.tsx b/examples/js/app.tsx index 0faeda914..a03594667 100644 --- a/examples/js/app.tsx +++ b/examples/js/app.tsx @@ -62,6 +62,7 @@ autocomplete({ return [ { + sourceId: 'products', getItems() { return getAlgoliaHits({ searchClient, diff --git a/examples/js/shortcutsPlugin.tsx b/examples/js/shortcutsPlugin.tsx index 76b104245..42f8b5732 100644 --- a/examples/js/shortcutsPlugin.tsx +++ b/examples/js/shortcutsPlugin.tsx @@ -14,6 +14,7 @@ export const shortcutsPlugin: AutocompletePlugin = { return [ { + sourceId: 'shortcutsPlugin', getItems() { return [ { diff --git a/packages/autocomplete-core/src/__tests__/concurrency.test.ts b/packages/autocomplete-core/src/__tests__/concurrency.test.ts index 1a02933dc..9921cb1cd 100644 --- a/packages/autocomplete-core/src/__tests__/concurrency.test.ts +++ b/packages/autocomplete-core/src/__tests__/concurrency.test.ts @@ -1,6 +1,6 @@ import userEvent from '@testing-library/user-event'; -import { defer } from '../../../../test/utils'; +import { createSource, defer } from '../../../../test/utils'; import { createAutocomplete } from '../createAutocomplete'; describe.skip('concurrency', () => { @@ -14,11 +14,11 @@ describe.skip('concurrency', () => { return defer(() => { return [ - { + createSource({ getItems() { return [{ label: query }]; }, - }, + }), ]; }, delays[deferCount]); }; diff --git a/packages/autocomplete-core/src/__tests__/getInputProps.test.ts b/packages/autocomplete-core/src/__tests__/getInputProps.test.ts index 648747b46..8a56a473d 100644 --- a/packages/autocomplete-core/src/__tests__/getInputProps.test.ts +++ b/packages/autocomplete-core/src/__tests__/getInputProps.test.ts @@ -1049,6 +1049,7 @@ describe('getInputProps', () => { { label: '2', url: '#2' }, ], source: { + sourceId: expect.any(String), getItemInputValue: expect.any(Function), getItemUrl: expect.any(Function), getItems: expect.any(Function), diff --git a/packages/autocomplete-core/src/__tests__/getSources.test.ts b/packages/autocomplete-core/src/__tests__/getSources.test.ts index b869b8009..f6243ff7c 100644 --- a/packages/autocomplete-core/src/__tests__/getSources.test.ts +++ b/packages/autocomplete-core/src/__tests__/getSources.test.ts @@ -47,6 +47,7 @@ describe('getSources', () => { getSources: () => { return [ { + sourceId: 'testSource', getItems() { return []; }, @@ -78,6 +79,7 @@ describe('getSources', () => { templates: expect.objectContaining({ item: expect.any(Function), }), + sourceId: expect.any(String), }, }), ]), @@ -92,6 +94,7 @@ describe('getSources', () => { getSources: () => { return [ { + sourceId: 'pluginSource', getItems() { return []; }, @@ -107,6 +110,7 @@ describe('getSources', () => { getSources: () => { return [ { + sourceId: 'testSource', getItems() { return []; }, diff --git a/packages/autocomplete-core/src/__tests__/stallThreshold.test.ts b/packages/autocomplete-core/src/__tests__/stallThreshold.test.ts index c2e91b470..5c1ab4bf8 100644 --- a/packages/autocomplete-core/src/__tests__/stallThreshold.test.ts +++ b/packages/autocomplete-core/src/__tests__/stallThreshold.test.ts @@ -1,6 +1,6 @@ import userEvent from '@testing-library/user-event'; -import { defer } from '../../../../test/utils'; +import { createSource, defer } from '../../../../test/utils'; import { createAutocomplete } from '../createAutocomplete'; describe('stallThreshold', () => { @@ -11,11 +11,11 @@ describe('stallThreshold', () => { getSources() { return defer(() => { return [ - { + createSource({ getItems() { return [{ label: '1' }, { label: 2 }]; }, - }, + }), ]; }, 500); }, @@ -59,11 +59,11 @@ describe('stallThreshold', () => { getSources() { return defer(() => { return [ - { + createSource({ getItems() { return [{ label: '1' }, { label: 2 }]; }, - }, + }), ]; }, 500); }, diff --git a/packages/autocomplete-core/src/types/AutocompleteSource.ts b/packages/autocomplete-core/src/types/AutocompleteSource.ts index 8cc4e62fb..5c7058683 100644 --- a/packages/autocomplete-core/src/types/AutocompleteSource.ts +++ b/packages/autocomplete-core/src/types/AutocompleteSource.ts @@ -58,6 +58,10 @@ export interface AutocompleteSource { * You can trigger different behaviors based on the event `type`. */ onActive?(params: OnActiveParams): void; + /** + * Identifier for the source. + */ + sourceId: string; } export type InternalAutocompleteSource = { diff --git a/packages/autocomplete-core/src/utils/__tests__/getNormalizedSources.test.ts b/packages/autocomplete-core/src/utils/__tests__/getNormalizedSources.test.ts index d5d1a0ede..c4cf54f5f 100644 --- a/packages/autocomplete-core/src/utils/__tests__/getNormalizedSources.test.ts +++ b/packages/autocomplete-core/src/utils/__tests__/getNormalizedSources.test.ts @@ -7,7 +7,7 @@ import { getNormalizedSources } from '../getNormalizedSources'; describe('getNormalizedSources', () => { test('returns a promise of sources', async () => { - const getSources = () => [{ getItems: () => [] }]; + const getSources = () => [{ sourceId: 'testSource', getItems: () => [] }]; const params = { query: '', state: createState({ @@ -23,12 +23,17 @@ describe('getNormalizedSources', () => { getItems: expect.any(Function), onActive: expect.any(Function), onSelect: expect.any(Function), + sourceId: 'testSource', }, ]); }); test('filters out falsy sources', async () => { - const getSources = () => [{ getItems: () => [] }, false, undefined]; + const getSources = () => [ + { sourceId: 'testSource', getItems: () => [] }, + false, + undefined, + ]; const params = { query: '', state: createState({ @@ -44,6 +49,7 @@ describe('getNormalizedSources', () => { getItems: expect.any(Function), onActive: expect.any(Function), onSelect: expect.any(Function), + sourceId: 'testSource', }, ]); }); @@ -64,8 +70,55 @@ describe('getNormalizedSources', () => { ); }); + test('with missing `sourceId` triggers invariant', async () => { + const getSources = () => [ + { + getItems() { + return []; + }, + templates: { + item() {}, + }, + }, + ]; + const params = { + query: '', + state: createState({}), + ...createScopeApi(), + }; + + // @ts-expect-error + await expect(getNormalizedSources(getSources, params)).rejects.toEqual( + new Error('[Autocomplete] A source must provide a `sourceId` string.') + ); + }); + + test('with wrong `sourceId` type triggers invariant', async () => { + const getSources = () => [ + { + sourceId: ['testSource'], + getItems() { + return []; + }, + templates: { + item() {}, + }, + }, + ]; + const params = { + query: '', + state: createState({}), + ...createScopeApi(), + }; + + // @ts-expect-error + await expect(getNormalizedSources(getSources, params)).rejects.toEqual( + new Error('[Autocomplete] A source must provide a `sourceId` string.') + ); + }); + test('provides a default implementation for getItemInputValue which returns the query', async () => { - const getSources = () => [{ getItems: () => [] }]; + const getSources = () => [{ sourceId: 'testSource', getItems: () => [] }]; const params = { query: '', state: createState({ @@ -82,7 +135,7 @@ describe('getNormalizedSources', () => { }); test('provides a default implementation for getItemUrl', async () => { - const getSources = () => [{ getItems: () => [] }]; + const getSources = () => [{ sourceId: 'testSource', getItems: () => [] }]; const params = { query: '', state: createState({}), @@ -97,7 +150,7 @@ describe('getNormalizedSources', () => { }); test('provides a default implementation for onSelect', async () => { - const getSources = () => [{ getItems: () => [] }]; + const getSources = () => [{ sourceId: 'testSource', getItems: () => [] }]; const params = { query: '', state: createState({}), @@ -119,7 +172,7 @@ describe('getNormalizedSources', () => { }); test('provides a default implementation for onActive', async () => { - const getSources = () => [{ getItems: () => [] }]; + const getSources = () => [{ sourceId: 'testSource', getItems: () => [] }]; const params = { query: '', state: createState({}), diff --git a/packages/autocomplete-core/src/utils/getNormalizedSources.ts b/packages/autocomplete-core/src/utils/getNormalizedSources.ts index 480f6eacb..fb560c50b 100644 --- a/packages/autocomplete-core/src/utils/getNormalizedSources.ts +++ b/packages/autocomplete-core/src/utils/getNormalizedSources.ts @@ -32,6 +32,11 @@ export function getNormalizedSources( Boolean(maybeSource) ) .map((source) => { + invariant( + typeof source.sourceId === 'string', + 'A source must provide a `sourceId` string.' + ); + const normalizedSource: InternalAutocompleteSource = { getItemInputValue({ state }) { return state.query; diff --git a/packages/autocomplete-js/src/__tests__/autocomplete.test.ts b/packages/autocomplete-js/src/__tests__/autocomplete.test.ts index 23035ae68..5bd5c03ef 100644 --- a/packages/autocomplete-js/src/__tests__/autocomplete.test.ts +++ b/packages/autocomplete-js/src/__tests__/autocomplete.test.ts @@ -11,6 +11,7 @@ describe('autocomplete-js', () => { getSources() { return [ { + sourceId: 'testSource', getItems() { return [ { label: 'Item 1' }, @@ -167,6 +168,7 @@ describe('autocomplete-js', () => { getSources() { return [ { + sourceId: 'testSource', getItems() { return []; }, @@ -206,6 +208,7 @@ describe('autocomplete-js', () => { getSources() { return [ { + sourceId: 'testSource', getItems() { return []; }, @@ -245,6 +248,7 @@ describe('autocomplete-js', () => { getSources() { return [ { + sourceId: 'testSource', getItems() { return []; }, @@ -290,6 +294,7 @@ describe('autocomplete-js', () => { getSources() { return [ { + sourceId: 'testSource', getItems() { return []; }, @@ -338,6 +343,7 @@ describe('autocomplete-js', () => { getSources() { return [ { + sourceId: 'testSource', getItems() { return []; }, @@ -383,6 +389,7 @@ describe('autocomplete-js', () => { getSources() { return [ { + sourceId: 'testSource', getItems() { return [ { label: 'Item 1' }, @@ -418,6 +425,7 @@ describe('autocomplete-js', () => { getSources() { return [ { + sourceId: 'testSource', getItems() { return [ { label: 'Item 1' }, @@ -449,6 +457,7 @@ describe('autocomplete-js', () => { getSources() { return [ { + sourceId: 'testSource', getItems() { return [ { label: 'Item 1' }, @@ -482,6 +491,7 @@ describe('autocomplete-js', () => { getSources() { return [ { + sourceId: 'testSource', getItems() { return [ { label: 'Item 1' }, @@ -512,6 +522,7 @@ describe('autocomplete-js', () => { getSources() { return [ { + sourceId: 'testSource', getItems() { return [ { label: 'Item 1' }, diff --git a/packages/autocomplete-js/src/__tests__/positioning.test.ts b/packages/autocomplete-js/src/__tests__/positioning.test.ts index 34b5f3e95..ee1540a26 100644 --- a/packages/autocomplete-js/src/__tests__/positioning.test.ts +++ b/packages/autocomplete-js/src/__tests__/positioning.test.ts @@ -35,6 +35,7 @@ const querySuggestionsFixturePlugin: AutocompletePlugin< getSources() { return [ { + sourceId: 'testSource', getItems() { return querySuggestions; }, diff --git a/packages/autocomplete-js/src/render.tsx b/packages/autocomplete-js/src/render.tsx index bcd588b6f..7eb5d13c9 100644 --- a/packages/autocomplete-js/src/render.tsx +++ b/packages/autocomplete-js/src/render.tsx @@ -92,7 +92,11 @@ export function renderPanel( dom.panel.classList.toggle('aa-Panel--stalled', state.status === 'stalled'); const sections = state.collections.map(({ source, items }, sourceIndex) => ( -
+
{source.templates.header && (
{source.templates.header({ diff --git a/packages/autocomplete-js/src/types/AutocompleteSource.ts b/packages/autocomplete-js/src/types/AutocompleteSource.ts index 943e12d3b..d0ee2d232 100644 --- a/packages/autocomplete-js/src/types/AutocompleteSource.ts +++ b/packages/autocomplete-js/src/types/AutocompleteSource.ts @@ -53,10 +53,20 @@ type WithTemplates = TType & { templates: SourceTemplates; }; +export interface AutocompleteCoreSourceWithDocs + extends AutocompleteCoreSource { + /** + * Identifier for the source. + * It is used as value for the `data-autocomplete-source-id` attribute of the source `section` container. + */ + sourceId: string; +} + export type AutocompleteSource = WithTemplates< - AutocompleteCoreSource, + AutocompleteCoreSourceWithDocs, TItem >; + export type InternalAutocompleteSource = WithTemplates< InternalAutocompleteCoreSource, TItem diff --git a/packages/autocomplete-plugin-query-suggestions/src/createQuerySuggestionsPlugin.ts b/packages/autocomplete-plugin-query-suggestions/src/createQuerySuggestionsPlugin.ts index 11d0cef3f..517baa1a8 100644 --- a/packages/autocomplete-plugin-query-suggestions/src/createQuerySuggestionsPlugin.ts +++ b/packages/autocomplete-plugin-query-suggestions/src/createQuerySuggestionsPlugin.ts @@ -36,6 +36,7 @@ export function createQuerySuggestionsPlugin< getSources({ query, setQuery, refresh, state }) { return [ { + sourceId: 'querySuggestionsPlugin', getItemInputValue({ item }) { return item.query; }, diff --git a/packages/autocomplete-plugin-recent-searches/src/createRecentSearchesPlugin.ts b/packages/autocomplete-plugin-recent-searches/src/createRecentSearchesPlugin.ts index 44fda124c..666e1b57f 100644 --- a/packages/autocomplete-plugin-recent-searches/src/createRecentSearchesPlugin.ts +++ b/packages/autocomplete-plugin-recent-searches/src/createRecentSearchesPlugin.ts @@ -68,6 +68,7 @@ export function createRecentSearchesPlugin({ return [ { + sourceId: 'recentSearchesPlugin', getItemInputValue({ item }) { return item.query; }, diff --git a/packages/website/docs/autocomplete-js.md b/packages/website/docs/autocomplete-js.md index f6ee5f2bb..234cb3dc1 100644 --- a/packages/website/docs/autocomplete-js.md +++ b/packages/website/docs/autocomplete-js.md @@ -29,6 +29,7 @@ const autocompleteSearch = autocomplete({ getSources() { return [ { + sourceId: 'querySuggestionsSources', getItemInputValue: ({ item }) => item.query, getItems({ query }) { return getAlgoliaHits({ diff --git a/packages/website/docs/createAutocomplete.md b/packages/website/docs/createAutocomplete.md index ab815b647..bffbe9f85 100644 --- a/packages/website/docs/createAutocomplete.md +++ b/packages/website/docs/createAutocomplete.md @@ -22,6 +22,7 @@ const autocomplete = createAutocomplete({ getSources() { return [ { + sourceId: 'querySuggestionsSource', getItemInputValue: ({ item }) => item.query, getItems({ query }) { return getAlgoliaHits({ diff --git a/packages/website/docs/creating-a-renderer.md b/packages/website/docs/creating-a-renderer.md index d6a93f313..61daf72fd 100644 --- a/packages/website/docs/creating-a-renderer.md +++ b/packages/website/docs/creating-a-renderer.md @@ -44,6 +44,10 @@ function Autocomplete() { return [ // (3) Use an Algolia index source. { + sourceId: 'querySuggestionsSource', + getItemInputValue({ item }) { + return item.query; + }, getItems({ query }) { return getAlgoliaHits({ searchClient, diff --git a/packages/website/docs/keyboard-navigation.md b/packages/website/docs/keyboard-navigation.md index de1f74370..da474c06c 100644 --- a/packages/website/docs/keyboard-navigation.md +++ b/packages/website/docs/keyboard-navigation.md @@ -24,6 +24,7 @@ const autocomplete = createAutocomplete({ getSources() { return [ { + sourceId: 'mySource', getItemUrl({ item }) { return item.url; }, diff --git a/packages/website/docs/sources.md b/packages/website/docs/sources.md index fd0ab0038..2ceb703e0 100644 --- a/packages/website/docs/sources.md +++ b/packages/website/docs/sources.md @@ -16,6 +16,7 @@ const autocomplete = createAutocomplete({ getSources() { return [ { + sourceId: 'staticSource', getItems() { return [ { label: 'Twitter', url: 'https://twitter.com' }, @@ -40,6 +41,7 @@ const autocomplete = createAutocomplete({ getSources() { return [ { + sourceId: 'staticSource', getItems({ query }) { return [ { label: 'Twitter', url: 'https://twitter.com' }, @@ -71,6 +73,7 @@ const autocomplete = createAutocomplete({ getSources() { return [ { + sourceId: 'algoliaHits', getItems({ query }) { return getAlgoliaHits({ searchClient, @@ -112,6 +115,7 @@ const autocomplete = createAutocomplete({ if (!query) { [ { + sourceId: 'staticSource', getItems() { return [ { label: 'Twitter', url: 'https://twitter.com' }, @@ -127,6 +131,7 @@ const autocomplete = createAutocomplete({ return [ { + sourceId: 'algoliaHits', getItems() { return getAlgoliaHits({ searchClient, @@ -184,12 +189,14 @@ const autocomplete = createAutocomplete({ return [ { + sourceId: 'querySuggestionsSource', getItems() { return querySuggestions.hits; }, getItemInputValue: ({ item }) => item.query, }, { + sourceId: 'algoliaHits', getItems() { return products.hits; }, @@ -283,6 +290,12 @@ Called when an item is active. You can trigger different behaviors if the item is active following a mouse event or a keyboard event based on the `event` param. +### `sourceId` + +> `string` + +Identifier for the source. It is used as value for the `data-autocomplete-source-id` attribute of the source `section` container. + ### `templates` (specific to `@algolia/autocomplete-js`) > `SourceTemplate` @@ -336,6 +349,7 @@ const autocompleteSearch = autocomplete({ getSources() { return [ { + sourceId: 'querySuggestionsSource', getItemInputValue({ item }) { return item.query; }, diff --git a/test/utils/createSource.ts b/test/utils/createSource.ts index 7c92f9411..537611309 100644 --- a/test/utils/createSource.ts +++ b/test/utils/createSource.ts @@ -8,6 +8,7 @@ export function createSource( source?: Partial> ): InternalAutocompleteSource { return { + sourceId: 'testSource', getItemInputValue: ({ state }) => state.query, getItemUrl: () => undefined, onActive: () => {},