Skip to content

Commit

Permalink
Extract anchor input component (#907)
Browse files Browse the repository at this point in the history
* Extract anchor input component

This extracts the anchor input component (with input filter) from the
authenticate page, which allows reuse in both the login and anchor
prompt pages.

Also removes unnecessary anchor input CSS.

* 🤖 Selenium screenshots auto-update

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: David Aerne <meodai@gmail.com>
  • Loading branch information
3 people authored Sep 23, 2022
1 parent 08ca6cf commit 048541c
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 142 deletions.
Binary file modified screenshots/00-welcome-desktop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/00-welcome-mobile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
84 changes: 84 additions & 0 deletions src/frontend/src/components/anchorInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { html } from "lit";
import { createRef, ref } from "lit/directives/ref.js";
import { TemplateRef } from "../utils/templateRef";

/** A component for inputting an anchor number */
export const mkAnchorInput = (
inputId: string,
userNumber?: bigint
): TemplateRef<{ userNumberInput: HTMLInputElement }> => {
const divRef = createRef();
const userNumberInput = createRef();

// How we react on unexpected (i.e. non-digit) input
const onBadInput = () => {
const div = divRef.value;
if (div !== undefined && !div.classList.contains("flash-error")) {
div.classList.add("flash-error");
setTimeout(() => div.classList.remove("flash-error"), 2000);
}
};

const template = html` <div
${ref(divRef)}
class="l-stack c-input c-input--vip c-input--anchor"
>
<input
${ref(userNumberInput)}
type="text"
id="${inputId}"
placeholder="Enter anchor"
value="${userNumber !== undefined ? userNumber : ""}"
style="width: 100%;"
@input=${inputFilter(isDigits, onBadInput)}
@keydown=${inputFilter(isDigits, onBadInput)}
@keyup=${inputFilter(isDigits, onBadInput)}
@mousedown=${inputFilter(isDigits, onBadInput)}
@mouseup=${inputFilter(isDigits, onBadInput)}
@select=${inputFilter(isDigits, onBadInput)}
@contextmenu=${inputFilter(isDigits, onBadInput)}
@drop=${inputFilter(isDigits, onBadInput)}
@focusout=${inputFilter(isDigits, onBadInput)}
/>
<p
id="invalidAnchorMessage"
class="anchor-error-message is-hidden t-paragraph t-strong"
>
The Identity Anchor is not valid. Please try again.
</p>
</div>`;

return { template, refs: { userNumberInput } };
};

const isDigits = (c: string) => /^\d*\.?\d*$/.test(c);

/* Adds a filter to the input that only allows the given regex.
* For more info see https://stackoverflow.com/questions/469357/html-text-input-allow-only-numeric-input
*/
const inputFilter = (inputFilter: (c: string) => boolean, onBad: () => void) =>
function (
this: (HTMLInputElement | HTMLTextAreaElement) & {
oldValue: string;
oldSelectionStart: number | null;
oldSelectionEnd: number | null;
}
) {
if (inputFilter(this.value)) {
this.oldValue = this.value;
this.oldSelectionStart = this.selectionStart;
this.oldSelectionEnd = this.selectionEnd;
} else {
onBad();

if (Object.prototype.hasOwnProperty.call(this, "oldValue")) {
this.value = this.oldValue;
if (this.oldSelectionStart !== null && this.oldSelectionEnd !== null) {
this.setSelectionRange(this.oldSelectionStart, this.oldSelectionEnd);
}
} else {
this.value = "";
}
}
};
91 changes: 8 additions & 83 deletions src/frontend/src/flows/authenticate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
setUserNumber,
} from "../../utils/userNumber";
import { withLoader } from "../../components/loader";
import { mkAnchorInput } from "../../components/anchorInput";
import { AuthenticatedConnection, Connection } from "../../utils/iiConnection";
import { TemplateRef, renderTemplateRef } from "../../utils/templateRef";
import { ref, createRef } from "lit/directives/ref.js";
Expand Down Expand Up @@ -46,8 +47,8 @@ const pageContent = (
const onRecoverClick = () => useRecovery(connection, readUserNumber());

const authorizeButton = createRef();
const userNumberInput = createRef();
const registerButton = createRef();
const anchorInput = mkAnchorInput("userNumberInput", userNumber);

const template = html` <div class="l-container c-card c-card--highlight">
<!-- The title is hidden but used for accessibility -->
Expand All @@ -69,23 +70,7 @@ const pageContent = (
</p>
</div>
<div class="l-stack c-input c-input--vip c-input--anchor">
<input
${ref(userNumberInput)}
type="text"
id="userNumberInput"
placeholder="Enter anchor"
value="${userNumber !== undefined ? userNumber : ""}"
style="width: 100%;"
/>
<p
id="invalidAnchorMessage"
class="anchor-error-message is-hidden t-paragraph t-strong"
>
The Identity Anchor is not valid. Please try again.
</p>
</div>
${anchorInput.template}
<button ${ref(authorizeButton)} id="authorizeButton" class="c-button">
Authorize
Expand Down Expand Up @@ -123,7 +108,11 @@ const pageContent = (

return {
template,
refs: { authorizeButton, userNumberInput, registerButton },
refs: {
authorizeButton,
userNumberInput: anchorInput.refs.userNumberInput,
registerButton,
},
};
};

Expand Down Expand Up @@ -312,8 +301,6 @@ export const displayPage = (
container
);

setInputFilter(ret.userNumberInput, (c) => /^\d*\.?\d*$/.test(c));

return ret;
};

Expand Down Expand Up @@ -349,65 +336,3 @@ const readUserNumber = () => {
// get rid of null, we use undefined for 'not set'
return parsedUserNumber === null ? undefined : parsedUserNumber;
};

/* Adds a filter to the input that only allows the given regex.
* For more info see https://stackoverflow.com/questions/469357/html-text-input-allow-only-numeric-input
*/
function setInputFilter(
textbox: HTMLInputElement,
inputFilter: (value: string) => boolean
): void {
[
"input",
"keydown",
"keyup",
"mousedown",
"mouseup",
"select",
"contextmenu",
"drop",
"focusout",
].forEach(function (event) {
textbox.addEventListener(
event,
function (
this: (HTMLInputElement | HTMLTextAreaElement) & {
oldValue: string;
oldSelectionStart: number | null;
oldSelectionEnd: number | null;
}
) {
if (inputFilter(this.value)) {
this.oldValue = this.value;
this.oldSelectionStart = this.selectionStart;
this.oldSelectionEnd = this.selectionEnd;
} else {
const parent = textbox.parentNode as HTMLElement;

if (
parent !== undefined &&
!parent.classList.contains("flash-error")
) {
parent.classList.add("flash-error");
setTimeout(() => parent.classList.remove("flash-error"), 2000);
}

if (Object.prototype.hasOwnProperty.call(this, "oldValue")) {
this.value = this.oldValue;
if (
this.oldSelectionStart !== null &&
this.oldSelectionEnd !== null
) {
this.setSelectionRange(
this.oldSelectionStart,
this.oldSelectionEnd
);
}
} else {
this.value = "";
}
}
}
);
});
}
26 changes: 13 additions & 13 deletions src/frontend/src/flows/login/unknownAnchor.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import { render, html } from "lit";
import { html } from "lit";
import { Connection } from "../../utils/iiConnection";
import { parseUserNumber, setUserNumber } from "../../utils/userNumber";
import { withLoader } from "../../components/loader";
import { icLogo } from "../../components/icons";
import { navbar } from "../../components/navbar";
import { footer } from "../../components/footer";
import { mkAnchorInput } from "../../components/anchorInput";
import { useRecovery } from "../recovery/useRecovery";
import { apiResultToLoginFlowResult, LoginFlowResult } from "./flowResult";
import { addRemoteDevice } from "../addDevice/welcomeView";
import { registerIfAllowed } from "../../utils/registerAllowedCheck";
import { TemplateRef, renderTemplateRef } from "../../utils/templateRef";

const pageContent = () => html`
const pageContent = (): TemplateRef<{ userNumberInput: HTMLInputElement }> => {
const anchorInput = mkAnchorInput("registerUserNumber");
const template = html`
<section class="l-container c-card c-card--highlight" aria-label="Authentication">
<div class="c-logo">${icLogo}</div>
<article class="l-stack">
Expand All @@ -19,12 +23,7 @@ const pageContent = () => html`
<p class="t-lead">Provide an Identity Anchor to authenticate.</p>
<hgroup>
<div class="l-stack">
<input
type="text"
class="c-input c-input--vip"
id="registerUserNumber"
placeholder="Enter Anchor"
/>
${anchorInput.template}
<button type="button" id="loginButton" class="c-button">
Authenticate
</button>
Expand Down Expand Up @@ -55,13 +54,16 @@ const pageContent = () => html`
</section>
${footer}`;

return { ...anchorInput, template };
};

export const loginUnknownAnchor = async (
connection: Connection
): Promise<LoginFlowResult> => {
const container = document.getElementById("pageContent") as HTMLElement;
render(pageContent(), container);
const { userNumberInput } = renderTemplateRef(pageContent(), container);
return new Promise((resolve, reject) => {
initLogin(connection, resolve);
initLogin(connection, { userNumberInput }, resolve);
initLinkDevice(connection);
initRegister(connection, resolve, reject);
initRecovery(connection);
Expand Down Expand Up @@ -98,11 +100,9 @@ const initRecovery = (connection: Connection) => {

const initLogin = (
connection: Connection,
{ userNumberInput }: { userNumberInput: HTMLInputElement },
resolve: (res: LoginFlowResult) => void
) => {
const userNumberInput = document.getElementById(
"registerUserNumber"
) as HTMLInputElement;
const loginButton = document.getElementById(
"loginButton"
) as HTMLButtonElement;
Expand Down
52 changes: 27 additions & 25 deletions src/frontend/src/flows/promptUserNumber.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,44 @@
import { html, render } from "lit";
import { html } from "lit";
import { TemplateRef, renderTemplateRef } from "../utils/templateRef";
import { parseUserNumber } from "../utils/userNumber";
import { mkAnchorInput } from "../components/anchorInput";

const pageContent = (title: string, userNumber: bigint | null) => html`
<div class="l-container c-card c-card--highlight">
<hgroup>
<h1 class="t-title t-title--main">${title}</h1>
<p class="t-lead">Please provide an Identity Anchor.</p>
</hgroup>
<input
type="text"
id="userNumberInput"
class="c-input c-input--vip"
placeholder="Enter Anchor"
value=${userNumber ?? ""}
/>
<div class="c-button-group">
<button id="userNumberCancel" class="c-button c-button--secondary">
Cancel
</button>
<button id="userNumberContinue" class="c-button">Continue</button>
const pageContent = (
title: string,
userNumber: bigint | null
): TemplateRef<{ userNumberInput: HTMLInputElement }> => {
const anchorInput = mkAnchorInput("userNumberInput", userNumber ?? undefined);
const template = html`
<div class="l-container c-card c-card--highlight">
<hgroup>
<h1 class="t-title t-title--main">${title}</h1>
<p class="t-lead">Please provide an Identity Anchor.</p>
</hgroup>
${anchorInput.template}
<div class="c-button-group">
<button id="userNumberCancel" class="c-button c-button--secondary">
Cancel
</button>
<button id="userNumberContinue" class="c-button">Continue</button>
</div>
</div>
</div>
`;
`;

return { ...anchorInput, template };
};

export const promptUserNumber = async (
title: string,
userNumber: bigint | null
): Promise<bigint | null> =>
new Promise((resolve) => {
const container = document.getElementById("pageContent") as HTMLElement;
render(pageContent(title, userNumber), container);
const content = pageContent(title, userNumber);
const { userNumberInput } = renderTemplateRef(content, container);

const userNumberContinue = document.getElementById(
"userNumberContinue"
) as HTMLButtonElement;
const userNumberInput = document.getElementById(
"userNumberInput"
) as HTMLInputElement;

userNumberInput.onkeypress = (e) => {
// submit if user hits enter
Expand Down
23 changes: 2 additions & 21 deletions src/frontend/src/styles/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -1056,28 +1056,9 @@ a:hover,
display: block !important;
}

@keyframes flash-error {
0% {
background-image: linear-gradient(white, white),
linear-gradient(270deg, var(--rg-brand));
}
25% {
background-image: linear-gradient(white, white),
linear-gradient(270deg, var(--rg-brand-bruised));
}
75% {
background-image: linear-gradient(white, white),
linear-gradient(270deg, var(--rg-brand-bruised));
}
100% {
background-image: linear-gradient(white, white),
linear-gradient(270deg, var(--rg-brand));
}
}

.c-input--anchor.flash-error {
animation-name: flash-error;
animation-duration: 6s;
background-image: linear-gradient(white, white),
linear-gradient(270deg, var(--rg-brand-bruised));
}

.c-input--anchor.flash-error::after {
Expand Down

0 comments on commit 048541c

Please sign in to comment.