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 350f2d7 commit d1bcc70
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 41 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 @@ -490,47 +490,100 @@ const docs: ComponentDocs = {
label: 'Suggestion highlights',
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.
While the <Strong>filterSuggestions</Strong> function will handle
highlights for you, you may need to separate the logic of
highlighting from filtering.
</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.
You can use the <Strong>automaticHighlights</Strong> prop to
automatically handle highlighting for you. By default, this will not
search within words. However, you can customise this and more
behaviour by providing it an <Strong>options</Strong> object.
</Text>
<Text>
If you require more custom behaviour, you can instead provide
explicit highlight ranges for each suggestion
</Text>
<Notice tone="info">
<Text>
The following example does not use accept input and does not use
the <Strong>filterSuggestions</Strong> function.
The following examples do not use the{' '}
<Strong>filterSuggestions</Strong> function, and the suggestions
will not be filtered.
</Text>
</Notice>
</>
),
Example: ({ id }) =>
Example: ({ id, setDefaultState, setState, getState, resetState }) =>
source(
<>
<Autosuggest
label="Label"
id={id}
value={{ text: 'App' }}
onChange={() => {}}
suggestions={[
{
text: 'Apples',
value: 1,
highlights: [{ start: 0, end: 3 }],
},
{
text: 'Apples and bananas',
value: 2,
highlights: [{ start: 0, end: 3 }],
},
]}
/>
{setDefaultState('field1', { text: '' })}
{setDefaultState('field2', { text: '' })}
{setDefaultState('field3', { text: '' })}

<Stack space="large">
<Autosuggest
label="Label"
description="Automatic highlights with default options"
id={`${id}_highlights1`}
value={getState('field1')}
onChange={setState('field1')}
onClear={() => resetState('field1')}
automaticHighlights
suggestions={[
{ text: 'Apples' },
{ text: 'Bananas' },
{ text: 'Carrots' },
]}
/>
<Autosuggest
label="Label"
description="Automatic highlights with custom options"
id={`${id}_highlights2`}
value={getState('field2')}
onChange={setState('field2')}
onClear={() => resetState('field2')}
automaticHighlights={{
insideWords: true,
findAllOccurrences: true,
}}
suggestions={[
{ text: 'Apples' },
{ text: 'Bananas' },
{ text: 'Carrots' },
]}
/>
<Autosuggest
label="Label"
description="Manually configured highlights"
id={`${id}_highlights3`}
value={getState('field3')}
onChange={setState('field3')}
onClear={() => resetState('field3')}
suggestions={[
{
text: 'Apples',
value: 1,
highlights: [{ start: 0, end: 2 }],
},
{
text: 'Bananas',
value: 2,
highlights: [{ start: 0, end: 2 }],
},
{
text: 'Broccoli',
value: 3,
highlights: [{ start: 0, end: 2 }],
},
{
text: 'Carrots',
value: 4,
highlights: [{ start: 0, end: 2 }],
},
]}
/>
</Stack>
</>,
),
},
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 (automaticHighlights && hasExplicitHighlights) {
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.warn(
dedent`
In Autosuggest, you are using the "automaticHighlight" prop with suggestions that already have highlights.
The provided highlights will be overridden.
If you want to use your own highlights, set "automaticHighlight" 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 d1bcc70

Please sign in to comment.