Skip to content

Commit

Permalink
Autocomplete: refactor to TypeScript (#47751)
Browse files Browse the repository at this point in the history
* Remove from exclude list

* Rename and add files

* Add `WPCompleter` typing

* Add `AutocompleterUI` typing

* Add `getDeafaultUseItems` typing

* Add `useAutocomplete` typing

* Add `useAutocompleteProps` method typing

* Add `Autocomplete` typing

* Supress declaration module warnings from `@wordpress/rich-text`

* Update and type unit test

* update CHANGELOG

* update README

* Clean up `AutocompleterUI`s `popoverRef` typing

* Simplify check for matches in `AutocompleterUI`

* README whitespace cleanup

* Improved optional `onKeyDownRef.current()` call

* Simplify/inline `getOptionCompletion` type declaration

* inferred return type for `getDefualtUseItems`

* Rename `DebouncedPromise` to `CancelablePromise`

* replace a negated OR with AND comparison

* restore origional action/value destructuring

* Clean up ContentRef typing to use immutable ref object

* improve typing in unit test

* Conditionally type `WPCompleter.options`

* `switch to `React.Type` instead of importing from `react`

* remove outdated code comment

* remove unnecessary JSDoc

* fix rich text `@see` tag

* fix typos

* Max 80 char per line

* Revert "Conditionally type `WPCompleter.options`"

This reverts commit 4bb27ee.

* Simplify `options` typing

* typo

* update option completion types

* Type `Component` prop as `React.ElementType`

* Add more specific type/variable name to filteredOptions default value

* Add missing CHANGELOG entry

* Add back undefined checks for action/value completionObject

---------

Co-authored-by: Marco Ciampini <marco.ciampo@gmail.com>
  • Loading branch information
chad1008 and ciampo authored Mar 1, 2023
1 parent 0895abd commit 4c40823
Show file tree
Hide file tree
Showing 8 changed files with 417 additions and 146 deletions.
2 changes: 2 additions & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,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 )
) {
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

0 comments on commit 4c40823

Please sign in to comment.