Skip to content

Commit

Permalink
feat: add TextInput
Browse files Browse the repository at this point in the history
  • Loading branch information
hosseinmd committed Nov 23, 2021
1 parent a1a4c5b commit 49fddf6
Show file tree
Hide file tree
Showing 6 changed files with 470 additions and 9 deletions.
1 change: 1 addition & 0 deletions packages/component/src/molecules/index.ts
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 packages/component/src/molecules/textInput/TextInputState/index.ts
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;
330 changes: 330 additions & 0 deletions packages/component/src/molecules/textInput/index.tsx
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 };
Loading

0 comments on commit 49fddf6

Please sign in to comment.