@acusti/input-text
Advanced tools
Comparing version 1.6.2 to 1.7.0
import * as React from 'react'; | ||
import type { InputHTMLAttributes } from 'react'; | ||
type InputElement = HTMLInputElement | HTMLTextAreaElement; | ||
export type InputElement = HTMLInputElement | HTMLTextAreaElement; | ||
export type Props = { | ||
@@ -60,5 +60,3 @@ autoCapitalize?: 'none' | 'off' | 'sentences' | 'words' | 'characters'; | ||
}; | ||
declare const _default: React.ForwardRefExoticComponent< | ||
Props & React.RefAttributes<HTMLInputElement> | ||
>; | ||
declare const _default: React.ForwardRefExoticComponent<Props & React.RefAttributes<HTMLInputElement>>; | ||
export default _default; |
import * as React from 'react'; | ||
const { useCallback, useEffect, useImperativeHandle, useRef, useState } = React; | ||
export default React.forwardRef(function InputText( | ||
{ | ||
autoCapitalize, | ||
autoComplete, | ||
autoFocus, | ||
className, | ||
disabled, | ||
doubleClickToEdit, | ||
enterKeyHint, | ||
form, | ||
initialValue, | ||
list, | ||
max, | ||
maxHeight = Infinity, | ||
maxLength, | ||
min, | ||
minLength, | ||
multiLine, | ||
multiple, | ||
name, | ||
onBlur, | ||
onChange, | ||
onFocus, | ||
onKeyDown, | ||
onKeyUp, | ||
pattern, | ||
placeholder, | ||
readOnly, | ||
required, | ||
selectTextOnFocus, | ||
size, | ||
style, | ||
step, | ||
submitOnEnter, | ||
tabIndex, | ||
title, | ||
type = 'text', | ||
}, | ||
ref, | ||
) { | ||
export default React.forwardRef(function InputText({ autoCapitalize, autoComplete, autoFocus, className, disabled, doubleClickToEdit, enterKeyHint, form, initialValue, list, max, maxHeight = Infinity, maxLength, min, minLength, multiLine, multiple, name, onBlur, onChange, onFocus, onKeyDown, onKeyUp, pattern, placeholder, readOnly, required, selectTextOnFocus, size, style, step, submitOnEnter, tabIndex, title, type = 'text', }, ref) { | ||
const inputRef = useRef(null); | ||
@@ -53,3 +14,4 @@ useImperativeHandle(ref, () => inputRef.current); | ||
inputRef.current.setAttribute('autofocus', 'autofocus'); | ||
} else if (!autoFocus && inputRef.current.autofocus) { | ||
} | ||
else if (!autoFocus && inputRef.current.autofocus) { | ||
inputRef.current.removeAttribute('autofocus'); | ||
@@ -60,24 +22,22 @@ } | ||
useEffect(() => { | ||
if (!inputRef.current) return; | ||
inputRef.current.value = | ||
initialValue !== null && initialValue !== void 0 ? initialValue : ''; | ||
if (!inputRef.current) | ||
return; | ||
inputRef.current.value = initialValue !== null && initialValue !== void 0 ? initialValue : ''; | ||
}, [initialValue]); | ||
const [readOnlyState, setReadOnlyState] = useState( | ||
readOnly !== null && readOnly !== void 0 ? readOnly : doubleClickToEdit, | ||
); | ||
const [readOnlyState, setReadOnlyState] = useState(readOnly !== null && readOnly !== void 0 ? readOnly : doubleClickToEdit); | ||
const isInitialSelectionRef = useRef(true); | ||
const startEditing = useCallback(() => { | ||
if (!doubleClickToEdit) return; | ||
if (!doubleClickToEdit) | ||
return; | ||
setReadOnlyState(false); | ||
}, [doubleClickToEdit]); | ||
const setInputHeight = useCallback(() => { | ||
if (!inputElement) return; | ||
if (!inputElement) | ||
return; | ||
if (inputElement.style.height) { | ||
inputElement.style.height = ''; | ||
} | ||
if (!multiLine) return; | ||
const height = Math.min( | ||
inputElement.scrollHeight, | ||
typeof maxHeight === 'string' ? parseFloat(maxHeight) : maxHeight, | ||
); | ||
if (!multiLine) | ||
return; | ||
const height = Math.min(inputElement.scrollHeight, typeof maxHeight === 'string' ? parseFloat(maxHeight) : maxHeight); | ||
if (height) { | ||
@@ -89,118 +49,79 @@ inputElement.style.height = `${height}px`; | ||
useEffect(setInputHeight, [setInputHeight]); | ||
const handleFocus = useCallback( | ||
(event) => { | ||
if (onFocus) onFocus(event); | ||
if (multiLine) setInputHeight(); | ||
}, | ||
[multiLine, onFocus, setInputHeight], | ||
); | ||
const handleBlur = useCallback( | ||
(event) => { | ||
if (onBlur) onBlur(event); | ||
if (doubleClickToEdit) { | ||
setReadOnlyState(true); | ||
} | ||
if (!selectTextOnFocus) return; | ||
setInputElement(event.currentTarget); | ||
// When input loses focus, reset isInitialSelection to true for next onSelect event | ||
isInitialSelectionRef.current = true; | ||
}, | ||
[doubleClickToEdit, onBlur, selectTextOnFocus, setInputElement], | ||
); | ||
const handleFocus = useCallback((event) => { | ||
if (onFocus) | ||
onFocus(event); | ||
if (multiLine) | ||
setInputHeight(); | ||
}, [multiLine, onFocus, setInputHeight]); | ||
const handleBlur = useCallback((event) => { | ||
if (onBlur) | ||
onBlur(event); | ||
if (doubleClickToEdit) { | ||
setReadOnlyState(true); | ||
} | ||
if (!selectTextOnFocus) | ||
return; | ||
setInputElement(event.currentTarget); | ||
// When input loses focus, reset isInitialSelection to true for next onSelect event | ||
isInitialSelectionRef.current = true; | ||
}, [doubleClickToEdit, onBlur, selectTextOnFocus, setInputElement]); | ||
// NOTE Selecting the contents of the input onFocus makes for the best UX, | ||
// but it doesn’t work in Safari, so we use the initial onSelect event instead | ||
const handleSelect = useCallback( | ||
(event) => { | ||
if (!selectTextOnFocus) return; | ||
const input = event.currentTarget; | ||
setInputElement(input); | ||
// Do nothing if this isn’t the initial select-on-focus event | ||
if (!isInitialSelectionRef.current) return; | ||
// This is the initial select-on-focus event, so reset isInitialSelection to false | ||
isInitialSelectionRef.current = false; | ||
// Do nothing if input has no value | ||
if (!input.value) return; | ||
// Do nothing if input is no longer the document’s activeElement | ||
if (input.ownerDocument.activeElement !== input) return; | ||
// Do nothing if input’s contents are already selected | ||
const valueLength = input.value.length; | ||
if (input.selectionStart === 0 && input.selectionEnd === valueLength) return; | ||
input.selectionStart = 0; | ||
input.selectionEnd = valueLength; | ||
}, | ||
[selectTextOnFocus, setInputElement], | ||
); | ||
const handleKeyDown = useCallback( | ||
(event) => { | ||
if (onKeyDown) onKeyDown(event); | ||
if ( | ||
multiLine && | ||
submitOnEnter && | ||
event.key === 'Enter' && | ||
!event.shiftKey && | ||
!event.altKey && | ||
!event.ctrlKey | ||
) { | ||
event.preventDefault(); | ||
const form = event.currentTarget.closest('form'); | ||
if (form) { | ||
form.requestSubmit(); | ||
} else { | ||
// if no form to submit, trigger input blur | ||
event.currentTarget.blur(); | ||
const handleSelect = useCallback((event) => { | ||
if (!selectTextOnFocus) | ||
return; | ||
const input = event.currentTarget; | ||
setInputElement(input); | ||
// Do nothing if this isn’t the initial select-on-focus event | ||
if (!isInitialSelectionRef.current) | ||
return; | ||
// This is the initial select-on-focus event, so reset isInitialSelection to false | ||
isInitialSelectionRef.current = false; | ||
// Do nothing if input has no value | ||
if (!input.value) | ||
return; | ||
// Do nothing if input is no longer the document’s activeElement | ||
if (input.ownerDocument.activeElement !== input) | ||
return; | ||
// Do nothing if input’s contents are already selected | ||
const valueLength = input.value.length; | ||
if (input.selectionStart === 0 && input.selectionEnd === valueLength) | ||
return; | ||
input.selectionStart = 0; | ||
input.selectionEnd = valueLength; | ||
}, [selectTextOnFocus, setInputElement]); | ||
const handleKeyDown = useCallback((event) => { | ||
if (onKeyDown) | ||
onKeyDown(event); | ||
if (multiLine && | ||
submitOnEnter && | ||
event.key === 'Enter' && | ||
!event.shiftKey && | ||
!event.altKey && | ||
!event.ctrlKey) { | ||
event.preventDefault(); | ||
const form = event.currentTarget.closest('form'); | ||
if (form) { | ||
form.requestSubmit(); | ||
} | ||
else { | ||
// if no form to submit, trigger input blur | ||
event.currentTarget.blur(); | ||
} | ||
} | ||
else if (doubleClickToEdit && inputRef.current) { | ||
if (readOnlyState) { | ||
if (event.key === 'Enter') { | ||
setReadOnlyState(false); | ||
} | ||
} else if (doubleClickToEdit && inputRef.current) { | ||
if (readOnlyState) { | ||
if (event.key === 'Enter') { | ||
setReadOnlyState(false); | ||
} | ||
} else if (event.key === 'Enter' || event.key === 'Escape') { | ||
inputRef.current.blur(); | ||
} | ||
} | ||
}, | ||
[doubleClickToEdit, multiLine, onKeyDown, readOnlyState, submitOnEnter], | ||
); | ||
else if (event.key === 'Enter' || event.key === 'Escape') { | ||
inputRef.current.blur(); | ||
} | ||
} | ||
}, [doubleClickToEdit, multiLine, onKeyDown, readOnlyState, submitOnEnter]); | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment | ||
const Element = multiLine ? 'textarea' : 'input'; | ||
return React.createElement( | ||
Element, | ||
Object.assign( | ||
{ | ||
autoCapitalize: autoCapitalize, | ||
autoComplete: autoComplete, | ||
autoFocus: autoFocus, | ||
className: className, | ||
defaultValue: | ||
initialValue !== null && initialValue !== void 0 ? initialValue : '', | ||
disabled: disabled, | ||
enterKeyHint: enterKeyHint, | ||
form: form, | ||
list: list, | ||
maxLength: maxLength, | ||
minLength: minLength, | ||
multiple: multiple, | ||
name: name, | ||
onBlur: handleBlur, | ||
onChange: onChange, | ||
onDoubleClick: startEditing, | ||
onFocus: handleFocus, | ||
onKeyDown: handleKeyDown, | ||
onKeyUp: onKeyUp, | ||
onSelect: handleSelect, | ||
pattern: pattern, | ||
placeholder: placeholder, | ||
readOnly: readOnlyState, | ||
ref: setInputElement, | ||
required: required, | ||
size: size, | ||
style: style, | ||
tabIndex: tabIndex, | ||
title: title, | ||
type: type, | ||
}, | ||
multiLine ? { onInput: setInputHeight, rows: 1 } : { max, min, step }, | ||
), | ||
); | ||
const Element = (multiLine ? 'textarea' : 'input'); | ||
return (React.createElement(Element, Object.assign({ autoCapitalize: autoCapitalize, autoComplete: autoComplete, autoFocus: autoFocus, className: className, defaultValue: initialValue !== null && initialValue !== void 0 ? initialValue : '', disabled: disabled, enterKeyHint: enterKeyHint, form: form, list: list, maxLength: maxLength, minLength: minLength, multiple: multiple, name: name, onBlur: handleBlur, onChange: onChange, onDoubleClick: startEditing, onFocus: handleFocus, onKeyDown: handleKeyDown, onKeyUp: onKeyUp, onSelect: handleSelect, pattern: pattern, placeholder: placeholder, readOnly: readOnlyState, ref: setInputElement, required: required, size: size, style: style, tabIndex: tabIndex, title: title, type: type }, (multiLine ? { onInput: setInputHeight, rows: 1 } : { max, min, step })))); | ||
}); | ||
//# sourceMappingURL=InputText.js.map | ||
//# sourceMappingURL=InputText.js.map |
@@ -10,3 +10,3 @@ // @vitest-environment happy-dom | ||
it('renders a text input with the given props.initialValue', () => { | ||
render(React.createElement(InputText, { initialValue: 'foo Bar' })); | ||
render(React.createElement(InputText, { initialValue: "foo Bar" })); | ||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion | ||
@@ -18,5 +18,3 @@ const input = screen.getByRole('textbox'); | ||
const user = userEvent.setup(); | ||
const { rerender } = render( | ||
React.createElement(InputText, { initialValue: 'foo Bar' }), | ||
); | ||
const { rerender } = render(React.createElement(InputText, { initialValue: "foo Bar" })); | ||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion | ||
@@ -28,6 +26,6 @@ const input = screen.getByRole('textbox'); | ||
// re-render with same initialValue, value state shouldn’t change | ||
rerender(React.createElement(InputText, { initialValue: 'foo Bar' })); | ||
rerender(React.createElement(InputText, { initialValue: "foo Bar" })); | ||
expect(input.value).toBe('foo bar'); | ||
// re-render with different initialValue, value state should reset | ||
rerender(React.createElement(InputText, { initialValue: 'foo Bar ' })); | ||
rerender(React.createElement(InputText, { initialValue: "foo Bar " })); | ||
expect(input.value).toBe('foo Bar '); | ||
@@ -37,7 +35,4 @@ }); | ||
const user = userEvent.setup(); | ||
const longText = | ||
'Lorem ipsum dolor sit amet. Consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi'; | ||
render( | ||
React.createElement(InputText, { initialValue: longText, multiLine: true }), | ||
); | ||
const longText = 'Lorem ipsum dolor sit amet. Consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi'; | ||
render(React.createElement(InputText, { initialValue: longText, multiLine: true })); | ||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion | ||
@@ -50,2 +45,2 @@ const textarea = screen.getByRole('textbox'); | ||
}); | ||
//# sourceMappingURL=InputText.test.js.map | ||
//# sourceMappingURL=InputText.test.js.map |
{ | ||
"name": "@acusti/input-text", | ||
"version": "1.6.2", | ||
"version": "1.7.0", | ||
"type": "module", | ||
@@ -5,0 +5,0 @@ "sideEffects": false, |
@@ -98,1 +98,3 @@ # @acusti/input-text | ||
``` | ||
Note: the `InputElement` type referenced in the event handlers above is a union of `HTMLInputElement` and `HTMLTextAreaElement` and is available as an export (`import type { InputElement } from '@acusti/input-text';`). |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
100
38678
525