-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
470 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
export * from "./loadingView"; | ||
export * from "./scrollView"; | ||
export * from "./text"; | ||
export * from "./textInput"; |
30 changes: 30 additions & 0 deletions
30
packages/component/src/molecules/textInput/TextInputState/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
/** | ||
* Copyright (c) Nicolas Gallagher. Copyright (c) Facebook, Inc. and its affiliates. | ||
* | ||
* This source code is licensed under the MIT license found in the LICENSE file | ||
* in the root directory of this source tree. | ||
* | ||
* @flow | ||
*/ | ||
|
||
/** | ||
* This class is responsible for coordinating the "focused" state for | ||
* TextInputs. All calls relating to the keyboard should be funneled through here | ||
*/ | ||
const TextInputState = { | ||
/** Internal state */ | ||
_currentlyFocusedNode: null as null | Object, | ||
|
||
/** | ||
* Returns the ID of the currently focused text field, if one exists If no | ||
* text field is focused it returns null | ||
*/ | ||
currentlyFocusedField(): null | Object { | ||
if (document.activeElement !== this._currentlyFocusedNode) { | ||
this._currentlyFocusedNode = null; | ||
} | ||
return this._currentlyFocusedNode; | ||
}, | ||
}; | ||
|
||
export default TextInputState; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,330 @@ | ||
/* eslint-disable @typescript-eslint/naming-convention */ | ||
import classNames from "classnames"; | ||
import jss from "jss"; | ||
import * as React from "react"; | ||
import { composeRef, useElementLayout } from "reactjs-view-core"; | ||
import { flattenStyle } from "../../atoms"; | ||
import TextInputState from "./TextInputState"; | ||
import { TextInputProps } from "./types"; | ||
|
||
/** Determines whether a 'selection' prop differs from a node's existing selection state. */ | ||
const isSelectionStale = ( | ||
node: { selectionEnd: any; selectionStart: any }, | ||
selection: { start: any; end?: any }, | ||
) => { | ||
const { selectionEnd, selectionStart } = node; | ||
const { start, end } = selection; | ||
return start !== selectionStart || end !== selectionEnd; | ||
}; | ||
|
||
/** Certain input types do no support 'selectSelectionRange' and will throw an error. */ | ||
const setSelection = (node: any, selection: { start: any; end?: any }) => { | ||
if (isSelectionStale(node, selection)) { | ||
const { start, end } = selection; | ||
try { | ||
node.setSelectionRange(start, end || start); | ||
} catch (e) {} | ||
} | ||
}; | ||
|
||
// If an Input Method Editor is processing key input, the 'keyCode' is 229. | ||
// https://www.w3.org/TR/uievents/#determine-keydown-keyup-keyCode | ||
function isEventComposing(nativeEvent: any) { | ||
return nativeEvent.isComposing || nativeEvent.keyCode === 229; | ||
} | ||
|
||
const TextInput = React.forwardRef<HTMLElement, TextInputProps>( | ||
( | ||
{ | ||
autoCapitalize = "sentences", | ||
autoComplete, | ||
autoCompleteType, | ||
autoCorrect = true, | ||
blurOnSubmit, | ||
clearTextOnFocus, | ||
dir, | ||
editable = true, | ||
keyboardType = "default", | ||
multiline = false, | ||
numberOfLines = 1, | ||
onBlur, | ||
onChange, | ||
onChangeText, | ||
onContentSizeChange, | ||
onFocus, | ||
onKeyPress, | ||
onLayout, | ||
onSelectionChange, | ||
onSubmitEditing, | ||
placeholderTextColor, | ||
returnKeyType, | ||
secureTextEntry = false, | ||
selection, | ||
selectTextOnFocus, | ||
spellCheck, | ||
className, | ||
style: styleProps, | ||
...rest | ||
}, | ||
forwardedRef, | ||
) => { | ||
let type: React.InputHTMLAttributes<HTMLInputElement>["type"]; | ||
let inputMode: React.HTMLAttributes<HTMLInputElement>["inputMode"]; | ||
|
||
switch (keyboardType) { | ||
case "email-address": | ||
type = "email"; | ||
break; | ||
case "number-pad": | ||
case "numeric": | ||
inputMode = "numeric"; | ||
break; | ||
case "decimal-pad": | ||
inputMode = "decimal"; | ||
break; | ||
case "phone-pad": | ||
type = "tel"; | ||
break; | ||
case "search": | ||
case "web-search": | ||
type = "search"; | ||
break; | ||
case "url": | ||
type = "url"; | ||
break; | ||
default: | ||
type = "text"; | ||
} | ||
|
||
if (secureTextEntry) { | ||
type = "password"; | ||
} | ||
|
||
const dimensions = React.useRef({ height: null, width: null }); | ||
const hostRef = React.useRef(null); | ||
|
||
const handleContentSizeChange = React.useCallback( | ||
(hostNode) => { | ||
if (multiline && onContentSizeChange && hostNode != null) { | ||
const newHeight = hostNode.scrollHeight; | ||
const newWidth = hostNode.scrollWidth; | ||
if ( | ||
newHeight !== dimensions.current.height || | ||
newWidth !== dimensions.current.width | ||
) { | ||
dimensions.current.height = newHeight; | ||
dimensions.current.width = newWidth; | ||
onContentSizeChange({ | ||
nativeEvent: { | ||
contentSize: { | ||
height: dimensions.current.height, | ||
width: dimensions.current.width, | ||
}, | ||
}, | ||
}); | ||
} | ||
} | ||
}, | ||
[multiline, onContentSizeChange], | ||
); | ||
|
||
const imperativeRef = React.useMemo( | ||
() => (hostNode: any) => { | ||
// TextInput needs to add more methods to the hostNode in addition to those | ||
// added by `usePlatformMethods`. This is temporarily until an API like | ||
// `TextInput.clear(hostRef)` is added to React Native. | ||
if (hostNode != null) { | ||
hostNode.clear = function () { | ||
if (hostNode != null) { | ||
hostNode.value = ""; | ||
} | ||
}; | ||
hostNode.isFocused = function () { | ||
return ( | ||
hostNode != null && | ||
TextInputState.currentlyFocusedField() === hostNode | ||
); | ||
}; | ||
handleContentSizeChange(hostNode); | ||
} | ||
}, | ||
[handleContentSizeChange], | ||
); | ||
|
||
function handleBlur(e: React.FocusEvent<HTMLInputElement>) { | ||
TextInputState._currentlyFocusedNode = null; | ||
if (onBlur) { | ||
//@ts-ignore | ||
e.nativeEvent.text = e.target.value; | ||
onBlur(e); | ||
} | ||
} | ||
|
||
function handleChange( | ||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, | ||
) { | ||
const hostNode = e.target; | ||
const text = hostNode.value; | ||
//@ts-ignore | ||
e.nativeEvent.text = text; | ||
handleContentSizeChange(hostNode); | ||
if (onChange) { | ||
onChange(e); | ||
} | ||
if (onChangeText) { | ||
onChangeText(text); | ||
} | ||
} | ||
|
||
function handleFocus(e: React.FocusEvent<HTMLInputElement>) { | ||
const hostNode = e.target; | ||
if (onFocus) { | ||
//@ts-ignore | ||
e.nativeEvent.text = hostNode.value; | ||
onFocus(e); | ||
} | ||
if (hostNode != null) { | ||
TextInputState._currentlyFocusedNode = hostNode; | ||
if (clearTextOnFocus) { | ||
hostNode.value = ""; | ||
} | ||
if (selectTextOnFocus) { | ||
// Safari requires selection to occur in a setTimeout | ||
setTimeout(() => { | ||
hostNode.select(); | ||
}, 0); | ||
} | ||
} | ||
} | ||
|
||
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) { | ||
const hostNode = e.target; | ||
// Prevent key events bubbling (see #612) | ||
e.stopPropagation(); | ||
|
||
const blurOnSubmitDefault = !multiline; | ||
const shouldBlurOnSubmit = | ||
blurOnSubmit == null ? blurOnSubmitDefault : blurOnSubmit; | ||
|
||
const nativeEvent = e.nativeEvent; | ||
const isComposing = isEventComposing(nativeEvent); | ||
|
||
if (onKeyPress) { | ||
onKeyPress(e); | ||
} | ||
|
||
if ( | ||
e.key === "Enter" && | ||
!e.shiftKey && | ||
// Do not call submit if composition is occuring. | ||
!isComposing && | ||
!e.isDefaultPrevented() | ||
) { | ||
if ((blurOnSubmit || !multiline) && onSubmitEditing) { | ||
// prevent "Enter" from inserting a newline or submitting a form | ||
e.preventDefault(); | ||
//@ts-ignore | ||
nativeEvent.text = e.target.value; | ||
onSubmitEditing(e); | ||
} | ||
if (shouldBlurOnSubmit && hostNode != null) { | ||
//@ts-ignore | ||
hostNode.blur(); | ||
} | ||
} | ||
} | ||
|
||
const handleSelectionChange: React.DOMAttributes<HTMLInputElement>["onSelect"] = | ||
(e) => { | ||
if (onSelectionChange) { | ||
try { | ||
const node = e.target; | ||
const { selectionStart, selectionEnd } = node as any; | ||
//@ts-ignore | ||
e.nativeEvent.selection = { | ||
start: selectionStart, | ||
end: selectionEnd, | ||
}; | ||
//@ts-ignore | ||
e.nativeEvent.text = e.target.value; | ||
onSelectionChange(e); | ||
} catch (e) {} | ||
} | ||
}; | ||
|
||
React.useLayoutEffect(() => { | ||
const node = hostRef.current; | ||
if (node && selection) { | ||
setSelection(node, selection); | ||
} | ||
if (document.activeElement === node) { | ||
TextInputState._currentlyFocusedNode = node; | ||
} | ||
}, [hostRef, selection]); | ||
|
||
const style = flattenStyle([ | ||
styleProps, | ||
//@ts-ignore | ||
!!placeholderTextColor && { placeholderTextColor }, | ||
]); | ||
|
||
useElementLayout(hostRef, onLayout); | ||
|
||
const supportedProps: | ||
| React.TextareaHTMLAttributes<HTMLTextAreaElement> | ||
| React.InputHTMLAttributes<HTMLInputElement> = rest; | ||
|
||
supportedProps.autoCapitalize = autoCapitalize; | ||
supportedProps.autoComplete = autoComplete || autoCompleteType || "on"; | ||
supportedProps.autoCorrect = autoCorrect ? "on" : "off"; | ||
supportedProps.className = classNames(classes.textinput, className); | ||
// 'auto' by default allows browsers to infer writing direction | ||
supportedProps.dir = dir !== undefined ? dir : "auto"; | ||
supportedProps.enterKeyHint = returnKeyType; | ||
supportedProps.onBlur = handleBlur; | ||
supportedProps.onChange = handleChange; | ||
supportedProps.onFocus = handleFocus; | ||
supportedProps.onKeyDown = handleKeyDown; | ||
supportedProps.onSelect = handleSelectionChange; | ||
supportedProps.readOnly = !editable; | ||
// @ts-ignore | ||
supportedProps.rows = multiline ? numberOfLines : undefined; | ||
supportedProps.spellCheck = spellCheck != null ? spellCheck : autoCorrect; | ||
supportedProps.style = style; | ||
supportedProps.type = multiline ? undefined : type; | ||
supportedProps.inputMode = inputMode; | ||
|
||
const setRef = composeRef(hostRef, imperativeRef, forwardedRef); | ||
|
||
return multiline ? ( | ||
<textarea | ||
ref={setRef} | ||
{...(supportedProps as React.TextareaHTMLAttributes<HTMLTextAreaElement>)} | ||
/> | ||
) : ( | ||
<input ref={setRef} {...supportedProps} /> | ||
); | ||
}, | ||
); | ||
|
||
TextInput.displayName = "TextInput"; | ||
|
||
const classes = jss | ||
.createStyleSheet({ | ||
textinput: { | ||
MozAppearance: "textfield", | ||
WebkitAppearance: "none", | ||
backgroundColor: "transparent", | ||
border: "0 solid black", | ||
borderRadius: 0, | ||
boxSizing: "border-box", | ||
font: "14px System", | ||
margin: 0, | ||
padding: 0, | ||
resize: "none", | ||
}, | ||
}) | ||
.attach().classes; | ||
|
||
export type { TextInputProps }; | ||
export { TextInput }; |
Oops, something went wrong.