Skip to content

Commit

Permalink
Add automaticHighlight prop
Browse files Browse the repository at this point in the history
  • Loading branch information
felixhabib committed Jul 15, 2024
1 parent 2a95673 commit 3c23cff
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 57 deletions.
2 changes: 1 addition & 1 deletion packages/braid-design-system/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@
"@vanilla-extract/dynamic": "^2.0.3",
"@vanilla-extract/sprinkles": "^1.5.1",
"assert": "^2.0.0",
"autosuggest-highlight": "^3.1.1",
"autosuggest-highlight": "^3.3.4",
"clsx": "^2.1.1",
"csstype": "^3.0.6",
"dedent": "^1.5.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,33 +330,6 @@ const docs: ComponentDocs = {
</>,
),
},
{
label: 'Client-side filtering',
description: (
<>
<Text>
The logic for filtering suggestions typically lives on the server
rather than the client because it’s impractical to send all possible
suggestions over the network. However, when prototyping in Playroom
or working with smaller datasets, you may want to perform this
filtering on the client instead. For this case, we provide a{' '}
<Strong>filterSuggestions</Strong> function to make this as painless
as possible.
</Text>
<Text>
If filtering is being performed on the server, this can be safely
omitted.
</Text>
<Alert tone="info">
<Text>
All examples on this page, except where noted, use the{' '}
<Strong>filterSuggestions</Strong> function to demonstrate
real-world filtering behaviour.
</Text>
</Alert>
</>
),
},
{
label: 'Automatic selection',
description: (
Expand Down Expand Up @@ -487,47 +460,114 @@ const docs: ComponentDocs = {
),
},
{
label: 'Suggestion highlights',
label: 'Client-side filtering',
description: (
<>
{/* Todo - callout autosuggest-highlight? */}
<Text>
Suggestion items can optionally contain a highlight range. In other
examples on this page, highlights are automatically handled by the{' '}
<Strong>filterSuggestions</Strong> function, highlighting the
portion of each suggestion that matches the search value.
The logic for filtering suggestions typically lives on the server
rather than the client because it’s impractical to send all possible
suggestions over the network. However, when prototyping in Playroom
or working with smaller datasets, you may want to perform this
filtering on the client instead. For this case, we provide a{' '}
<Strong>filterSuggestions</Strong> function to make this as painless
as possible.
</Text>
<Text>
If you are not using <Strong>filterSuggestions</Strong>, you can
provide a highlight range to each suggestion, as shown in the
example below.
If filtering is being performed on the server, this can be safely
omitted.
</Text>
<Alert tone="info">
<Text>
All examples on this page, except where noted, use the{' '}
<Strong>filterSuggestions</Strong> function to demonstrate
real-world filtering behaviour.
</Text>
</Alert>
</>
),
},
{
label: 'Automatic suggestion highlights',
description: (
<>
<Text>
While the <Strong>filterSuggestions</Strong> function will handle
highlights for you, you may need to separate the logic of
highlighting from filtering.
</Text>
<Text>
You can use the <Strong>automaticHighlights</Strong> prop to
automatically handle highlighting for you. In the following example,
while <Strong>filterSuggestions</Strong> is not used, highlights
work as expected.
</Text>
<Notice tone="info">
<Text>
The following example does not use accept input and does not use
the <Strong>filterSuggestions</Strong> function.
<Strong>automaticHighlights</Strong> can be configured further by
providing an <Strong>options</Strong> object.
</Text>
</Notice>
</>
),
Example: ({ id }) =>
Example: ({ id, setDefaultState, setState, getState, resetState }) =>
source(
<>
{setDefaultState('value', { text: '' })}

<Autosuggest
label="Label"
id={id}
value={{ text: 'App' }}
onChange={() => {}}
id={`${id}_highlights1`}
value={getState('value')}
onChange={setState('value')}
onClear={() => resetState('value')}
automaticHighlights
suggestions={[
{ text: 'Apples' },
{ text: 'Bananas' },
{ text: 'Carrots' },
]}
/>
</>,
),
},
{
label: 'Custom suggestion highlights',
description: (
<>
<Text>
If <Strong>automaticHighlights</Strong> is not suitable for your use
case, you can provide explicit highlight ranges for each suggestion.
</Text>
<Notice tone="info">
<Text>
This is a simplified example that does not use{' '}
<Strong>filterSuggestions</Strong>. Suggestions will not change
from your input.
</Text>
</Notice>
</>
),
Example: ({ id, setDefaultState, setState, getState, resetState }) =>
source(
<>
{setDefaultState('value', { text: '' })}

<Autosuggest
label="Label"
id={`${id}_highlights3`}
value={getState('value')}
onChange={setState('value')}
onClear={() => resetState('value')}
suggestions={[
{
text: 'Apples',
value: 1,
highlights: [{ start: 0, end: 3 }],
highlights: [{ start: 0, end: 2 }],
},
{
text: 'Apples and bananas',
text: 'Bananas',
value: 2,
highlights: [{ start: 0, end: 3 }],
highlights: [{ start: 0, end: 2 }],
},
]}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import React, {
} from 'react';
import dedent from 'dedent';
import parseHighlights from 'autosuggest-highlight/parse';
import matchHighlights from 'autosuggest-highlight/match';
import { Box } from '../Box/Box';
import { Text } from '../Text/Text';
import { Strong } from '../Strong/Strong';
Expand Down Expand Up @@ -277,6 +278,8 @@ interface LegacyMessageSuggestion {
message: string;
}

type MatchHighlightsOptions = Parameters<typeof matchHighlights>[2];

export type AutosuggestBaseProps<Value> = Omit<
FieldBaseProps,
'value' | 'autoComplete' | 'prefix'
Expand All @@ -292,6 +295,7 @@ export type AutosuggestBaseProps<Value> = Omit<
onChange: (value: AutosuggestValue<Value>) => void;
clearLabel?: string;
automaticSelection?: boolean;
automaticHighlights?: boolean | MatchHighlightsOptions;
hideSuggestionsOnSelection?: boolean;
showMobileBackdrop?: boolean;
scrollToTopOnMobile?: boolean;
Expand Down Expand Up @@ -337,6 +341,15 @@ function normaliseNoSuggestionMessage<Value>(
}
}

function getAutomaticHighlights(
suggestion: string,
value: string,
options?: MatchHighlightsOptions,
): SuggestionMatch {
const matches = matchHighlights(suggestion, value, options);
return matches.map(([start, end]) => ({ start, end }));
}

export const Autosuggest = forwardRef(function <Value>(
{
id,
Expand All @@ -345,6 +358,7 @@ export const Autosuggest = forwardRef(function <Value>(
noSuggestionsMessage: noSuggestionsMessageProp,
onChange = noop,
automaticSelection = false,
automaticHighlights = false,
showMobileBackdrop = false,
scrollToTopOnMobile = true,
hideSuggestionsOnSelection = true,
Expand Down Expand Up @@ -373,6 +387,23 @@ export const Autosuggest = forwardRef(function <Value>(
);
const hasItems = suggestions.length > 0 || Boolean(noSuggestionsMessage);

const hasExplicitHighlights = suggestions.some(
(suggestion) => 'highlights' in suggestion,
);

if (process.env.NODE_ENV !== 'production') {
if (automaticHighlights && hasExplicitHighlights) {
// eslint-disable-next-line no-console
console.warn(
dedent`
In Autosuggest, you are using the "automaticHighlights" prop with suggestions that already have highlights.
The provided highlights will be overridden.
If you want to use your own highlights, set "automaticHighlights" to false.
`,
);
}
}

// We need a ref regardless so we can imperatively
// focus the field when clicking the clear button
const defaultRef = useRef<HTMLInputElement | null>(null);
Expand Down Expand Up @@ -568,7 +599,7 @@ export const Autosuggest = forwardRef(function <Value>(
}
}
// re-running this effect if the suggestionCount changes
// to ensure asychronous updates aren't left out of view.
// to ensure asynchronous updates aren't left out of view.
}, [isOpen, isMobile, suggestionCount]);

const inputProps = {
Expand Down Expand Up @@ -804,14 +835,26 @@ export const Autosuggest = forwardRef(function <Value>(
? normalisedSuggestions.map((suggestion, index) => {
const { text } = suggestion;
const groupHeading = groupHeadingIndexes.get(index);
const highlights = automaticHighlights
? getAutomaticHighlights(
suggestion.text,
value.text,
typeof automaticHighlights === 'boolean'
? {}
: automaticHighlights,
)
: undefined;

return (
<Fragment key={index + text}>
{groupHeading ? (
<GroupHeading>{groupHeading}</GroupHeading>
) : null}
<SuggestionItem
suggestion={suggestion}
suggestion={{
...suggestion,
...(highlights && { highlights }),
}}
highlighted={highlightedIndex === index}
selected={value === suggestion}
onClick={() => {
Expand Down
18 changes: 9 additions & 9 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 3c23cff

Please sign in to comment.