UX/UI: various d-otp improvements (#35993)

- auto submit after otp is filled on login form
- ensures input is not taking full width and we render the 1password
helper outside of the input
- ensures there's enough spacing between otp input and submit button
- changes autofocus option from autofocus to autoFocus, this is to have
parity with our autoFocus modifier
- prevents a white rectangle to appear while 1password is filling the
field

This is how it should look now:
<img width="638" height="206" alt="Screenshot 2025-11-12 at 16 48 16"
src="https://github.com/user-attachments/assets/bb367458-9959-4cae-a045-d9887ab60e71"
/>
This commit is contained in:
Joffrey JAFFEUX
2025-11-14 09:28:27 +01:00
committed by GitHub
parent 4793193c0b
commit d4313be485
11 changed files with 33 additions and 27 deletions
@@ -588,22 +588,6 @@ body.signup-page {
}
}
// Login page
.login-fullpage {
#second-factor {
input {
width: 100%;
padding: 0.75em 0.5em;
min-width: 250px;
box-shadow: none;
}
input:focus {
outline: none;
}
}
}
// Signup and invite page
.invite-page,
.signup-fullpage {
@@ -131,3 +131,10 @@ body.invite-page {
}
}
}
.totp-token {
flex-direction: column;
display: flex;
align-items: flex-start;
gap: 0.5rem;
}
@@ -2,6 +2,7 @@
display: flex;
align-items: center;
position: relative;
width: min-content;
}
.d-otp-group {
@@ -55,7 +56,7 @@
.d-otp-input {
position: absolute;
inset: 0;
width: 100%;
width: calc(100% + 40px);
height: 100%;
display: flex;
text-align: left;
@@ -72,6 +73,11 @@
font-size: var(--root-height);
font-family: monospace;
font-variant-numeric: tabular-nums;
clip-path: inset(0 40px 0 0);
&[data-com-onepassword-filled] {
background-clip: text;
}
}
@keyframes otp-cursor-blink {
@@ -19,7 +19,7 @@ const DEFAULT_SLOTS = 6;
* @property {object} Args
*
* @property {number} [Args.slots] - Number of OTP input slots to display (defaults to 6)
* @property {boolean} [Args.autofocus] - Whether to autofocus the input on mount (defaults to true)
* @property {boolean} [Args.autoFocus] - Whether to autoFocus the input on mount (defaults to true)
* @property {function(string): void} [Args.onChange] - Callback invoked whenever the OTP value changes
* @property {function(string): void} [Args.onFill] - Callback invoked when all OTP slots are filled
*
@@ -36,8 +36,8 @@ export default class DOTP extends Component {
return this.args.slots ?? DEFAULT_SLOTS;
}
get autofocus() {
return this.args.autofocus ?? true;
get autoFocus() {
return this.args.autoFocus ?? true;
}
get isFilled() {
@@ -163,7 +163,7 @@ export default class DOTP extends Component {
{{on "blur" this.onBlur}}
{{on "paste" this.onPaste}}
aria-label={{i18n "d_otp.screen_reader" count=this.slots}}
{{(if this.autofocus (modifier autoFocus))}}
{{(if this.autoFocus (modifier autoFocus))}}
...attributes
/>
</div>
@@ -125,6 +125,12 @@ export default class LocalLoginForm extends Component {
}
}
@action
filledSecondFactorToken(otp) {
this.args.secondFactorTokenChanged(otp);
this.args.login();
}
@action
handleForgotPassword(event) {
event?.preventDefault();
@@ -260,6 +266,7 @@ export default class LocalLoginForm extends Component {
{{on "keydown" this.loginOnEnter}}
{{on "focusin" this.scrollInputIntoView}}
@onChange={{fn (mut @secondFactorToken)}}
@onFill={{this.filledSecondFactorToken}}
@secondFactorMethod={{@secondFactorMethod}}
value={{@secondFactorToken}}
id="login-second-factor"
@@ -178,6 +178,11 @@ export default class LoginPageController extends Controller {
this.loginPassword = event.target.value;
}
@action
secondFactorTokenChanged(token) {
this.secondFactorToken = token;
}
@action
showNotActivated(props) {
this.modal.show(NotActivatedModal, { model: props });
@@ -96,6 +96,7 @@ export default <template>
@loginPasswordChanged={{@controller.loginPasswordChanged}}
@secondFactorMethod={{@controller.secondFactorMethod}}
@secondFactorToken={{@controller.secondFactorToken}}
@secondFactorTokenChanged={{@controller.secondFactorTokenChanged}}
@backupEnabled={{@controller.backupEnabled}}
@totpEnabled={{@controller.totpEnabled}}
@securityKeyAllowedCredentialIds={{@controller.securityKeyAllowedCredentialIds}}
@@ -98,7 +98,6 @@ acceptance("Signing In", function () {
.isEnabled("enables the login button");
await fillIn("#login-second-factor", "123456");
await click(".login-fullpage .btn-primary");
assert
.dom(".login-fullpage .btn-primary")
@@ -80,12 +80,12 @@ module("Integration | Component | DOTP", function (hooks) {
assert.strictEqual(this.value, "12");
});
test("@autofocus", async function (assert) {
test("@autoFocus", async function (assert) {
await render(<template><DOTP /></template>);
assert.dom(".d-otp-slot[data-index='0'].--is-focused").exists();
await render(<template><DOTP @autofocus={{false}} /></template>);
await render(<template><DOTP @autoFocus={{false}} /></template>);
assert.dom(".d-otp-slot[data-index='0'].--is-focused").doesNotExist();
});
@@ -53,7 +53,6 @@ describe "Discourse Connect Provider", type: :system do
totp = ROTP::TOTP.new(user_second_factor.data).now
find("#login-second-factor").fill_in(with: totp)
login_form.click_login
expect(page).to have_current_path(
/#{Regexp.escape(return_url)}\?sso=.*&sig=[0-9a-f]+/,
-2
View File
@@ -327,7 +327,6 @@ shared_examples "login scenarios" do
totp = ROTP::TOTP.new(user_second_factor.data).now
find("#login-second-factor").fill_in(with: totp)
login_form.click_login
expect(page).to have_css(".header-dropdown-toggle.current-user")
end
@@ -342,7 +341,6 @@ shared_examples "login scenarios" do
totp = ROTP::TOTP.new(user_second_factor.data).now
find("#login-second-factor").fill_in(with: totp)
login_form.click_login
expect(page).to have_current_path("/about")
end