diff --git a/package.json b/package.json
index 7c008d5ccc..620957dd04 100644
--- a/package.json
+++ b/package.json
@@ -118,9 +118,11 @@
"@peculiar/webcrypto": "^1.0.22",
"@types/classnames": "^2.2.10",
"@types/flux": "^3.1.9",
+ "@types/lodash": "^4.14.152",
"@types/modernizr": "^3.5.3",
"@types/qrcode": "^1.3.4",
"@types/react": "16.9",
+ "@types/react-dom": "^16.9.8",
"@types/zxcvbn": "^4.4.0",
"babel-eslint": "^10.0.3",
"babel-jest": "^24.9.0",
diff --git a/src/components/views/elements/Field.js b/src/components/views/elements/Field.tsx
similarity index 67%
rename from src/components/views/elements/Field.js
rename to src/components/views/elements/Field.tsx
index 2ebb90da26..100a6ebf56 100644
--- a/src/components/views/elements/Field.js
+++ b/src/components/views/elements/Field.tsx
@@ -15,10 +15,9 @@ limitations under the License.
*/
import React from 'react';
-import PropTypes from 'prop-types';
import classNames from 'classnames';
import * as sdk from '../../../index';
-import { debounce } from 'lodash';
+import { debounce, Cancelable } from 'lodash';
// Invoke validation from user input (when typing, etc.) at most once every N ms.
const VALIDATION_THROTTLE_MS = 200;
@@ -29,51 +28,88 @@ function getId() {
return `${BASE_ID}_${count++}`;
}
-export default class Field extends React.PureComponent {
- static propTypes = {
- // The field's ID, which binds the input and label together. Immutable.
- id: PropTypes.string,
- // The element to create. Defaults to "input".
- // To define options for a select, use
- element: PropTypes.oneOf(["input", "select", "textarea"]),
- // The field's type (when used as an ). Defaults to "text".
- type: PropTypes.string,
- // id of a element for suggestions
- list: PropTypes.string,
- // The field's label string.
- label: PropTypes.string,
- // The field's placeholder string. Defaults to the label.
- placeholder: PropTypes.string,
- // The field's value.
- // This is a controlled component, so the value is required.
- value: PropTypes.string.isRequired,
- // Optional component to include inside the field before the input.
- prefix: PropTypes.node,
- // Optional component to include inside the field after the input.
- postfix: PropTypes.node,
- // The callback called whenever the contents of the field
- // changes. Returns an object with `valid` boolean field
- // and a `feedback` react component field to provide feedback
- // to the user.
- onValidate: PropTypes.func,
- // If specified, overrides the value returned by onValidate.
- flagInvalid: PropTypes.bool,
- // If specified, contents will appear as a tooltip on the element and
- // validation feedback tooltips will be suppressed.
- tooltipContent: PropTypes.node,
- // If specified alongside tooltipContent, the class name to apply to the
- // tooltip itself.
- tooltipClassName: PropTypes.string,
- // If specified, an additional class name to apply to the field container
- className: PropTypes.string,
- // All other props pass through to the .
- };
+interface IProps extends React.HTMLAttributes {
+ // The field's ID, which binds the input and label together. Immutable.
+ id?: string,
+ // The element to create. Defaults to "input".
+ // To define options for a select, use
+ element?: InputType,
+ // The field's type (when used as an ). Defaults to "text".
+ type?: string,
+ // id of a element for suggestions
+ list?: string,
+ // The field's label string.
+ label?: string,
+ // The field's placeholder string. Defaults to the label.
+ placeholder?: string,
+ // The field's value.
+ // This is a controlled component, so the value is required.
+ value: string,
+ // Optional component to include inside the field before the input.
+ prefixComponent?: React.ReactNode,
+ // Optional component to include inside the field after the input.
+ postfixComponent?: React.ReactNode,
+ // The callback called whenever the contents of the field
+ // changes. Returns an object with `valid` boolean field
+ // and a `feedback` react component field to provide feedback
+ // to the user.
+ onValidate?: (
+ args: {value: string, focused: boolean, allowEmpty: boolean}
+ ) => {valid: boolean, feedback: React.ReactNode},
+ // If specified, overrides the value returned by onValidate.
+ flagInvalid?: boolean,
+ // If specified, contents will appear as a tooltip on the element and
+ // validation feedback tooltips will be suppressed.
+ tooltipContent?: React.ReactNode,
+ // If specified alongside tooltipContent, the class name to apply to the
+ // tooltip itself.
+ tooltipClassName?: string,
+ // If specified, an additional class name to apply to the field container
+ className?: string,
+ // All other props pass through to the .
+}
+enum InputType {
+ INPUT = "input",
+ SELECT = "select",
+ TEXTAREA = "textarea",
+}
+
+interface IState {
+ valid: boolean,
+ feedback: React.ReactNode,
+ feedbackVisible: boolean,
+ focused: boolean,
+}
+
+export default class Field extends React.PureComponent {
+ private id: string;
+ private input: HTMLInputElement;
+
+ /*
+ * This was changed from throttle to debounce: this is more traditional for
+ * form validation since it means that the validation doesn't happen at all
+ * until the user stops typing for a bit (debounce defaults to not running on
+ * the leading edge). If we're doing an HTTP hit on each validation, we have more
+ * incentive to prevent validating input that's very unlikely to be valid.
+ * We may find that we actually want different behaviour for registration
+ * fields, in which case we can add some options to control it.
+ */
+ validateOnChange = debounce(() => {
+ this.validate({
+ focused: true,
+ });
+ }, VALIDATION_THROTTLE_MS);
+
+ focus() {
+ this.input.focus();
+ }
constructor(props) {
super(props);
this.state = {
valid: undefined,
feedback: undefined,
+ feedbackVisible: false,
focused: false,
};
@@ -114,11 +150,7 @@ export default class Field extends React.PureComponent {
}
};
- focus() {
- this.input.focus();
- }
-
- async validate({ focused, allowEmpty = true }) {
+ async validate({ focused, allowEmpty = true }: {focused: boolean, allowEmpty?: boolean}) {
if (!this.props.onValidate) {
return;
}
@@ -149,48 +181,37 @@ export default class Field extends React.PureComponent {
}
}
- /*
- * This was changed from throttle to debounce: this is more traditional for
- * form validation since it means that the validation doesn't happen at all
- * until the user stops typing for a bit (debounce defaults to not running on
- * the leading edge). If we're doing an HTTP hit on each validation, we have more
- * incentive to prevent validating input that's very unlikely to be valid.
- * We may find that we actually want different behaviour for registration
- * fields, in which case we can add some options to control it.
- */
- validateOnChange = debounce(() => {
- this.validate({
- focused: true,
- });
- }, VALIDATION_THROTTLE_MS);
+
render() {
const {
- element, prefix, postfix, className, onValidate, children,
+ element, prefixComponent, postfixComponent, className, onValidate, children,
tooltipContent, flagInvalid, tooltipClassName, list, ...inputProps} = this.props;
const inputElement = element || "input";
// Set some defaults for the element
inputProps.type = inputProps.type || "text";
- inputProps.ref = input => this.input = input;
+ const ref = input => this.input = input;
inputProps.placeholder = inputProps.placeholder || inputProps.label;
inputProps.id = this.id; // this overwrites the id from props
inputProps.onFocus = this.onFocus;
inputProps.onChange = this.onChange;
inputProps.onBlur = this.onBlur;
- inputProps.list = list;
- const fieldInput = React.createElement(inputElement, inputProps, children);
+ // Appease typescript's inference
+ const inputProps_ = {...inputProps, ref, list};
+
+ const fieldInput = React.createElement(inputElement, inputProps_, children);
let prefixContainer = null;
- if (prefix) {
- prefixContainer = {prefix} ;
+ if (prefixComponent) {
+ prefixContainer = {prefixComponent} ;
}
let postfixContainer = null;
- if (postfix) {
- postfixContainer = {postfix} ;
+ if (postfixComponent) {
+ postfixContainer = {postfixComponent} ;
}
const hasValidationFlag = flagInvalid !== null && flagInvalid !== undefined;
@@ -198,7 +219,7 @@ export default class Field extends React.PureComponent {
// If we have a prefix element, leave the label always at the top left and
// don't animate it, as it looks a bit clunky and would add complexity to do
// properly.
- mx_Field_labelAlwaysTopLeft: prefix,
+ mx_Field_labelAlwaysTopLeft: prefixComponent,
mx_Field_valid: onValidate && this.state.valid === true,
mx_Field_invalid: hasValidationFlag
? flagInvalid
diff --git a/src/components/views/elements/Tooltip.js b/src/components/views/elements/Tooltip.tsx
similarity index 71%
rename from src/components/views/elements/Tooltip.js
rename to src/components/views/elements/Tooltip.tsx
index 4807ade3db..753052717c 100644
--- a/src/components/views/elements/Tooltip.js
+++ b/src/components/views/elements/Tooltip.tsx
@@ -18,67 +18,68 @@ limitations under the License.
*/
-import React from 'react';
+import React, { Component } from 'react';
import ReactDOM from 'react-dom';
-import PropTypes from 'prop-types';
-import createReactClass from 'create-react-class';
import dis from '../../../dispatcher/dispatcher';
import classNames from 'classnames';
+import { ViewTooltipPayload } from '../../../dispatcher/payloads/ViewUserPayload';
+import { Action } from '../../../dispatcher/actions';
const MIN_TOOLTIP_HEIGHT = 25;
-export default createReactClass({
- displayName: 'Tooltip',
-
- propTypes: {
+interface IProps {
// Class applied to the element used to position the tooltip
- className: PropTypes.string,
+ className: string,
// Class applied to the tooltip itself
- tooltipClassName: PropTypes.string,
+ tooltipClassName: string,
// Whether the tooltip is visible or hidden.
// The hidden state allows animating the tooltip away via CSS.
// Defaults to visible if unset.
- visible: PropTypes.bool,
+ visible: boolean,
// the react element to put into the tooltip
- label: PropTypes.node,
- },
+ label: React.ReactNode,
+}
- getDefaultProps() {
- return {
- visible: true,
- };
- },
+class Tooltip extends React.Component {
+ private tooltipContainer: HTMLElement;
+ private tooltip: void | Element | Component;
+ private parent: Element;
+
+
+ static defaultProps = {
+ visible: true,
+ };
// Create a wrapper for the tooltip outside the parent and attach it to the body element
- componentDidMount: function() {
+ componentDidMount() {
this.tooltipContainer = document.createElement("div");
this.tooltipContainer.className = "mx_Tooltip_wrapper";
document.body.appendChild(this.tooltipContainer);
- window.addEventListener('scroll', this._renderTooltip, true);
+ window.addEventListener('scroll', this.renderTooltip, true);
- this.parent = ReactDOM.findDOMNode(this).parentNode;
+ this.parent = ReactDOM.findDOMNode(this).parentNode as Element;
- this._renderTooltip();
- },
+ this.renderTooltip();
+ }
- componentDidUpdate: function() {
- this._renderTooltip();
- },
+ componentDidUpdate() {
+ this.renderTooltip();
+ }
// Remove the wrapper element, as the tooltip has finished using it
- componentWillUnmount: function() {
- dis.dispatch({
- action: 'view_tooltip',
+ componentWillUnmount() {
+ dis.dispatch({
+ action: Action.ViewTooltip,
tooltip: null,
parent: null,
});
ReactDOM.unmountComponentAtNode(this.tooltipContainer);
document.body.removeChild(this.tooltipContainer);
- window.removeEventListener('scroll', this._renderTooltip, true);
- },
+ window.removeEventListener('scroll', this.renderTooltip, true);
+ }
- _updatePosition(style) {
+ private updatePosition(style: {[key: string]: any}) {
const parentBox = this.parent.getBoundingClientRect();
let offset = 0;
if (parentBox.height > MIN_TOOLTIP_HEIGHT) {
@@ -91,16 +92,15 @@ export default createReactClass({
style.top = (parentBox.top - 2) + window.pageYOffset + offset;
style.left = 6 + parentBox.right + window.pageXOffset;
return style;
- },
+ }
- _renderTooltip: function() {
+ private renderTooltip() {
// Add the parent's position to the tooltips, so it's correctly
// positioned, also taking into account any window zoom
// NOTE: The additional 6 pixels for the left position, is to take account of the
// tooltips chevron
- const parent = ReactDOM.findDOMNode(this).parentNode;
- let style = {};
- style = this._updatePosition(style);
+ const parent = ReactDOM.findDOMNode(this).parentNode as Element;
+ const style = this.updatePosition({});
// Hide the entire container when not visible. This prevents flashing of the tooltip
// if it is not meant to be visible on first mount.
style.display = this.props.visible ? "block" : "none";
@@ -118,21 +118,21 @@ export default createReactClass({
);
// Render the tooltip manually, as we wish it not to be rendered within the parent
- this.tooltip = ReactDOM.render(tooltip, this.tooltipContainer);
+ this.tooltip = ReactDOM.render(tooltip, this.tooltipContainer);
// Tell the roomlist about us so it can manipulate us if it wishes
- dis.dispatch({
- action: 'view_tooltip',
+ dis.dispatch({
+ action: Action.ViewTooltip,
tooltip: this.tooltip,
parent: parent,
});
- },
+ }
- render: function() {
+ render() {
// Render a placeholder
return (
);
- },
-});
+ }
+}
diff --git a/src/components/views/settings/account/PhoneNumbers.js b/src/components/views/settings/account/PhoneNumbers.js
index ad2dabd8ae..02e995ac45 100644
--- a/src/components/views/settings/account/PhoneNumbers.js
+++ b/src/components/views/settings/account/PhoneNumbers.js
@@ -267,7 +267,7 @@ export default class PhoneNumbers extends React.Component {
label={_t("Phone Number")}
autoComplete="off"
disabled={this.state.verifying}
- prefix={phoneCountry}
+ prefixComponent={phoneCountry}
value={this.state.newPhoneNumber}
onChange={this._onChangeNewPhoneNumber}
/>
diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts
index a2f9c3efe3..9cd9f7c9ba 100644
--- a/src/dispatcher/actions.ts
+++ b/src/dispatcher/actions.ts
@@ -38,5 +38,10 @@ export enum Action {
* Open the user settings. No additional payload information required.
*/
ViewUserSettings = "view_user_settings",
+
+ /**
+ * Sets the current tooltip
+ */
+ ViewTooltip = "view_tooltip",
}
diff --git a/src/dispatcher/payloads/ViewUserPayload.ts b/src/dispatcher/payloads/ViewUserPayload.ts
index ed602d4e24..d1f6db8968 100644
--- a/src/dispatcher/payloads/ViewUserPayload.ts
+++ b/src/dispatcher/payloads/ViewUserPayload.ts
@@ -17,6 +17,7 @@ limitations under the License.
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { ActionPayload } from "../payloads";
import { Action } from "../actions";
+import { Component } from "react";
export interface ViewUserPayload extends ActionPayload {
action: Action.ViewUser,
@@ -27,3 +28,19 @@ export interface ViewUserPayload extends ActionPayload {
*/
member?: RoomMember;
}
+
+export interface ViewTooltipPayload extends ActionPayload {
+ action: Action.ViewTooltip,
+
+ /*
+ * The tooltip to render. If it's null the tooltip will not be rendered
+ * We need the void type because of typescript headaches.
+ */
+ tooltip: null | void | Element | Component;
+
+ /*
+ * The parent under which to render the tooltip. Can be null to remove
+ * the parent type.
+ */
+ parent: null | Element
+}
diff --git a/yarn.lock b/yarn.lock
index 93118dab22..9253442b7c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1265,6 +1265,11 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339"
integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==
+"@types/lodash@^4.14.152":
+ version "4.14.152"
+ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.152.tgz#7e7679250adce14e749304cdb570969f77ec997c"
+ integrity sha512-Vwf9YF2x1GE3WNeUMjT5bTHa2DqgUo87ocdgTScupY2JclZ5Nn7W2RLM/N0+oreexUk8uaVugR81NnTY/jNNXg==
+
"@types/minimatch@*":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
@@ -1292,6 +1297,13 @@
dependencies:
"@types/node" "*"
+"@types/react-dom@^16.9.8":
+ version "16.9.8"
+ resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.8.tgz#fe4c1e11dfc67155733dfa6aa65108b4971cb423"
+ integrity sha512-ykkPQ+5nFknnlU6lDd947WbQ6TE3NNzbQAkInC2EKY1qeYdTKp7onFusmYZb+ityzx2YviqT6BXSu+LyWWJwcA==
+ dependencies:
+ "@types/react" "*"
+
"@types/react@*":
version "16.9.35"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.35.tgz#a0830d172e8aadd9bd41709ba2281a3124bbd368"