I can think of 3 solutions, each with its advantages and complexity.
First, use hook:
// use-on-tab.js
const useOnTab = function (callback) {
let listener = function (event) {
if (event.code === 'Enter') {
callback()
}
}
return listener
}
export default useOnTab
// Foo.js
import useOnTab from './use-on-tab'
const Foo = function () {
let onTab = useOnTab(() => console.log('foo'))
return <div onKeyUp={onTab} tabIndex="0">Foo</div>
}
export default Foo
This solution is quite simple, you can hide the logic of checking key and only pass the callback. But you still have to manually call useOnTab and add onKeyUp prop on every components.
Second, use HOC:
// with-on-tab.js
const withOnTab = function (WrappedComponent) {
let createListener = function (callback) {
return function (event) {
if (event.code === 'Enter') {
callback()
}
}
}
return function ({ onTab, ...props }) {
return <WrappedComponent {...props} onKeyUp={createListener(onTab)} />
}
}
export default withOnTab
// Bar.js
import withOnTab from './with-on-tab'
const Bar = function (props) {
return <div tabIndex="0" {...props}>Bar</div>
}
export default withOnTab(Bar)
<Bar onTab={() => console.log('bar')} />
Read more about Higher-Order Components.
And the last one is custom JSX:
// jsx-dev-runtime.js
import * as ReactJSXRuntimeDev from 'react/jsx-dev-runtime'
export const Fragment = ReactJSXRuntimeDev.Fragment
export function jsxDEV(
type,
props,
key,
isStaticChildren,
source,
self
) {
let newProps = props
if (Object.prototype.hasOwnProperty.call(props, 'onTab')) {
let { onTab, ...props } = newProps
newProps = {
...props,
onKeyUp: event => {
if (event.code === 'Enter') {
onTab()
}
}
}
}
return ReactJSXRuntimeDev.jsxDEV(
type,
newProps,
key,
isStaticChildren,
source,
self
)
}
// Baz.js
const Baz = function (props) {
return <div tabIndex="0" {...props}>Baz</div>
}
export default Baz
/** @jsxImportSource custom-jsx-library */
<Baz onTab={() => console.log('baz')} />
I get this idea from @emotion/react which has to deal with the css prop. Github