Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Autocomplete: refactor to TypeScript #47751

Merged
merged 38 commits into from
Mar 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
def9352
Remove from exclude list
chad1008 Jan 23, 2023
566d9e3
Rename and add files
chad1008 Jan 23, 2023
7c43d84
Add `WPCompleter` typing
chad1008 Jan 23, 2023
f1c9041
Add `AutocompleterUI` typing
chad1008 Jan 25, 2023
c31f4e4
Add `getDeafaultUseItems` typing
chad1008 Jan 26, 2023
dbda576
Add `useAutocomplete` typing
chad1008 Jan 30, 2023
40d0215
Add `useAutocompleteProps` method typing
chad1008 Feb 2, 2023
9dcd60f
Add `Autocomplete` typing
chad1008 Feb 2, 2023
a34ff28
Supress declaration module warnings from `@wordpress/rich-text`
chad1008 Feb 2, 2023
23efc52
Update and type unit test
chad1008 Feb 3, 2023
3d4973b
update CHANGELOG
chad1008 Feb 3, 2023
90d358b
update README
chad1008 Feb 5, 2023
2b261ab
Clean up `AutocompleterUI`s `popoverRef` typing
chad1008 Feb 9, 2023
49b2cdc
Simplify check for matches in `AutocompleterUI`
chad1008 Feb 9, 2023
c8a2459
README whitespace cleanup
chad1008 Feb 9, 2023
cf072da
Improved optional `onKeyDownRef.current()` call
chad1008 Feb 9, 2023
f5261e4
Simplify/inline `getOptionCompletion` type declaration
chad1008 Feb 9, 2023
86e479f
inferred return type for `getDefualtUseItems`
chad1008 Feb 13, 2023
6fb3410
Rename `DebouncedPromise` to `CancelablePromise`
chad1008 Feb 13, 2023
b3d2a1f
replace a negated OR with AND comparison
chad1008 Feb 13, 2023
0764c6e
restore origional action/value destructuring
chad1008 Feb 13, 2023
feeee68
Clean up ContentRef typing to use immutable ref object
chad1008 Feb 13, 2023
fa51915
improve typing in unit test
chad1008 Feb 13, 2023
13012a3
Conditionally type `WPCompleter.options`
chad1008 Feb 13, 2023
a45c468
`switch to `React.Type` instead of importing from `react`
chad1008 Feb 13, 2023
7f7f686
remove outdated code comment
chad1008 Feb 13, 2023
aff2aa0
remove unnecessary JSDoc
chad1008 Feb 13, 2023
1dced4a
fix rich text `@see` tag
chad1008 Feb 13, 2023
391b873
fix typos
chad1008 Feb 15, 2023
8a20c8d
Max 80 char per line
chad1008 Feb 15, 2023
53efc8a
Revert "Conditionally type `WPCompleter.options`"
chad1008 Feb 16, 2023
ad9e424
Simplify `options` typing
chad1008 Feb 16, 2023
ef689a4
typo
chad1008 Feb 22, 2023
fa0da2f
update option completion types
chad1008 Feb 28, 2023
7348279
Type `Component` prop as `React.ElementType`
ciampo Mar 1, 2023
28402b8
Add more specific type/variable name to filteredOptions default value
ciampo Mar 1, 2023
349d9a2
Add missing CHANGELOG entry
ciampo Mar 1, 2023
0dc0b0f
Add back undefined checks for action/value completionObject
ciampo Mar 1, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
- `TabPanel`: Improve unit test in preparation for controlled component updates ([#48086](https://github.com/WordPress/gutenberg/pull/48086)).
- `Autocomplete`: performance: avoid setting state on every value change ([#48485](https://github.com/WordPress/gutenberg/pull/48485)).
- `Higher Order` -- `with-constrained-tabbing`: Convert to TypeScript ([#48162](https://github.com/WordPress/gutenberg/pull/48162)).
- `Autocomplete`: Convert to TypeScript ([#47751](https://github.com/WordPress/gutenberg/pull/47751)).
- `Autocomplete`: avoid calling setState on input ([#48565](https://github.com/WordPress/gutenberg/pull/48565)).

## 23.4.0 (2023-02-15)

Expand Down
51 changes: 51 additions & 0 deletions packages/components/src/autocomplete/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,57 @@

This component is used to provide autocompletion support for a child input component.

## Props

The following props are used to control the behavior of the component.

### record

The rich text value object the autocomleter is being applied to.

- Required: Yes
- Type: `RichTextValue`

### onChange

A function to be called when an option is selected to insert into the existing text.

- Required: Yes
- Type: `( value: string ) => void`

A function to be called when an option is selected to replace the existing text.

- Required: Yes
- Type: `( arg: [ OptionCompletion[ 'value' ] ] ) => void;`

### completers

An array of all of the completers to apply to the current element.

- Required: Yes
- Type: `Array< WPCompleter >`

### contentRef

A ref containing the editable element that will serve as the anchor for `Autocomplete`'s `Popover`.

- Required: Yes
- `MutableRefObject< HTMLElement | undefined >`

### children

A function that returns nodes to be rendered within the Autocomplete.

- Required: Yes
- Type: `Function`

### isSelected

Whether or not the Autocomplte componenet is selected, and if its `Popover` should be displayed.

- Required: Yes
- Type: `Boolean`

## Autocompleters

Autocompleters enable us to offer users options for completing text input. For example, Gutenberg includes a user autocompleter that provides a list of user names and completes a selection with a user mention like `@mary`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
useEffect,
useState,
} from '@wordpress/element';
// Error expected because `@wordpress/rich-text` is not yet fully typed.
// @ts-expect-error
import { useAnchor } from '@wordpress/rich-text';
import { useMergeRefs, useRefEffect } from '@wordpress/compose';

Expand All @@ -23,8 +25,9 @@ import Button from '../button';
import Popover from '../popover';
import { VisuallyHidden } from '../visually-hidden';
import { createPortal } from 'react-dom';
import type { AutocompleterUIProps, WPCompleter } from './types';

export function getAutoCompleterUI( autocompleter ) {
export function getAutoCompleterUI( autocompleter: WPCompleter ) {
const useItems = autocompleter.useItems
? autocompleter.useItems
: getDefaultUseItems( autocompleter );
Expand All @@ -41,15 +44,15 @@ export function getAutoCompleterUI( autocompleter ) {
reset,
value,
contentRef,
} ) {
}: AutocompleterUIProps ) {
const [ items ] = useItems( filterValue );
const popoverAnchor = useAnchor( {
editableContentElement: contentRef.current,
value,
} );

const [ needsA11yCompat, setNeedsA11yCompat ] = useState( false );
const popoverRef = useRef();
const popoverRef = useRef< HTMLElement >( null );
const popoverRefs = useMergeRefs( [
popoverRef,
useRefEffect(
Expand Down Expand Up @@ -77,11 +80,15 @@ export function getAutoCompleterUI( autocompleter ) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ items ] );

if ( ! items.length > 0 ) {
if ( items.length === 0 ) {
return null;
}

const ListBox = ( { Component = 'div' } ) => (
const ListBox = ( {
Component = 'div',
}: {
Component?: React.ElementType;
} ) => (
<Component
id={ listBoxId }
role="listbox"
Expand Down Expand Up @@ -134,11 +141,17 @@ export function getAutoCompleterUI( autocompleter ) {
return AutocompleterUI;
}

function useOnClickOutside( ref, handler ) {
function useOnClickOutside(
ref: React.RefObject< HTMLElement >,
handler: AutocompleterUIProps[ 'reset' ]
) {
useEffect( () => {
const listener = ( event ) => {
const listener = ( event: MouseEvent | TouchEvent ) => {
// Do nothing if clicking ref's element or descendent elements, or if the ref is not referencing an element
if ( ! ref.current || ref.current.contains( event.target ) ) {
if (
! ref.current ||
ref.current.contains( event.target as Node )
chad1008 marked this conversation as resolved.
Show resolved Hide resolved
) {
return;
}
handler( event );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@ import { useLayoutEffect, useState } from '@wordpress/element';
* Internal dependencies
*/
import { escapeRegExp } from '../utils/strings';
import type { CancelablePromise, KeyedOption, WPCompleter } from './types';

function filterOptions( search, options = [], maxResults = 10 ) {
function filterOptions(
search: RegExp,
options: Array< KeyedOption > = [],
maxResults = 10
) {
const filtered = [];
for ( let i = 0; i < options.length; i++ ) {
const option = options[ i ];
Expand Down Expand Up @@ -43,9 +48,9 @@ function filterOptions( search, options = [], maxResults = 10 ) {
return filtered;
}

export default function getDefaultUseItems( autocompleter ) {
return ( filterValue ) => {
const [ items, setItems ] = useState( [] );
export default function getDefaultUseItems( autocompleter: WPCompleter ) {
return ( filterValue: string ) => {
const [ items, setItems ] = useState< Array< KeyedOption > >( [] );
/*
* We support both synchronous and asynchronous retrieval of completer options
* but internally treat all as async so we maintain a single, consistent code path.
Expand All @@ -61,7 +66,7 @@ export default function getDefaultUseItems( autocompleter ) {
const { options, isDebounced } = autocompleter;
const loadOptions = debounce(
() => {
const promise = Promise.resolve(
const promise: CancelablePromise = Promise.resolve(
typeof options === 'function'
? options( filterValue )
: options
Expand Down Expand Up @@ -112,6 +117,6 @@ export default function getDefaultUseItems( autocompleter ) {
};
}, [ filterValue ] );

return [ items ];
return [ items ] as const;
};
}
Loading