@khanacademy/wonder-blocks-form
Advanced tools
Comparing version 1.5.0 to 1.6.0
@@ -1,2 +0,2 @@ | ||
import { createElement, Fragment, Component, Children, cloneElement } from 'react'; | ||
import { createElement, Fragment, Component, Children, cloneElement, forwardRef } from 'react'; | ||
import { StyleSheet, css } from 'aphrodite'; | ||
@@ -1041,14 +1041,16 @@ import Color, { mix, fade } from '@khanacademy/wonder-blocks-color'; | ||
// TODO(WB-1081): Change class name back to TextField after Styleguidist is gone. | ||
/** | ||
* A TextField is an element used to accept a single line of text from the user. | ||
*/ | ||
var TextField = /*#__PURE__*/function (_React$Component) { | ||
_inherits(TextField, _React$Component); | ||
var TextFieldInternal = /*#__PURE__*/function (_React$Component) { | ||
_inherits(TextFieldInternal, _React$Component); | ||
var _super = _createSuper(TextField); | ||
var _super = _createSuper(TextFieldInternal); | ||
function TextField(props) { | ||
function TextFieldInternal(props) { | ||
var _this; | ||
_classCallCheck(this, TextField); | ||
_classCallCheck(this, TextFieldInternal); | ||
@@ -1121,3 +1123,3 @@ _this = _super.call(this, props); | ||
_createClass(TextField, [{ | ||
_createClass(TextFieldInternal, [{ | ||
key: "componentDidMount", | ||
@@ -1141,2 +1143,3 @@ value: function componentDidMount() { | ||
testId = _this$props2.testId, | ||
forwardedRef = _this$props2.forwardedRef, | ||
onFocus = _this$props2.onFocus, | ||
@@ -1147,3 +1150,3 @@ onBlur = _this$props2.onBlur, | ||
onChange = _this$props2.onChange, | ||
otherProps = _objectWithoutProperties(_this$props2, ["id", "type", "value", "disabled", "onKeyDown", "placeholder", "required", "light", "style", "testId", "onFocus", "onBlur", "onValidate", "validate", "onChange"]); | ||
otherProps = _objectWithoutProperties(_this$props2, ["id", "type", "value", "disabled", "onKeyDown", "placeholder", "required", "light", "style", "testId", "forwardedRef", "onFocus", "onBlur", "onValidate", "validate", "onChange"]); | ||
@@ -1163,3 +1166,4 @@ return /*#__PURE__*/createElement("input", _extends({ | ||
required: required, | ||
"data-test-id": testId | ||
"data-test-id": testId, | ||
ref: forwardedRef | ||
}, otherProps)); | ||
@@ -1169,6 +1173,6 @@ } | ||
return TextField; | ||
return TextFieldInternal; | ||
}(Component); | ||
_defineProperty(TextField, "defaultProps", { | ||
_defineProperty(TextFieldInternal, "defaultProps", { | ||
type: "text", | ||
@@ -1178,2 +1182,3 @@ disabled: false, | ||
}); | ||
var styles$4 = StyleSheet.create({ | ||
@@ -1229,2 +1234,7 @@ input: { | ||
}); | ||
var TextField = forwardRef(function (props, ref) { | ||
return /*#__PURE__*/createElement(TextFieldInternal, _extends({}, props, { | ||
forwardedRef: ref | ||
})); | ||
}); | ||
@@ -1321,11 +1331,17 @@ var FieldHeading = /*#__PURE__*/function (_React$Component) { | ||
var LabeledTextField = /*#__PURE__*/function (_React$Component) { | ||
_inherits(LabeledTextField, _React$Component); | ||
// TODO(WB-1081): Change class name back to LabeledTextField after Styleguidist is gone. | ||
var _super = _createSuper(LabeledTextField); | ||
/** | ||
* A LabeledTextField is an element used to accept a single line of text | ||
* from the user paired with a label, description, and error field elements. | ||
*/ | ||
var LabeledTextFieldInternal = /*#__PURE__*/function (_React$Component) { | ||
_inherits(LabeledTextFieldInternal, _React$Component); | ||
function LabeledTextField(props) { | ||
var _super = _createSuper(LabeledTextFieldInternal); | ||
function LabeledTextFieldInternal(props) { | ||
var _this; | ||
_classCallCheck(this, LabeledTextField); | ||
_classCallCheck(this, LabeledTextFieldInternal); | ||
@@ -1390,3 +1406,3 @@ _this = _super.call(this, props); | ||
_createClass(LabeledTextField, [{ | ||
_createClass(LabeledTextFieldInternal, [{ | ||
key: "render", | ||
@@ -1407,3 +1423,4 @@ value: function render() { | ||
style = _this$props.style, | ||
testId = _this$props.testId; | ||
testId = _this$props.testId, | ||
forwardedRef = _this$props.forwardedRef; | ||
return /*#__PURE__*/createElement(IDProvider, { | ||
@@ -1432,3 +1449,4 @@ id: id, | ||
light: light, | ||
style: style | ||
style: style, | ||
ref: forwardedRef | ||
}), | ||
@@ -1443,6 +1461,6 @@ label: label, | ||
return LabeledTextField; | ||
return LabeledTextFieldInternal; | ||
}(Component); | ||
_defineProperty(LabeledTextField, "defaultProps", { | ||
_defineProperty(LabeledTextFieldInternal, "defaultProps", { | ||
type: "text", | ||
@@ -1453,2 +1471,8 @@ disabled: false, | ||
var LabeledTextField = forwardRef(function (props, ref) { | ||
return /*#__PURE__*/createElement(LabeledTextFieldInternal, _extends({}, props, { | ||
forwardedRef: ref | ||
})); | ||
}); | ||
export { Checkbox, CheckboxGroup, Choice, LabeledTextField, Radio, RadioGroup, TextField }; |
{ | ||
"name": "@khanacademy/wonder-blocks-form", | ||
"version": "1.5.0", | ||
"version": "1.6.0", | ||
"design": "v1", | ||
@@ -33,3 +33,3 @@ "description": "Form components for Wonder Blocks.", | ||
}, | ||
"gitHead": "3614532cac2a6efa09dea648f728b04bc7f156b5" | ||
"gitHead": "6529a7ca5f70d936fcf9388df73330e4024cad8d" | ||
} |
@@ -11,3 +11,3 @@ // This file is auto-generated by gen-snapshot-tests.js | ||
jest.mock("react-dom"); | ||
import {View, Text} from "@khanacademy/wonder-blocks-core"; | ||
import {View} from "@khanacademy/wonder-blocks-core"; | ||
import { | ||
@@ -19,4 +19,2 @@ Checkbox, | ||
RadioGroup, | ||
TextField, | ||
LabeledTextField, | ||
} from "@khanacademy/wonder-blocks-form"; | ||
@@ -31,4 +29,2 @@ import {StyleSheet} from "aphrodite"; | ||
import Color from "@khanacademy/wonder-blocks-color"; | ||
import {Strut} from "@khanacademy/wonder-blocks-layout"; | ||
import Spacing from "@khanacademy/wonder-blocks-spacing"; | ||
@@ -613,929 +609,2 @@ import CheckboxCore from "./../components/checkbox-core.js"; | ||
}); | ||
it("example 10", () => { | ||
class TextFieldExample extends React.Component { | ||
constructor(props) { | ||
super(props); | ||
this.state = { | ||
value: "", | ||
}; | ||
this.handleChange = this.handleChange.bind(this); | ||
this.handleKeyDown = this.handleKeyDown.bind(this); | ||
} | ||
handleChange(newValue) { | ||
this.setState({ | ||
value: newValue, | ||
}); | ||
} | ||
handleKeyDown(event) { | ||
if (event.key === "Enter") { | ||
event.currentTarget.blur(); | ||
} | ||
} | ||
render() { | ||
return ( | ||
<TextField | ||
id="tf-1" | ||
type="text" | ||
value={this.state.value} | ||
placeholder="Text" | ||
onChange={this.handleChange} | ||
onKeyDown={this.handleKeyDown} | ||
/> | ||
); | ||
} | ||
} | ||
const example = <TextFieldExample />; | ||
const tree = renderer.create(example).toJSON(); | ||
expect(tree).toMatchSnapshot(); | ||
}); | ||
it("example 11", () => { | ||
class TextFieldExample extends React.Component { | ||
constructor(props) { | ||
super(props); | ||
this.state = { | ||
value: "12345", | ||
}; | ||
this.handleChange = this.handleChange.bind(this); | ||
this.handleKeyDown = this.handleKeyDown.bind(this); | ||
} | ||
handleChange(newValue) { | ||
this.setState({ | ||
value: newValue, | ||
}); | ||
} | ||
handleKeyDown(event) { | ||
if (event.key === "Enter") { | ||
event.currentTarget.blur(); | ||
} | ||
} | ||
render() { | ||
return ( | ||
<TextField | ||
id="tf-1" | ||
type="number" | ||
value={this.state.value} | ||
placeholder="Number" | ||
onChange={this.handleChange} | ||
onKeyDown={this.handleKeyDown} | ||
/> | ||
); | ||
} | ||
} | ||
const example = <TextFieldExample />; | ||
const tree = renderer.create(example).toJSON(); | ||
expect(tree).toMatchSnapshot(); | ||
}); | ||
it("example 12", () => { | ||
class TextFieldExample extends React.Component { | ||
constructor(props) { | ||
super(props); | ||
this.state = { | ||
value: "Password123", | ||
errorMessage: null, | ||
focused: false, | ||
}; | ||
this.validate = this.validate.bind(this); | ||
this.handleValidate = this.handleValidate.bind(this); | ||
this.handleChange = this.handleChange.bind(this); | ||
this.handleKeyDown = this.handleKeyDown.bind(this); | ||
this.handleFocus = this.handleFocus.bind(this); | ||
this.handleBlur = this.handleBlur.bind(this); | ||
} | ||
validate(value) { | ||
if (value.length < 8) { | ||
return "Password must be at least 8 characters long"; | ||
} | ||
if (!/\d/.test(value)) { | ||
return "Password must contain a numeric value"; | ||
} | ||
} | ||
handleValidate(errorMessage) { | ||
this.setState({ | ||
errorMessage: errorMessage, | ||
}); | ||
} | ||
handleChange(newValue) { | ||
this.setState({ | ||
value: newValue, | ||
}); | ||
} | ||
handleKeyDown(event) { | ||
if (event.key === "Enter") { | ||
event.currentTarget.blur(); | ||
} | ||
} | ||
handleFocus() { | ||
this.setState({ | ||
focused: true, | ||
}); | ||
} | ||
handleBlur() { | ||
this.setState({ | ||
focused: false, | ||
}); | ||
} | ||
render() { | ||
return ( | ||
<View> | ||
<TextField | ||
id="tf-1" | ||
type="password" | ||
value={this.state.value} | ||
placeholder="Password" | ||
validate={this.validate} | ||
onValidate={this.handleValidate} | ||
onChange={this.handleChange} | ||
onKeyDown={this.handleKeyDown} | ||
onFocus={this.handleFocus} | ||
onBlur={this.handleBlur} | ||
/> | ||
{!this.state.focused && this.state.errorMessage && ( | ||
<View> | ||
<Strut size={Spacing.xSmall_8} /> | ||
<Text style={styles.errorMessage}> | ||
{this.state.errorMessage} | ||
</Text> | ||
</View> | ||
)} | ||
</View> | ||
); | ||
} | ||
} | ||
const styles = StyleSheet.create({ | ||
errorMessage: { | ||
color: Color.red, | ||
paddingLeft: Spacing.xxxSmall_4, | ||
}, | ||
}); | ||
const example = <TextFieldExample />; | ||
const tree = renderer.create(example).toJSON(); | ||
expect(tree).toMatchSnapshot(); | ||
}); | ||
it("example 13", () => { | ||
class TextFieldExample extends React.Component { | ||
constructor(props) { | ||
super(props); | ||
this.state = { | ||
value: "khan@khanacademy.org", | ||
errorMessage: null, | ||
focused: false, | ||
}; | ||
this.validate = this.validate.bind(this); | ||
this.handleValidate = this.handleValidate.bind(this); | ||
this.handleChange = this.handleChange.bind(this); | ||
this.handleKeyDown = this.handleKeyDown.bind(this); | ||
this.handleFocus = this.handleFocus.bind(this); | ||
this.handleBlur = this.handleBlur.bind(this); | ||
} | ||
validate(value) { | ||
const emailRegex = /^[^@\s]+@[^@\s.]+\.[^@.\s]+$/; | ||
if (!emailRegex.test(value)) { | ||
return "Please enter a valid email"; | ||
} | ||
} | ||
handleValidate(errorMessage) { | ||
this.setState({ | ||
errorMessage: errorMessage, | ||
}); | ||
} | ||
handleChange(newValue) { | ||
this.setState({ | ||
value: newValue, | ||
}); | ||
} | ||
handleKeyDown(event) { | ||
if (event.key === "Enter") { | ||
event.currentTarget.blur(); | ||
} | ||
} | ||
handleFocus() { | ||
this.setState({ | ||
focused: true, | ||
}); | ||
} | ||
handleBlur() { | ||
this.setState({ | ||
focused: false, | ||
}); | ||
} | ||
render() { | ||
return ( | ||
<View> | ||
<TextField | ||
id="tf-1" | ||
type="email" | ||
value={this.state.value} | ||
placeholder="Email" | ||
validate={this.validate} | ||
onValidate={this.handleValidate} | ||
onChange={this.handleChange} | ||
onKeyDown={this.handleKeyDown} | ||
onFocus={this.handleFocus} | ||
onBlur={this.handleBlur} | ||
/> | ||
{!this.state.focused && this.state.errorMessage && ( | ||
<View> | ||
<Strut size={Spacing.xSmall_8} /> | ||
<Text style={styles.errorMessage}> | ||
{this.state.errorMessage} | ||
</Text> | ||
</View> | ||
)} | ||
</View> | ||
); | ||
} | ||
} | ||
const styles = StyleSheet.create({ | ||
errorMessage: { | ||
color: Color.red, | ||
paddingLeft: Spacing.xxxSmall_4, | ||
}, | ||
}); | ||
const example = <TextFieldExample />; | ||
const tree = renderer.create(example).toJSON(); | ||
expect(tree).toMatchSnapshot(); | ||
}); | ||
it("example 14", () => { | ||
class TextFieldExample extends React.Component { | ||
constructor(props) { | ||
super(props); | ||
this.state = { | ||
value: "123-456-7890", | ||
errorMessage: null, | ||
focused: false, | ||
}; | ||
this.validate = this.validate.bind(this); | ||
this.handleValidate = this.handleValidate.bind(this); | ||
this.handleChange = this.handleChange.bind(this); | ||
this.handleKeyDown = this.handleKeyDown.bind(this); | ||
this.handleFocus = this.handleFocus.bind(this); | ||
this.handleBlur = this.handleBlur.bind(this); | ||
} | ||
validate(value) { | ||
const telRegex = /^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/; | ||
if (!telRegex.test(value)) { | ||
return "Invalid US telephone number"; | ||
} | ||
} | ||
handleValidate(errorMessage) { | ||
this.setState({ | ||
errorMessage: errorMessage, | ||
}); | ||
} | ||
handleChange(newValue) { | ||
this.setState({ | ||
value: newValue, | ||
}); | ||
} | ||
handleKeyDown(event) { | ||
if (event.key === "Enter") { | ||
event.currentTarget.blur(); | ||
} | ||
} | ||
handleFocus() { | ||
this.setState({ | ||
focused: true, | ||
}); | ||
} | ||
handleBlur() { | ||
this.setState({ | ||
focused: false, | ||
}); | ||
} | ||
render() { | ||
return ( | ||
<View> | ||
<TextField | ||
id="tf-1" | ||
type="email" | ||
value={this.state.value} | ||
placeholder="Telephone" | ||
validate={this.validate} | ||
onValidate={this.handleValidate} | ||
onChange={this.handleChange} | ||
onKeyDown={this.handleKeyDown} | ||
onFocus={this.handleFocus} | ||
onBlur={this.handleBlur} | ||
/> | ||
{!this.state.focused && this.state.errorMessage && ( | ||
<View> | ||
<Strut size={Spacing.xSmall_8} /> | ||
<Text style={styles.errorMessage}> | ||
{this.state.errorMessage} | ||
</Text> | ||
</View> | ||
)} | ||
</View> | ||
); | ||
} | ||
} | ||
const styles = StyleSheet.create({ | ||
errorMessage: { | ||
color: Color.red, | ||
paddingLeft: Spacing.xxxSmall_4, | ||
}, | ||
}); | ||
const example = <TextFieldExample />; | ||
const tree = renderer.create(example).toJSON(); | ||
expect(tree).toMatchSnapshot(); | ||
}); | ||
it("example 15", () => { | ||
class TextFieldExample extends React.Component { | ||
constructor(props) { | ||
super(props); | ||
this.state = { | ||
value: "khan", | ||
errorMessage: null, | ||
focused: false, | ||
}; | ||
this.validate = this.validate.bind(this); | ||
this.handleValidate = this.handleValidate.bind(this); | ||
this.handleChange = this.handleChange.bind(this); | ||
this.handleKeyDown = this.handleKeyDown.bind(this); | ||
this.handleFocus = this.handleFocus.bind(this); | ||
this.handleBlur = this.handleBlur.bind(this); | ||
} | ||
validate(value) { | ||
const emailRegex = /^[^@\s]+@[^@\s.]+\.[^@.\s]+$/; | ||
if (!emailRegex.test(value)) { | ||
return "Please enter a valid email"; | ||
} | ||
} | ||
handleValidate(errorMessage) { | ||
this.setState({ | ||
errorMessage: errorMessage, | ||
}); | ||
} | ||
handleChange(newValue) { | ||
this.setState({ | ||
value: newValue, | ||
}); | ||
} | ||
handleKeyDown(event) { | ||
if (event.key === "Enter") { | ||
event.currentTarget.blur(); | ||
} | ||
} | ||
handleFocus() { | ||
this.setState({ | ||
focused: true, | ||
}); | ||
} | ||
handleBlur() { | ||
this.setState({ | ||
focused: false, | ||
}); | ||
} | ||
render() { | ||
return ( | ||
<View> | ||
<TextField | ||
id="tf-1" | ||
type="email" | ||
value={this.state.value} | ||
placeholder="Email" | ||
validate={this.validate} | ||
onValidate={this.handleValidate} | ||
onChange={this.handleChange} | ||
onKeyDown={this.handleKeyDown} | ||
onFocus={this.handleFocus} | ||
onBlur={this.handleBlur} | ||
/> | ||
{!this.state.focused && this.state.errorMessage && ( | ||
<View> | ||
<Strut size={Spacing.xSmall_8} /> | ||
<Text style={styles.errorMessage}> | ||
{this.state.errorMessage} | ||
</Text> | ||
</View> | ||
)} | ||
</View> | ||
); | ||
} | ||
} | ||
const styles = StyleSheet.create({ | ||
errorMessage: { | ||
color: Color.red, | ||
paddingLeft: Spacing.xxxSmall_4, | ||
}, | ||
}); | ||
const example = <TextFieldExample />; | ||
const tree = renderer.create(example).toJSON(); | ||
expect(tree).toMatchSnapshot(); | ||
}); | ||
it("example 16", () => { | ||
const example = ( | ||
<TextField | ||
id="tf-1" | ||
value="" | ||
placeholder="This field is disabled." | ||
onChange={() => {}} | ||
disabled={true} | ||
/> | ||
); | ||
const tree = renderer.create(example).toJSON(); | ||
expect(tree).toMatchSnapshot(); | ||
}); | ||
it("example 17", () => { | ||
class TextFieldExample extends React.Component { | ||
constructor(props) { | ||
super(props); | ||
this.state = { | ||
value: "khan@khanacademy.org", | ||
errorMessage: null, | ||
focused: false, | ||
}; | ||
this.validate = this.validate.bind(this); | ||
this.handleValidate = this.handleValidate.bind(this); | ||
this.handleChange = this.handleChange.bind(this); | ||
this.handleKeyDown = this.handleKeyDown.bind(this); | ||
this.handleFocus = this.handleFocus.bind(this); | ||
this.handleBlur = this.handleBlur.bind(this); | ||
} | ||
validate(value) { | ||
const emailRegex = /^[^@\s]+@[^@\s.]+\.[^@.\s]+$/; | ||
if (!emailRegex.test(value)) { | ||
return "Please enter a valid email"; | ||
} | ||
} | ||
handleValidate(errorMessage) { | ||
this.setState({ | ||
errorMessage: errorMessage, | ||
}); | ||
} | ||
handleChange(newValue) { | ||
this.setState({ | ||
value: newValue, | ||
}); | ||
} | ||
handleKeyDown(event) { | ||
if (event.key === "Enter") { | ||
event.currentTarget.blur(); | ||
} | ||
} | ||
handleFocus() { | ||
this.setState({ | ||
focused: true, | ||
}); | ||
} | ||
handleBlur() { | ||
this.setState({ | ||
focused: false, | ||
}); | ||
} | ||
render() { | ||
return ( | ||
<View style={styles.darkBackground}> | ||
<TextField | ||
id="tf-1" | ||
type="email" | ||
value={this.state.value} | ||
light={true} | ||
placeholder="Email" | ||
validate={this.validate} | ||
onValidate={this.handleValidate} | ||
onChange={this.handleChange} | ||
onKeyDown={this.handleKeyDown} | ||
onFocus={this.handleFocus} | ||
onBlur={this.handleBlur} | ||
/> | ||
{!this.state.focused && this.state.errorMessage && ( | ||
<View> | ||
<Strut size={Spacing.xSmall_8} /> | ||
<Text style={styles.errorMessage}> | ||
{this.state.errorMessage} | ||
</Text> | ||
</View> | ||
)} | ||
</View> | ||
); | ||
} | ||
} | ||
const styles = StyleSheet.create({ | ||
errorMessage: { | ||
color: Color.white, | ||
paddingLeft: Spacing.xxxSmall_4, | ||
}, | ||
darkBackground: { | ||
backgroundColor: Color.darkBlue, | ||
padding: Spacing.medium_16, | ||
}, | ||
}); | ||
const example = <TextFieldExample />; | ||
const tree = renderer.create(example).toJSON(); | ||
expect(tree).toMatchSnapshot(); | ||
}); | ||
it("example 18", () => { | ||
class TextFieldExample extends React.Component { | ||
constructor(props) { | ||
super(props); | ||
this.state = { | ||
value: "", | ||
}; | ||
this.handleChange = this.handleChange.bind(this); | ||
this.handleKeyDown = this.handleKeyDown.bind(this); | ||
} | ||
handleChange(newValue) { | ||
this.setState({ | ||
value: newValue, | ||
}); | ||
} | ||
handleKeyDown(event) { | ||
if (event.key === "Enter") { | ||
event.currentTarget.blur(); | ||
} | ||
} | ||
render() { | ||
return ( | ||
<TextField | ||
id="tf-1" | ||
style={styles.customField} | ||
type="text" | ||
value={this.state.value} | ||
placeholder="Text" | ||
onChange={this.handleChange} | ||
onKeyDown={this.handleKeyDown} | ||
/> | ||
); | ||
} | ||
} | ||
const styles = StyleSheet.create({ | ||
customField: { | ||
backgroundColor: Color.darkBlue, | ||
color: Color.white, | ||
border: "none", | ||
maxWidth: 250, | ||
"::placeholder": { | ||
color: Color.white64, | ||
}, | ||
}, | ||
}); | ||
const example = <TextFieldExample />; | ||
const tree = renderer.create(example).toJSON(); | ||
expect(tree).toMatchSnapshot(); | ||
}); | ||
it("example 19", () => { | ||
class LabeledTextFieldExample extends React.Component { | ||
handleKeyDown(event) { | ||
if (event.key === "Enter") { | ||
event.currentTarget.blur(); | ||
} | ||
} | ||
render() { | ||
return ( | ||
<LabeledTextField | ||
label="Name" | ||
description="Please enter your name" | ||
initialValue="Khan" | ||
placeholder="Name" | ||
onKeyDown={this.handleKeyDown} | ||
/> | ||
); | ||
} | ||
} | ||
const example = <LabeledTextFieldExample />; | ||
const tree = renderer.create(example).toJSON(); | ||
expect(tree).toMatchSnapshot(); | ||
}); | ||
it("example 20", () => { | ||
class LabeledTextFieldExample extends React.Component { | ||
handleKeyDown(event) { | ||
if (event.key === "Enter") { | ||
event.currentTarget.blur(); | ||
} | ||
} | ||
render() { | ||
return ( | ||
<LabeledTextField | ||
label="Age" | ||
type="number" | ||
description="Please enter your age" | ||
initialValue="18" | ||
placeholder="Age" | ||
onKeyDown={this.handleKeyDown} | ||
/> | ||
); | ||
} | ||
} | ||
const example = <LabeledTextFieldExample />; | ||
const tree = renderer.create(example).toJSON(); | ||
expect(tree).toMatchSnapshot(); | ||
}); | ||
it("example 21", () => { | ||
class LabeledTextFieldExample extends React.Component { | ||
validate(value) { | ||
if (value.length < 8) { | ||
return "Password must be at least 8 characters long"; | ||
} | ||
if (!/\d/.test(value)) { | ||
return "Password must contain a numeric value"; | ||
} | ||
} | ||
handleKeyDown(event) { | ||
if (event.key === "Enter") { | ||
event.currentTarget.blur(); | ||
} | ||
} | ||
render() { | ||
return ( | ||
<LabeledTextField | ||
label="Password" | ||
type="password" | ||
description="Please enter a secure password" | ||
initialValue="Password123" | ||
placeholder="Password" | ||
validate={this.validate} | ||
onKeyDown={this.handleKeyDown} | ||
/> | ||
); | ||
} | ||
} | ||
const example = <LabeledTextFieldExample />; | ||
const tree = renderer.create(example).toJSON(); | ||
expect(tree).toMatchSnapshot(); | ||
}); | ||
it("example 22", () => { | ||
class LabeledTextFieldExample extends React.Component { | ||
validate(value) { | ||
const emailRegex = /^[^@\s]+@[^@\s.]+\.[^@.\s]+$/; | ||
if (!emailRegex.test(value)) { | ||
return "Please enter a valid email"; | ||
} | ||
} | ||
handleKeyDown(event) { | ||
if (event.key === "Enter") { | ||
event.currentTarget.blur(); | ||
} | ||
} | ||
render() { | ||
return ( | ||
<LabeledTextField | ||
label="Email" | ||
type="email" | ||
description="Please provide your personal email" | ||
initialValue="khan@khan.org" | ||
placeholder="Email" | ||
validate={this.validate} | ||
onKeyDown={this.handleKeyDown} | ||
/> | ||
); | ||
} | ||
} | ||
const example = <LabeledTextFieldExample />; | ||
const tree = renderer.create(example).toJSON(); | ||
expect(tree).toMatchSnapshot(); | ||
}); | ||
it("example 23", () => { | ||
class LabeledTextFieldExample extends React.Component { | ||
validate(value) { | ||
const telRegex = /^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/; | ||
if (!telRegex.test(value)) { | ||
return "Invalid US telephone number"; | ||
} | ||
} | ||
handleKeyDown(event) { | ||
if (event.key === "Enter") { | ||
event.currentTarget.blur(); | ||
} | ||
} | ||
render() { | ||
return ( | ||
<LabeledTextField | ||
label="Telephone" | ||
type="tel" | ||
description="Please provide your personal phone number" | ||
initialValue="123-456-7890" | ||
placeholder="Telephone" | ||
validate={this.validate} | ||
onKeyDown={this.handleKeyDown} | ||
/> | ||
); | ||
} | ||
} | ||
const example = <LabeledTextFieldExample />; | ||
const tree = renderer.create(example).toJSON(); | ||
expect(tree).toMatchSnapshot(); | ||
}); | ||
it("example 24", () => { | ||
class LabeledTextFieldExample extends React.Component { | ||
validate(value) { | ||
const emailRegex = /^[^@\s]+@[^@\s.]+\.[^@.\s]+$/; | ||
if (!emailRegex.test(value)) { | ||
return "Please enter a valid email"; | ||
} | ||
} | ||
handleKeyDown(event) { | ||
if (event.key === "Enter") { | ||
event.currentTarget.blur(); | ||
} | ||
} | ||
render() { | ||
return ( | ||
<LabeledTextField | ||
label="Email" | ||
type="email" | ||
description="Please enter your personal email" | ||
initialValue="khan" | ||
placeholder="Email" | ||
validate={this.validate} | ||
onKeyDown={this.handleKeyDown} | ||
/> | ||
); | ||
} | ||
} | ||
const example = <LabeledTextFieldExample />; | ||
const tree = renderer.create(example).toJSON(); | ||
expect(tree).toMatchSnapshot(); | ||
}); | ||
it("example 25", () => { | ||
const example = ( | ||
<LabeledTextField | ||
label="Name" | ||
description="Please enter your name" | ||
placeholder="Name" | ||
disabled={true} | ||
/> | ||
); | ||
const tree = renderer.create(example).toJSON(); | ||
expect(tree).toMatchSnapshot(); | ||
}); | ||
it("example 26", () => { | ||
class LabeledTextFieldExample extends React.Component { | ||
handleKeyDown(event) { | ||
if (event.key === "Enter") { | ||
event.currentTarget.blur(); | ||
} | ||
} | ||
render() { | ||
return ( | ||
<View style={styles.darkBackground}> | ||
<LabeledTextField | ||
label={ | ||
<LabelMedium style={styles.whiteColor}> | ||
Name | ||
</LabelMedium> | ||
} | ||
description={ | ||
<LabelSmall style={styles.offWhiteColor}> | ||
Please enter your name | ||
</LabelSmall> | ||
} | ||
placeholder="Name" | ||
light={true} | ||
onKeyDown={this.handleKeyDown} | ||
/> | ||
</View> | ||
); | ||
} | ||
} | ||
const styles = StyleSheet.create({ | ||
darkBackground: { | ||
background: Color.darkBlue, | ||
padding: `${Spacing.medium_16}px`, | ||
}, | ||
whiteColor: { | ||
color: Color.white, | ||
}, | ||
offWhiteColor: { | ||
color: Color.white64, | ||
}, | ||
}); | ||
const example = <LabeledTextFieldExample />; | ||
const tree = renderer.create(example).toJSON(); | ||
expect(tree).toMatchSnapshot(); | ||
}); | ||
it("example 27", () => { | ||
class LabeledTextFieldExample extends React.Component { | ||
handleKeyDown(event) { | ||
if (event.key === "Enter") { | ||
event.currentTarget.blur(); | ||
} | ||
} | ||
render() { | ||
return ( | ||
<LabeledTextField | ||
label="Name" | ||
description="Please enter your name" | ||
initialValue="Khan" | ||
placeholder="Name" | ||
style={styles.customField} | ||
onKeyDown={this.handleKeyDown} | ||
/> | ||
); | ||
} | ||
} | ||
const styles = StyleSheet.create({ | ||
customField: { | ||
backgroundColor: Color.darkBlue, | ||
color: Color.white, | ||
border: "none", | ||
maxWidth: 250, | ||
"::placeholder": { | ||
color: Color.white64, | ||
}, | ||
}, | ||
}); | ||
const example = <LabeledTextFieldExample />; | ||
const tree = renderer.create(example).toJSON(); | ||
expect(tree).toMatchSnapshot(); | ||
}); | ||
}); |
@@ -18,3 +18,3 @@ //@flow | ||
const wrapper = mount(<LabeledTextField label="Label" />); | ||
const field = wrapper.find("TextField"); | ||
const field = wrapper.find("TextFieldInternal"); | ||
@@ -25,3 +25,6 @@ // Act | ||
// Assert | ||
expect(wrapper).toHaveState("focused", true); | ||
expect(wrapper.find("LabeledTextFieldInternal")).toHaveState( | ||
"focused", | ||
true, | ||
); | ||
}); | ||
@@ -32,3 +35,3 @@ | ||
const wrapper = mount(<LabeledTextField label="Label" />); | ||
const field = wrapper.find("TextField"); | ||
const field = wrapper.find("TextFieldInternal"); | ||
@@ -41,3 +44,6 @@ // Act | ||
// Assert | ||
expect(wrapper).toHaveState("focused", false); | ||
expect(wrapper.find("LabeledTextFieldInternal")).toHaveState( | ||
"focused", | ||
false, | ||
); | ||
}); | ||
@@ -55,3 +61,6 @@ | ||
// Assert | ||
expect(wrapper).toHaveState("value", newValue); | ||
expect(wrapper.find("LabeledTextFieldInternal")).toHaveState( | ||
"value", | ||
newValue, | ||
); | ||
}); | ||
@@ -239,3 +248,3 @@ | ||
// Act | ||
const field = wrapper.find("TextField"); | ||
const field = wrapper.find("TextFieldInternal"); | ||
field.simulate("focus"); | ||
@@ -255,3 +264,3 @@ | ||
// Act | ||
const field = wrapper.find("TextField"); | ||
const field = wrapper.find("TextFieldInternal"); | ||
field.simulate("focus"); | ||
@@ -288,3 +297,3 @@ await wait(0); | ||
// Assert | ||
const textField = wrapper.find("TextField"); | ||
const textField = wrapper.find("TextFieldInternal"); | ||
expect(textField).toHaveProp("light", true); | ||
@@ -308,3 +317,3 @@ }); | ||
// Assert | ||
const textField = wrapper.find("TextField"); | ||
const textField = wrapper.find("TextFieldInternal"); | ||
expect(textField).toHaveStyle(styles.style1); | ||
@@ -311,0 +320,0 @@ }); |
@@ -24,3 +24,3 @@ // @flow | ||
// Assert | ||
expect(wrapper).toHaveState("focused", true); | ||
expect(wrapper.find("TextFieldInternal")).toHaveState("focused", true); | ||
}); | ||
@@ -60,3 +60,3 @@ | ||
// Assert | ||
expect(wrapper).toHaveState("focused", false); | ||
expect(wrapper.find("TextFieldInternal")).toHaveState("focused", false); | ||
}); | ||
@@ -63,0 +63,0 @@ |
@@ -10,2 +10,4 @@ // @flow | ||
type WithForwardRef = {|forwardedRef: React.Ref<"input">|}; | ||
type Props = {| | ||
@@ -95,6 +97,11 @@ /** | ||
type PropsWithForwardRef = {| | ||
...Props, | ||
...WithForwardRef, | ||
|}; | ||
type DefaultProps = {| | ||
type: $PropertyType<Props, "type">, | ||
disabled: $PropertyType<Props, "disabled">, | ||
light: $PropertyType<Props, "light">, | ||
type: $PropertyType<PropsWithForwardRef, "type">, | ||
disabled: $PropertyType<PropsWithForwardRef, "disabled">, | ||
light: $PropertyType<PropsWithForwardRef, "light">, | ||
|}; | ||
@@ -119,3 +126,11 @@ | ||
export default class LabeledTextField extends React.Component<Props, State> { | ||
// TODO(WB-1081): Change class name back to LabeledTextField after Styleguidist is gone. | ||
/** | ||
* A LabeledTextField is an element used to accept a single line of text | ||
* from the user paired with a label, description, and error field elements. | ||
*/ | ||
class LabeledTextFieldInternal extends React.Component< | ||
PropsWithForwardRef, | ||
State, | ||
> { | ||
static defaultProps: DefaultProps = { | ||
@@ -127,3 +142,3 @@ type: "text", | ||
constructor(props: Props) { | ||
constructor(props: PropsWithForwardRef) { | ||
super(props); | ||
@@ -190,2 +205,3 @@ this.state = { | ||
testId, | ||
forwardedRef, | ||
} = this.props; | ||
@@ -219,2 +235,3 @@ | ||
style={style} | ||
ref={forwardedRef} | ||
/> | ||
@@ -231,1 +248,15 @@ } | ||
} | ||
type ExportProps = $Diff< | ||
React.ElementConfig<typeof LabeledTextFieldInternal>, | ||
WithForwardRef, | ||
>; | ||
const LabeledTextField: React.AbstractComponent< | ||
ExportProps, | ||
HTMLInputElement, | ||
> = React.forwardRef<ExportProps, HTMLInputElement>((props, ref) => ( | ||
<LabeledTextFieldInternal {...props} forwardedRef={ref} /> | ||
)); | ||
export default LabeledTextField; |
@@ -318,2 +318,60 @@ LabeledTextField derives from TextField and allows the handling of single lines of text with convenient label, description, and error messages. | ||
<LabeledTextFieldExample /> | ||
``` | ||
``` | ||
The field forwards its ref to the input | ||
```js | ||
import {View} from "@khanacademy/wonder-blocks-core"; | ||
import {LabeledTextField} from "@khanacademy/wonder-blocks-form"; | ||
import Button from "@khanacademy/wonder-blocks-button"; | ||
import {Strut} from "@khanacademy/wonder-blocks-layout"; | ||
import Spacing from "@khanacademy/wonder-blocks-spacing"; | ||
import {StyleSheet} from "aphrodite"; | ||
class LabeledTextFieldExample extends React.Component { | ||
constructor(props) { | ||
super(props); | ||
this.inputRef = React.createRef(); | ||
this.handleSubmit = this.handleSubmit.bind(this); | ||
} | ||
handleKeyDown(event) { | ||
if (event.key === "Enter") { | ||
event.currentTarget.blur(); | ||
} | ||
} | ||
handleSubmit() { | ||
if (this.inputRef.current) { | ||
this.inputRef.current.focus(); | ||
} | ||
} | ||
render() { | ||
return ( | ||
<View> | ||
<LabeledTextField | ||
label="Name" | ||
description="Please enter your name" | ||
initialValue="Khan" | ||
placeholder="Name" | ||
onKeyDown={this.handleKeyDown} | ||
ref={this.inputRef} | ||
/> | ||
<Strut size={Spacing.medium_16} /> | ||
<Button style={styles.button} onClick={this.handleSubmit}> | ||
Focus Input | ||
</Button> | ||
</View> | ||
); | ||
} | ||
} | ||
const styles = StyleSheet.create({ | ||
button: { | ||
maxWidth: 150, | ||
}, | ||
}); | ||
<LabeledTextFieldExample /> | ||
``` |
@@ -9,2 +9,4 @@ // @flow | ||
import Spacing from "@khanacademy/wonder-blocks-spacing"; | ||
import {Strut} from "@khanacademy/wonder-blocks-layout"; | ||
import Button from "@khanacademy/wonder-blocks-button"; | ||
import {StyleSheet} from "aphrodite"; | ||
@@ -218,2 +220,35 @@ | ||
export const ref: StoryComponentType = () => { | ||
const inputRef = React.createRef<HTMLInputElement>(); | ||
const handleKeyDown = (event: SyntheticKeyboardEvent<HTMLInputElement>) => { | ||
if (event.key === "Enter") { | ||
event.currentTarget.blur(); | ||
} | ||
}; | ||
const handleSubmit = () => { | ||
if (inputRef.current) { | ||
inputRef.current.focus(); | ||
} | ||
}; | ||
return ( | ||
<View> | ||
<LabeledTextField | ||
label="Name" | ||
description="Please enter your name" | ||
initialValue="Khan" | ||
placeholder="Name" | ||
onKeyDown={handleKeyDown} | ||
ref={inputRef} | ||
/> | ||
<Strut size={Spacing.medium_16} /> | ||
<Button style={styles.button} onClick={handleSubmit}> | ||
Focus Input | ||
</Button> | ||
</View> | ||
); | ||
}; | ||
const styles = StyleSheet.create({ | ||
@@ -239,2 +274,5 @@ darkBackground: { | ||
}, | ||
button: { | ||
maxWidth: 150, | ||
}, | ||
}); |
@@ -13,2 +13,4 @@ // @flow | ||
type WithForwardRef = {|forwardedRef: React.Ref<"input">|}; | ||
type Props = {| | ||
@@ -94,6 +96,11 @@ ...AriaProps, | ||
type PropsWithForwardRef = {| | ||
...Props, | ||
...WithForwardRef, | ||
|}; | ||
type DefaultProps = {| | ||
type: $PropertyType<Props, "type">, | ||
disabled: $PropertyType<Props, "disabled">, | ||
light: $PropertyType<Props, "light">, | ||
type: $PropertyType<PropsWithForwardRef, "type">, | ||
disabled: $PropertyType<PropsWithForwardRef, "disabled">, | ||
light: $PropertyType<PropsWithForwardRef, "light">, | ||
|}; | ||
@@ -113,6 +120,7 @@ | ||
// TODO(WB-1081): Change class name back to TextField after Styleguidist is gone. | ||
/** | ||
* A TextField is an element used to accept a single line of text from the user. | ||
*/ | ||
export default class TextField extends React.Component<Props, State> { | ||
class TextFieldInternal extends React.Component<PropsWithForwardRef, State> { | ||
static defaultProps: DefaultProps = { | ||
@@ -124,3 +132,3 @@ type: "text", | ||
constructor(props: Props) { | ||
constructor(props: PropsWithForwardRef) { | ||
super(props); | ||
@@ -197,2 +205,3 @@ if (props.validate) { | ||
testId, | ||
forwardedRef, | ||
// The following props are being included here to avoid | ||
@@ -238,2 +247,3 @@ // passing them down to the otherProps spread | ||
data-test-id={testId} | ||
ref={forwardedRef} | ||
{...otherProps} | ||
@@ -295,1 +305,15 @@ /> | ||
}); | ||
type ExportProps = $Diff< | ||
React.ElementConfig<typeof TextFieldInternal>, | ||
WithForwardRef, | ||
>; | ||
const TextField: React.AbstractComponent< | ||
ExportProps, | ||
HTMLInputElement, | ||
> = React.forwardRef<ExportProps, HTMLInputElement>((props, ref) => ( | ||
<TextFieldInternal {...props} forwardedRef={ref} /> | ||
)); | ||
export default TextField; |
@@ -71,3 +71,3 @@ Text | ||
<TextField | ||
id="tf-1" | ||
id="tf-2" | ||
type="number" | ||
@@ -147,3 +147,3 @@ value={this.state.value} | ||
<TextField | ||
id="tf-1" | ||
id="tf-3" | ||
type="password" | ||
@@ -239,3 +239,3 @@ value={this.state.value} | ||
<TextField | ||
id="tf-1" | ||
id="tf-4" | ||
type="email" | ||
@@ -331,3 +331,3 @@ value={this.state.value} | ||
<TextField | ||
id="tf-1" | ||
id="tf-5" | ||
type="email" | ||
@@ -423,3 +423,3 @@ value={this.state.value} | ||
<TextField | ||
id="tf-1" | ||
id="tf-6" | ||
type="email" | ||
@@ -462,3 +462,3 @@ value={this.state.value} | ||
<TextField | ||
id="tf-1" value="" | ||
id="tf-7" value="" | ||
placeholder="This field is disabled." | ||
@@ -529,3 +529,3 @@ onChange={() => {}} | ||
<TextField | ||
id="tf-1" | ||
id="tf-8" | ||
type="email" | ||
@@ -598,3 +598,3 @@ value={this.state.value} | ||
<TextField | ||
id="tf-1" | ||
id="tf-9" | ||
style={styles.customField} | ||
@@ -625,1 +625,69 @@ type="text" | ||
``` | ||
Using Ref | ||
```js | ||
import {View} from "@khanacademy/wonder-blocks-core"; | ||
import {TextField} from "@khanacademy/wonder-blocks-form"; | ||
import Button from "@khanacademy/wonder-blocks-button"; | ||
import {Strut} from "@khanacademy/wonder-blocks-layout"; | ||
import Spacing from "@khanacademy/wonder-blocks-spacing"; | ||
import {StyleSheet} from "aphrodite"; | ||
class TextFieldExample extends React.Component { | ||
constructor(props) { | ||
super(props); | ||
this.state = { | ||
value: "", | ||
}; | ||
this.inputRef = React.createRef(); | ||
this.handleChange = this.handleChange.bind(this); | ||
this.handleKeyDown = this.handleKeyDown.bind(this); | ||
this.handleSubmit = this.handleSubmit.bind(this); | ||
} | ||
handleChange(newValue) { | ||
this.setState({value: newValue}); | ||
} | ||
handleKeyDown(event) { | ||
if (event.key === "Enter") { | ||
event.currentTarget.blur(); | ||
} | ||
} | ||
handleSubmit() { | ||
if (this.inputRef.current) { | ||
this.inputRef.current.focus(); | ||
} | ||
} | ||
render() { | ||
return ( | ||
<View> | ||
<TextField | ||
id="tf-10" | ||
type="text" | ||
value={this.state.value} | ||
placeholder="Text" | ||
onChange={this.handleChange} | ||
onKeyDown={this.handleKeyDown} | ||
ref={this.inputRef} | ||
/> | ||
<Strut size={Spacing.medium_16} /> | ||
<Button style={styles.button} onClick={this.handleSubmit}> | ||
Focus Input | ||
</Button> | ||
</View> | ||
); | ||
} | ||
} | ||
const styles = StyleSheet.create({ | ||
button: { | ||
maxWidth: 150, | ||
}, | ||
}); | ||
<TextFieldExample /> | ||
``` |
@@ -10,2 +10,3 @@ // @flow | ||
import {TextField} from "@khanacademy/wonder-blocks-form"; | ||
import Button from "@khanacademy/wonder-blocks-button"; | ||
@@ -397,2 +398,41 @@ import type {StoryComponentType} from "@storybook/react"; | ||
export const ref: StoryComponentType = () => { | ||
const [value, setValue] = React.useState(""); | ||
const inputRef: RefObject<typeof HTMLInputElement> = React.createRef(); | ||
const handleChange = (newValue: string) => { | ||
setValue(newValue); | ||
}; | ||
const handleKeyDown = (event: SyntheticKeyboardEvent<HTMLInputElement>) => { | ||
if (event.key === "Enter") { | ||
event.currentTarget.blur(); | ||
} | ||
}; | ||
const handleSubmit = () => { | ||
if (inputRef.current) { | ||
inputRef.current.focus(); | ||
} | ||
}; | ||
return ( | ||
<View> | ||
<TextField | ||
id="tf-1" | ||
type="text" | ||
value={value} | ||
placeholder="Text" | ||
onChange={handleChange} | ||
onKeyDown={handleKeyDown} | ||
ref={inputRef} | ||
/> | ||
<Strut size={Spacing.medium_16} /> | ||
<Button style={styles.button} onClick={handleSubmit}> | ||
Focus Input | ||
</Button> | ||
</View> | ||
); | ||
}; | ||
const styles = StyleSheet.create({ | ||
@@ -420,2 +460,5 @@ errorMessage: { | ||
}, | ||
button: { | ||
maxWidth: 150, | ||
}, | ||
}); |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
508939
6699