mirror of
https://github.com/element-hq/element-web.git
synced 2025-12-07 01:21:02 +00:00
Compare commits
2 Commits
hs/better-
...
hs/a11y-up
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3f43e1060 | ||
|
|
856a35175f |
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024, 2025 New Vector Ltd.
|
||||
Copyright 2019-2023 The Matrix.org Foundation C.I.C
|
||||
Copyright 2017-2019 New Vector Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
@@ -217,7 +217,7 @@ textarea {
|
||||
}
|
||||
|
||||
input[type="text"]:focus,
|
||||
input[type="password"]:focus,
|
||||
:not(.mx_ChangePasswordForm input) > input[type="password"],
|
||||
textarea:focus {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
@@ -592,6 +592,7 @@ legend {
|
||||
*/
|
||||
.mx_Dialog
|
||||
button:not(
|
||||
.mx_ChangePasswordForm button,
|
||||
.mx_EncryptionUserSettingsTab button,
|
||||
.mx_UserProfileSettings button,
|
||||
.mx_ShareDialog button,
|
||||
@@ -620,6 +621,7 @@ legend {
|
||||
|
||||
.mx_Dialog
|
||||
button:not(
|
||||
.mx_ChangePasswordForm button,
|
||||
.mx_Dialog_nonDialogButton,
|
||||
[class|="maplibregl"],
|
||||
.mx_AccessibleButton,
|
||||
@@ -634,6 +636,7 @@ legend {
|
||||
|
||||
.mx_Dialog
|
||||
button:not(
|
||||
.mx_ChangePasswordForm button,
|
||||
.mx_Dialog_nonDialogButton,
|
||||
[class|="maplibregl"],
|
||||
.mx_AccessibleButton,
|
||||
@@ -653,6 +656,7 @@ legend {
|
||||
.mx_Dialog input[type="submit"].mx_Dialog_primary,
|
||||
.mx_Dialog_buttons
|
||||
button:not(
|
||||
.mx_ChangePasswordForm button,
|
||||
.mx_Dialog_nonDialogButton,
|
||||
.mx_AccessibleButton,
|
||||
.mx_UserProfileSettings button,
|
||||
@@ -672,6 +676,7 @@ legend {
|
||||
.mx_Dialog input[type="submit"].danger,
|
||||
.mx_Dialog_buttons
|
||||
button.danger:not(
|
||||
.mx_ChangePasswordForm button,
|
||||
.mx_Dialog_nonDialogButton,
|
||||
.mx_AccessibleButton,
|
||||
.mx_UserProfileSettings button,
|
||||
@@ -694,6 +699,7 @@ legend {
|
||||
|
||||
.mx_Dialog
|
||||
button:not(
|
||||
.mx_ChangePasswordForm button,
|
||||
.mx_Dialog_nonDialogButton,
|
||||
[class|="maplibregl"],
|
||||
.mx_AccessibleButton,
|
||||
|
||||
@@ -6,15 +6,24 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type ComponentProps, PureComponent, type RefCallback, type RefObject } from "react";
|
||||
import React, { type RefCallback, type RefObject, useCallback, useMemo, useState } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import type { ZxcvbnResult } from "@zxcvbn-ts/core";
|
||||
import type { Score, ZxcvbnResult } from "@zxcvbn-ts/core";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import withValidation, { type IFieldState, type IValidationResult } from "../elements/Validation";
|
||||
import withValidation, { type IValidationResult } from "../elements/Validation";
|
||||
import { _t, _td, type TranslationKey } from "../../../languageHandler";
|
||||
import Field, { type IInputProps } from "../elements/Field";
|
||||
import { type IInputProps } from "../elements/Field";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { Field, Label, PasswordInput, Progress } from "@vector-im/compound-web";
|
||||
|
||||
const SCORE_TINT: Record<Score, "red" | "orange" | "lime" | "green"> ={
|
||||
"0": "red",
|
||||
"1": "red",
|
||||
"2": "orange",
|
||||
"3": "lime",
|
||||
"4": "green"
|
||||
};
|
||||
|
||||
interface IProps extends Omit<IInputProps, "onValidate" | "element"> {
|
||||
autoFocus?: boolean;
|
||||
@@ -22,43 +31,44 @@ interface IProps extends Omit<IInputProps, "onValidate" | "element"> {
|
||||
className?: string;
|
||||
minScore: 0 | 1 | 2 | 3 | 4;
|
||||
value: string;
|
||||
fieldRef?: RefCallback<Field> | RefObject<Field>;
|
||||
fieldRef?: RefCallback<HTMLInputElement> | RefObject<HTMLInputElement>;
|
||||
// Additional strings such as a username used to catch bad passwords
|
||||
userInputs?: string[];
|
||||
|
||||
label: TranslationKey;
|
||||
labelEnterPassword: TranslationKey;
|
||||
labelStrongPassword: TranslationKey;
|
||||
labelAllowedButUnsafe: TranslationKey;
|
||||
tooltipAlignment?: ComponentProps<typeof Field>["tooltipAlignment"];
|
||||
labelEnterPassword?: TranslationKey;
|
||||
labelStrongPassword?: TranslationKey;
|
||||
labelAllowedButUnsafe?: TranslationKey;
|
||||
// tooltipAlignment?: ComponentProps<typeof Field>["tooltipAlignment"];
|
||||
|
||||
onChange(ev: React.FormEvent<HTMLElement>): void;
|
||||
onValidate?(result: IValidationResult): void;
|
||||
}
|
||||
|
||||
class PassphraseField extends PureComponent<IProps> {
|
||||
public static defaultProps = {
|
||||
label: _td("common|password"),
|
||||
labelEnterPassword: _td("auth|password_field_label"),
|
||||
labelStrongPassword: _td("auth|password_field_strong_label"),
|
||||
labelAllowedButUnsafe: _td("auth|password_field_weak_label"),
|
||||
};
|
||||
const DEFAULT_PROPS = {
|
||||
label: _td("common|password"),
|
||||
labelEnterPassword: _td("auth|password_field_label"),
|
||||
labelStrongPassword: _td("auth|password_field_strong_label"),
|
||||
labelAllowedButUnsafe: _td("auth|password_field_weak_label"),
|
||||
};
|
||||
|
||||
public readonly validate = withValidation<this, ZxcvbnResult | null>({
|
||||
const PassphraseField: React.FC<IProps> = (props) => {
|
||||
const { labelEnterPassword, userInputs, minScore, label, labelStrongPassword, labelAllowedButUnsafe, className, id, fieldRef, autoFocus, onChange, onValidate} = {...DEFAULT_PROPS, ...props};
|
||||
const validateFn = useMemo(() => withValidation<{}, ZxcvbnResult | null>({
|
||||
description: function (complexity) {
|
||||
const score = complexity ? complexity.score : 0;
|
||||
return <progress className="mx_PassphraseField_progress" max={4} value={score} />;
|
||||
return <Progress tint={SCORE_TINT[score]} size="sm" value={score} max={4} />
|
||||
},
|
||||
deriveData: async ({ value }): Promise<ZxcvbnResult | null> => {
|
||||
if (!value) return null;
|
||||
const { scorePassword } = await import("../../../utils/PasswordScorer");
|
||||
return scorePassword(MatrixClientPeg.get(), value, this.props.userInputs);
|
||||
return scorePassword(MatrixClientPeg.get(), value, userInputs);
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test: ({ value, allowEmpty }) => allowEmpty || !!value,
|
||||
invalid: () => _t(this.props.labelEnterPassword),
|
||||
invalid: () => _t(labelEnterPassword),
|
||||
},
|
||||
{
|
||||
key: "complexity",
|
||||
@@ -66,7 +76,7 @@ class PassphraseField extends PureComponent<IProps> {
|
||||
if (!value || !complexity) {
|
||||
return false;
|
||||
}
|
||||
const safe = complexity.score >= this.props.minScore;
|
||||
const safe = complexity.score >= minScore;
|
||||
const allowUnsafe = SdkConfig.get("dangerously_allow_unsafe_and_insecure_passwords");
|
||||
return allowUnsafe || safe;
|
||||
},
|
||||
@@ -74,10 +84,10 @@ class PassphraseField extends PureComponent<IProps> {
|
||||
// Unsafe passwords that are valid are only possible through a
|
||||
// configuration flag. We'll print some helper text to signal
|
||||
// to the user that their password is allowed, but unsafe.
|
||||
if (complexity && complexity.score >= this.props.minScore) {
|
||||
return _t(this.props.labelStrongPassword);
|
||||
if (complexity && complexity.score >= minScore) {
|
||||
return _t(labelStrongPassword);
|
||||
}
|
||||
return _t(this.props.labelAllowedButUnsafe);
|
||||
return _t(labelAllowedButUnsafe);
|
||||
},
|
||||
invalid: function (complexity) {
|
||||
if (!complexity) {
|
||||
@@ -89,33 +99,26 @@ class PassphraseField extends PureComponent<IProps> {
|
||||
},
|
||||
],
|
||||
memoize: true,
|
||||
});
|
||||
}), [labelEnterPassword, userInputs, minScore, labelStrongPassword, labelAllowedButUnsafe]);
|
||||
const [feedback, setFeedback]= useState<string|JSX.Element>();
|
||||
|
||||
public onValidate = async (fieldState: IFieldState): Promise<IValidationResult> => {
|
||||
const result = await this.validate(fieldState);
|
||||
if (this.props.onValidate) {
|
||||
this.props.onValidate(result);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const onInputChange = useCallback<React.ChangeEventHandler<HTMLInputElement>>((ev) => {
|
||||
onChange(ev);
|
||||
validateFn({
|
||||
value: ev.target.value,
|
||||
focused: true,
|
||||
}).then((v) => {
|
||||
setFeedback(v.feedback);
|
||||
onValidate?.(v);
|
||||
});
|
||||
}, [validateFn]);
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<Field
|
||||
id={this.props.id}
|
||||
autoFocus={this.props.autoFocus}
|
||||
className={classNames("mx_PassphraseField", this.props.className)}
|
||||
ref={this.props.fieldRef}
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
label={_t(this.props.label)}
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
onValidate={this.onValidate}
|
||||
tooltipAlignment={this.props.tooltipAlignment}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <Field id={id} name="password" className={classNames("mx_PassphraseField", className)}>
|
||||
<Label>{_t(label)}</Label>
|
||||
<PasswordInput ref={fieldRef} autoFocus={autoFocus} onChange={onInputChange} />
|
||||
{feedback}
|
||||
</Field>
|
||||
}
|
||||
|
||||
export default PassphraseField;
|
||||
|
||||
@@ -9,16 +9,12 @@ Please see LICENSE files in the repository root for full details.
|
||||
import React from "react";
|
||||
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import Field from "../elements/Field";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import AccessibleButton, { type AccessibleButtonKind } from "../elements/AccessibleButton";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import withValidation, { type IFieldState, type IValidationResult } from "../elements/Validation";
|
||||
import { UserFriendlyError, _t, _td } from "../../../languageHandler";
|
||||
import Modal from "../../../Modal";
|
||||
import PassphraseField from "../auth/PassphraseField";
|
||||
import { PASSWORD_MIN_SCORE } from "../auth/RegistrationForm";
|
||||
import SetEmailDialog from "../dialogs/SetEmailDialog";
|
||||
import { Root, Field as CpdField, PasswordInput, Label, InlineSpinner, HelpMessage, Button } from "@vector-im/compound-web";
|
||||
import PassphraseField from "../auth/PassphraseField";
|
||||
|
||||
const FIELD_OLD_PASSWORD = "field_old_password";
|
||||
const FIELD_NEW_PASSWORD = "field_new_password";
|
||||
@@ -34,19 +30,11 @@ enum Phase {
|
||||
interface IProps {
|
||||
onFinished: (outcome: { didSetEmail?: boolean }) => void;
|
||||
onError: (error: Error) => void;
|
||||
rowClassName?: string;
|
||||
buttonClassName?: string;
|
||||
buttonKind?: AccessibleButtonKind;
|
||||
buttonLabel?: string;
|
||||
confirm?: boolean;
|
||||
// Whether to autoFocus the new password input
|
||||
autoFocusNewPasswordInput?: boolean;
|
||||
className?: string;
|
||||
shouldAskForEmail?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
fieldValid: Partial<Record<FieldType, boolean>>;
|
||||
fieldValid: Partial<Record<FieldType, IValidationResult>>;
|
||||
phase: Phase;
|
||||
oldPassword: string;
|
||||
newPassword: string;
|
||||
@@ -54,15 +42,13 @@ interface IState {
|
||||
}
|
||||
|
||||
export default class ChangePassword extends React.Component<IProps, IState> {
|
||||
private [FIELD_OLD_PASSWORD]: Field | null = null;
|
||||
private [FIELD_NEW_PASSWORD]: Field | null = null;
|
||||
private [FIELD_NEW_PASSWORD_CONFIRM]: Field | null = null;
|
||||
private [FIELD_OLD_PASSWORD]: HTMLInputElement | null = null;
|
||||
private [FIELD_NEW_PASSWORD]: HTMLInputElement | null = null;
|
||||
private [FIELD_NEW_PASSWORD_CONFIRM]: HTMLInputElement | null = null;
|
||||
|
||||
public static defaultProps: Partial<IProps> = {
|
||||
onFinished() {},
|
||||
onError() {},
|
||||
|
||||
confirm: true,
|
||||
};
|
||||
|
||||
public constructor(props: IProps) {
|
||||
@@ -100,15 +86,7 @@ export default class ChangePassword extends React.Component<IProps, IState> {
|
||||
cli.setPassword(authDict, newPassword, false)
|
||||
.then(
|
||||
() => {
|
||||
if (this.props.shouldAskForEmail) {
|
||||
return this.optionallySetEmail().then((confirmed) => {
|
||||
this.props.onFinished({
|
||||
didSetEmail: confirmed,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.props.onFinished({});
|
||||
}
|
||||
this.props.onFinished({});
|
||||
},
|
||||
(err) => {
|
||||
if (err instanceof Error) {
|
||||
@@ -149,17 +127,9 @@ export default class ChangePassword extends React.Component<IProps, IState> {
|
||||
}
|
||||
}
|
||||
|
||||
private optionallySetEmail(): Promise<boolean> {
|
||||
// Ask for an email otherwise the user has no way to reset their password
|
||||
const modal = Modal.createDialog(SetEmailDialog, {
|
||||
title: _t("auth|set_email_prompt"),
|
||||
});
|
||||
return modal.finished.then(([confirmed]) => !!confirmed);
|
||||
}
|
||||
|
||||
private markFieldValid(fieldID: FieldType, valid?: boolean): void {
|
||||
private markFieldValid(fieldID: FieldType, result: IValidationResult): void {
|
||||
const { fieldValid } = this.state;
|
||||
fieldValid[fieldID] = valid;
|
||||
fieldValid[fieldID] = result;
|
||||
this.setState({
|
||||
fieldValid,
|
||||
});
|
||||
@@ -169,11 +139,16 @@ export default class ChangePassword extends React.Component<IProps, IState> {
|
||||
this.setState({
|
||||
oldPassword: ev.target.value,
|
||||
});
|
||||
this.onOldPasswordValidate({
|
||||
value: ev.target.value,
|
||||
focused: true,
|
||||
allowEmpty: true,
|
||||
});
|
||||
};
|
||||
|
||||
private onOldPasswordValidate = async (fieldState: IFieldState): Promise<IValidationResult> => {
|
||||
const result = await this.validateOldPasswordRules(fieldState);
|
||||
this.markFieldValid(FIELD_OLD_PASSWORD, result.valid);
|
||||
this.markFieldValid(FIELD_OLD_PASSWORD, result);
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -194,18 +169,24 @@ export default class ChangePassword extends React.Component<IProps, IState> {
|
||||
};
|
||||
|
||||
private onNewPasswordValidate = (result: IValidationResult): void => {
|
||||
this.markFieldValid(FIELD_NEW_PASSWORD, result.valid);
|
||||
this.markFieldValid(FIELD_NEW_PASSWORD, result);
|
||||
};
|
||||
|
||||
private onChangeNewPasswordConfirm = (ev: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
newPasswordConfirm: ev.target.value,
|
||||
});
|
||||
|
||||
this.onNewPasswordConfirmValidate({
|
||||
value: ev.target.value,
|
||||
focused: true,
|
||||
allowEmpty: true,
|
||||
});
|
||||
};
|
||||
|
||||
private onNewPasswordConfirmValidate = async (fieldState: IFieldState): Promise<IValidationResult> => {
|
||||
const result = await this.validatePasswordConfirmRules(fieldState);
|
||||
this.markFieldValid(FIELD_NEW_PASSWORD_CONFIRM, result.valid);
|
||||
this.markFieldValid(FIELD_NEW_PASSWORD_CONFIRM, result);
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -266,26 +247,25 @@ export default class ChangePassword extends React.Component<IProps, IState> {
|
||||
activeElement.blur();
|
||||
}
|
||||
|
||||
// Run all fields with stricter validation that no longer allows empty
|
||||
// values for required fields.
|
||||
await this.onOldPasswordValidate({
|
||||
value: this[FIELD_OLD_PASSWORD]?.value ?? null,
|
||||
allowEmpty: false,
|
||||
focused: true,
|
||||
});
|
||||
await this.onNewPasswordConfirmValidate({
|
||||
value: this[FIELD_NEW_PASSWORD_CONFIRM]?.value ?? null,
|
||||
allowEmpty: false,
|
||||
focused: true,
|
||||
});
|
||||
|
||||
const fieldIDsInDisplayOrder: FieldType[] = [
|
||||
FIELD_OLD_PASSWORD,
|
||||
FIELD_NEW_PASSWORD,
|
||||
FIELD_NEW_PASSWORD_CONFIRM,
|
||||
];
|
||||
|
||||
// Run all fields with stricter validation that no longer allows empty
|
||||
// values for required fields.
|
||||
for (const fieldID of fieldIDsInDisplayOrder) {
|
||||
const field = this[fieldID];
|
||||
if (!field) {
|
||||
continue;
|
||||
}
|
||||
// We must wait for these validations to finish before queueing
|
||||
// up the setState below so our setState goes in the queue after
|
||||
// all the setStates from these validate calls (that's how we
|
||||
// know they've finished).
|
||||
await field.validate({ allowEmpty: false });
|
||||
}
|
||||
|
||||
// Validation and state updates are async, so we need to wait for them to complete
|
||||
// first. Queue a `setState` callback and wait for it to resolve.
|
||||
await new Promise<void>((resolve) => this.setState({}, resolve));
|
||||
@@ -303,15 +283,16 @@ export default class ChangePassword extends React.Component<IProps, IState> {
|
||||
// Focus the first invalid field and show feedback in the stricter mode
|
||||
// that no longer allows empty values for required fields.
|
||||
invalidField.focus();
|
||||
invalidField.validate({ allowEmpty: false, focused: true });
|
||||
// TODO: HMM
|
||||
// invalidField.validate({ allowEmpty: false, focused: true });
|
||||
return false;
|
||||
}
|
||||
|
||||
private allFieldsValid(): boolean {
|
||||
return Object.values(this.state.fieldValid).every(Boolean);
|
||||
return Object.values(this.state.fieldValid).map(v => v.valid).every(Boolean);
|
||||
}
|
||||
|
||||
private findFirstInvalidField(fieldIDs: FieldType[]): Field | null {
|
||||
private findFirstInvalidField(fieldIDs: FieldType[]): HTMLInputElement | null {
|
||||
for (const fieldID of fieldIDs) {
|
||||
if (!this.state.fieldValid[fieldID] && this[fieldID]) {
|
||||
return this[fieldID];
|
||||
@@ -321,60 +302,56 @@ export default class ChangePassword extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const rowClassName = this.props.rowClassName;
|
||||
const buttonClassName = this.props.buttonClassName;
|
||||
const { fieldValid, phase } = this.state;
|
||||
|
||||
switch (this.state.phase) {
|
||||
switch (phase) {
|
||||
case Phase.Edit:
|
||||
return (
|
||||
<form className={this.props.className} onSubmit={this.onClickChange}>
|
||||
<div className={rowClassName}>
|
||||
<Field
|
||||
ref={(field) => (this[FIELD_OLD_PASSWORD] = field)}
|
||||
type="password"
|
||||
label={_t("auth|change_password_current_label")}
|
||||
value={this.state.oldPassword}
|
||||
onChange={this.onChangeOldPassword}
|
||||
onValidate={this.onOldPasswordValidate}
|
||||
/>
|
||||
</div>
|
||||
<div className={rowClassName}>
|
||||
<PassphraseField
|
||||
fieldRef={(field) => (this[FIELD_NEW_PASSWORD] = field)}
|
||||
type="password"
|
||||
label={_td("auth|change_password_new_label")}
|
||||
minScore={PASSWORD_MIN_SCORE}
|
||||
value={this.state.newPassword}
|
||||
autoFocus={this.props.autoFocusNewPasswordInput}
|
||||
onChange={this.onChangeNewPassword}
|
||||
onValidate={this.onNewPasswordValidate}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
<div className={rowClassName}>
|
||||
<Field
|
||||
ref={(field) => (this[FIELD_NEW_PASSWORD_CONFIRM] = field)}
|
||||
type="password"
|
||||
label={_t("auth|change_password_confirm_label")}
|
||||
value={this.state.newPasswordConfirm}
|
||||
onChange={this.onChangeNewPasswordConfirm}
|
||||
onValidate={this.onNewPasswordConfirmValidate}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
<AccessibleButton
|
||||
className={buttonClassName}
|
||||
kind={this.props.buttonKind}
|
||||
<Root className={"mx_ChangePasswordForm"} onSubmit={this.onClickChange}>
|
||||
<CpdField name={FIELD_OLD_PASSWORD}>
|
||||
<Label>
|
||||
{_t("auth|change_password_current_label")}
|
||||
</Label>
|
||||
<PasswordInput ref={(field) => (this[FIELD_OLD_PASSWORD] = field)} data-invalid={fieldValid[FIELD_OLD_PASSWORD]?.valid === false ? true : undefined} value={this.state.oldPassword} onChange={this.onChangeOldPassword} />
|
||||
{fieldValid[FIELD_OLD_PASSWORD]?.feedback && <HelpMessage>
|
||||
{fieldValid[FIELD_OLD_PASSWORD]?.feedback}
|
||||
</HelpMessage>}
|
||||
</CpdField>
|
||||
{ /* This is a compound field. */}
|
||||
<PassphraseField
|
||||
fieldRef={(field) => (this[FIELD_NEW_PASSWORD] = field)}
|
||||
type="password"
|
||||
label={_td("auth|change_password_new_label")}
|
||||
minScore={PASSWORD_MIN_SCORE}
|
||||
value={this.state.newPassword}
|
||||
onChange={this.onChangeNewPassword}
|
||||
onValidate={this.onNewPasswordValidate}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<CpdField name={FIELD_NEW_PASSWORD_CONFIRM}>
|
||||
<Label>
|
||||
{_t("auth|change_password_confirm_label")}
|
||||
</Label>
|
||||
<PasswordInput autoComplete="new-password" ref={(field) => (this[FIELD_NEW_PASSWORD_CONFIRM] = field)} data-invalid={fieldValid[FIELD_NEW_PASSWORD_CONFIRM]?.valid === false ? true : undefined} value={this.state.newPasswordConfirm} onChange={this.onChangeNewPasswordConfirm} />
|
||||
{fieldValid[FIELD_NEW_PASSWORD_CONFIRM]?.feedback && <HelpMessage>
|
||||
{fieldValid[FIELD_NEW_PASSWORD_CONFIRM]?.feedback}
|
||||
</HelpMessage>}
|
||||
</CpdField>
|
||||
<Button
|
||||
disabled={!this.allFieldsValid()}
|
||||
style={{width: "fit-content"}}
|
||||
onClick={this.onClickChange}
|
||||
kind="primary"
|
||||
size="sm"
|
||||
>
|
||||
{this.props.buttonLabel || _t("auth|change_password_action")}
|
||||
</AccessibleButton>
|
||||
</form>
|
||||
</Button>
|
||||
</Root>
|
||||
);
|
||||
case Phase.Uploading:
|
||||
return (
|
||||
<div className="mx_Dialog_content">
|
||||
<Spinner />
|
||||
<InlineSpinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user