Expose DOM methods via a forwarded ref

This commit is contained in:
Tom Southall
2022-04-06 22:27:33 +01:00
parent c1d53e1d2b
commit 2b1bd141fa
7 changed files with 114 additions and 21 deletions

View File

@@ -1,5 +1,8 @@
# Changelog
### 1.4.0 (6 Apr 2022)
- Expose DOM methods via a forwarded ref
### 1.3.0 (2 Apr 2022)
- Extend listbox and defaultListbox prop types to allow functions returning a promise resolving to an array of group settings

View File

@@ -11,6 +11,7 @@ Turnstone is a highly customisable, easy-to-use autocomplete search component fo
- [API](#api)
- [Props](#props)
- [Component Props](#component-props)
- [Methods](#methods)
- [Contributing](#contributing)
- [Release Checklist](#release-checklist)
- [License](#license)
@@ -624,6 +625,62 @@ The following custom components can also be supplied as props:
- **`id`** (string) The `id` of the group supplied in the `listbox` or `defaultListbox` prop.
- **`index`** (number) The index of the group. Matches the order supplied in the `listbox` or `defaultListbox` prop. Zero-indexed.
### Methods
There are a number of methods accessible via a ref supplied to the Turnstone component.
For example:
```jsx
import React, { useRef } from 'react'
import Turnstone from 'turnstone'
import data from './data'
const App = () => {
const listbox = { data }
const turnstoneRef = useRef()
const handleQuery = () => {
turnstoneRef.current?.query('new')
}
const handleClear = () => {
turnstoneRef.current?.clear()
}
return (
<>
<Turnstone ref={turnstoneRef} listbox={listbox} />
<button onClick={handleQuery}>Perform Query</button>
<button onClick={handleClear}>Clear Contents</button>
</>
)
}
```
The methods are as follows:
#### `blur()`
Removes keyboard focus from the search box.
#### `clear()`
Clears the contents of the search box
#### `focus()`
Sets keyboard focus on the search box.
#### `query(<string>)`
Sets the search box contents to the string argument supplied to the function.
#### `select()`
Selects the contents of the search box
## Contributing
- Fork the project
- Run the project in development mode: `$ npm run dev`

View File

@@ -1,6 +1,6 @@
/* eslint no-console: 0 */
import React, { useCallback, useState } from 'react'
import React, { useCallback, useState, useRef } from 'react'
import Turnstone from '../../src/lib'
import styles from './styles/App.module.css'
import autocompleteStyles from './styles/autocomplete.module.css'
@@ -71,6 +71,7 @@ const listbox = [
// }
const App = () => {
const ref = useRef()
const [selectedItem, setSelectedItem] = useState(undef)
const onSelect = useCallback(
@@ -98,6 +99,7 @@ const App = () => {
<div>
<label htmlFor="autocomplete">Search:</label>&nbsp;
<Turnstone
ref={ref}
autoFocus={false}
cancelButton={true}
clearButton={true}

18
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "turnstone",
"version": "1.2.3",
"version": "1.3.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "turnstone",
"version": "1.2.3",
"version": "1.3.0",
"license": "MIT",
"dependencies": {
"escape-string-regexp": "^5.0.0",
@@ -16,7 +16,7 @@
"setify": "^1.0.4",
"split-match": "^0.2.2",
"swr": "^1.1.2",
"turnstone-recent-searches": "^0.4.0",
"turnstone-recent-searches": "^0.5.0",
"use-debounce": "^7.0.1"
},
"devDependencies": {
@@ -5008,9 +5008,9 @@
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
},
"node_modules/turnstone-recent-searches": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/turnstone-recent-searches/-/turnstone-recent-searches-0.4.0.tgz",
"integrity": "sha512-3/ejeRHv+awlU2mI6l3dFXmbvzCsi0KgEmbJ7AD/X50d0Eu7VbJXnoH5RsMU4rFuO6swPGMhsuTx5Dl48MyBLw=="
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/turnstone-recent-searches/-/turnstone-recent-searches-0.5.0.tgz",
"integrity": "sha512-OFGRM92OKAx467QTYeJXJY5Uj1XhPiMmqiXwL1/wPky38SLXotyxVjFpMvCb8MuGsY61fsJDkF2vsmseae42VA=="
},
"node_modules/type-check": {
"version": "0.4.0",
@@ -9061,9 +9061,9 @@
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
},
"turnstone-recent-searches": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/turnstone-recent-searches/-/turnstone-recent-searches-0.4.0.tgz",
"integrity": "sha512-3/ejeRHv+awlU2mI6l3dFXmbvzCsi0KgEmbJ7AD/X50d0Eu7VbJXnoH5RsMU4rFuO6swPGMhsuTx5Dl48MyBLw=="
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/turnstone-recent-searches/-/turnstone-recent-searches-0.5.0.tgz",
"integrity": "sha512-OFGRM92OKAx467QTYeJXJY5Uj1XhPiMmqiXwL1/wPky38SLXotyxVjFpMvCb8MuGsY61fsJDkF2vsmseae42VA=="
},
"type-check": {
"version": "0.4.0",

View File

@@ -1,6 +1,6 @@
{
"name": "turnstone",
"version": "1.3.0",
"version": "1.4.0",
"description": "React customisable autocomplete component with typeahead and grouped results from multiple APIs.",
"author": "Tom Southall",
"keywords": [
@@ -52,7 +52,7 @@
"setify": "^1.0.4",
"split-match": "^0.2.2",
"swr": "^1.1.2",
"turnstone-recent-searches": "^0.4.0",
"turnstone-recent-searches": "^0.5.0",
"use-debounce": "^7.0.1"
},
"devDependencies": {

View File

@@ -1,4 +1,4 @@
import React, { useState, useRef, useContext } from 'react'
import React, { useState, useRef, useContext, useImperativeHandle } from 'react'
import { StateContext } from '../context/state'
import Listbox from './listbox'
import Errorbox from './errorbox'
@@ -23,7 +23,7 @@ import {
clear
} from '../actions/actions'
export default function Container(props) {
const Container = React.forwardRef((props, ref) => {
// Destructure props
const {
autoFocus,
@@ -202,6 +202,29 @@ export default function Container(props) {
setBlockBlurHandler(false)
}
// Expose methods to parent components
useImperativeHandle(ref, () => ({
focus: () => {
queryInput.current.focus()
},
blur: () => {
queryInput.current.blur()
},
select: () => {
queryInput.current.select()
},
clear: () => {
clearState()
},
query: (query) => {
if(typeof query === 'string') {
queryInput.current.value = query
queryInput.current.focus()
handleInput()
}
}
}))
return (
<React.Fragment>
<div
@@ -288,4 +311,8 @@ export default function Container(props) {
</div>
</React.Fragment>
)
}
})
Container.displayName = 'Container'
export default Container

View File

@@ -28,12 +28,12 @@ const propDefaults = {
Clear: () => '\u00d7'
}
const render = (Component, componentProps, pluginIndex) => {
const render = (Component, componentProps, pluginIndex, ref) => {
const p = Array.isArray(componentProps.plugins) && componentProps.plugins[pluginIndex]
if(p) {
const [Plugin, pluginProps] = Array.isArray(p) ? p : [p]
return <Plugin {...{
return <Plugin ref={ref} {...{
...pluginProps,
Component,
componentProps,
@@ -41,20 +41,24 @@ const render = (Component, componentProps, pluginIndex) => {
render
}} />
}
return <Component {...componentProps} />
return <Component ref={ref} {...componentProps} />
}
export default function Turnstone(props) {
const Turnstone = React.forwardRef((props, ref) => {
const componentProps = {...propDefaults, ...props}
return (
<React.StrictMode>
<StateContextProvider {...componentProps}>
{ render(Container, componentProps, 0) }
{ render(Container, componentProps, 0, ref) }
</StateContextProvider>
</React.StrictMode>
)
}
})
Turnstone.displayName = 'Turnstone'
export default Turnstone
//////////////////////////////////////////////////////
// Prop validation //