From ddb67ff44e72ef5ccba2ba8274a88e6e3740fed0 Mon Sep 17 00:00:00 2001 From: George Gevoian Date: Mon, 24 Jan 2022 12:38:32 -0800 Subject: [PATCH] (core) Make new account page mobile-friendly Summary: Tweaks CSS of account page, ApiKey and MFAConfig to work better on narrow-screen devices. Test Plan: Tested manually. Reviewers: paulfitz Reviewed By: paulfitz Subscribers: dsagal Differential Revision: https://phab.getgrist.com/D3234 --- app/client/ui/AccountPage.ts | 36 +++++++++++++++++++++++++++++------- app/client/ui/ApiKey.ts | 14 +++++++++++--- app/client/ui/MFAConfig.ts | 36 +++++++++++++++++------------------- 3 files changed, 57 insertions(+), 29 deletions(-) diff --git a/app/client/ui/AccountPage.ts b/app/client/ui/AccountPage.ts index 574aa27e..9e7243a2 100644 --- a/app/client/ui/AccountPage.ts +++ b/app/client/ui/AccountPage.ts @@ -53,7 +53,10 @@ export class AccountPage extends Disposable { return domComputed(this._userObs, (user) => user && ( cssContainer(cssAccountPage( cssHeader('Account settings'), - cssDataRow(cssSubHeader('Email'), user.email), + cssDataRow( + cssSubHeader('Email'), + cssEmail(user.email), + ), cssDataRow( cssSubHeader('Name'), domComputed(this._isEditingName, (isEditing) => ( @@ -64,14 +67,16 @@ export class AccountPage extends Disposable { save: (val) => this._isNameValid.get() && this._updateUserName(val), close: () => { this._isEditingName.set(false); this._nameEdit.set(''); }, }, + { size: '5' }, // Lower size so that input can shrink below ~152px. dom.on('input', (_ev, el) => this._nameEdit.set(el.value)), + cssFlexGrow.cls(''), ), cssTextBtn( cssIcon('Settings'), 'Save', // No need to save on 'click'. The transient input already does it on close. ), ] : [ - user.name, + cssName(user.name), cssTextBtn( cssIcon('Settings'), 'Edit', dom.on('click', () => this._isEditingName.set(true)), @@ -85,7 +90,7 @@ export class AccountPage extends Disposable { cssHeader('Password & Security'), cssDataRow( cssSubHeader('Login Method'), - user.loginMethod, + cssLoginMethod(user.loginMethod), user.loginMethod === 'Email + Password' ? cssTextBtn( cssIcon('Settings'), 'Reset', dom.on('click', () => confirmPwdResetModal(user.email)), @@ -111,6 +116,7 @@ export class AccountPage extends Disposable { onCreate: () => this._createApiKey(), onDelete: () => this._deleteApiKey(), anonymous: false, + inputArgs: [{ size: '5' }], // Lower size so that input can shrink below ~152px. }) )), ), @@ -209,7 +215,7 @@ const cssHeader = styled('div', ` const cssAccountPage = styled('div', ` max-width: 600px; - padding: 32px 64px 24px 64px; + padding: 16px; `); const cssDataRow = styled('div', ` @@ -226,7 +232,7 @@ const cssSubHeaderFullWidth = styled('div', ` `); const cssSubHeader = styled(cssSubHeaderFullWidth, ` - width: 110px; + min-width: 110px; `); const cssContent = styled('div', ` @@ -237,12 +243,12 @@ const cssTextBtn = styled('button', ` font-size: ${vars.mediumFontSize}; color: ${colors.lightGreen}; cursor: pointer; - margin-left: auto; + margin-left: 16px; background-color: transparent; border: none; padding: 0; text-align: left; - width: 90px; + min-width: 90px; &:hover { color: ${colors.darkGreen}; @@ -266,3 +272,19 @@ const cssDescription = styled('div', ` color: #8a8a8a; font-size: 13px; `); + +const cssFlexGrow = styled('div', ` + flex-grow: 1; +`); + +const cssName = styled(cssFlexGrow, ` + word-break: break-word; +`); + +const cssEmail = styled('div', ` + word-break: break-word; +`); + +const cssLoginMethod = styled(cssFlexGrow, ` + word-break: break-word; +`); diff --git a/app/client/ui/ApiKey.ts b/app/client/ui/ApiKey.ts index c5e6902f..59e99ad6 100644 --- a/app/client/ui/ApiKey.ts +++ b/app/client/ui/ApiKey.ts @@ -2,7 +2,7 @@ import * as billingPageCss from 'app/client/ui/BillingPageCss'; import { basicButton } from 'app/client/ui2018/buttons'; import { confirmModal } from 'app/client/ui2018/modals'; -import { Disposable, dom, makeTestId, Observable, observable, styled } from "grainjs"; +import { Disposable, dom, IDomArgs, makeTestId, Observable, observable, styled } from "grainjs"; interface IWidgetOptions { apiKey: Observable; @@ -11,6 +11,7 @@ interface IWidgetOptions { anonymous?: boolean; // Configure appearance and available options for anonymous use. // When anonymous, no modifications are permitted to profile information. // TODO: add browser test for this option. + inputArgs?: IDomArgs; } const testId = makeTestId('test-apikey-'); @@ -27,6 +28,7 @@ export class ApiKey extends Disposable { private _onDeleteCB: () => Promise; private _onCreateCB: () => Promise; private _anonymous: boolean; + private _inputArgs: IDomArgs; private _loading = observable(false); constructor(options: IWidgetOptions) { @@ -35,6 +37,7 @@ export class ApiKey extends Disposable { this._onDeleteCB = options.onDelete; this._onCreateCB = options.onCreate; this._anonymous = Boolean(options.anonymous); + this._inputArgs = options.inputArgs ?? []; } public buildDom() { @@ -42,8 +45,13 @@ export class ApiKey extends Disposable { dom.maybe(this._apiKey, (apiKey) => dom('div', cssRow( cssInput( - {readonly: true, value: this._apiKey.get()}, testId('key'), - dom.on('click', (ev, el) => el.select()) + { + readonly: true, + value: this._apiKey.get(), + }, + testId('key'), + dom.on('click', (ev, el) => el.select()), + this._inputArgs ), cssTextBtn( cssBillingIcon('Remove'), 'Remove', diff --git a/app/client/ui/MFAConfig.ts b/app/client/ui/MFAConfig.ts index a3e44591..5ee922d3 100644 --- a/app/client/ui/MFAConfig.ts +++ b/app/client/ui/MFAConfig.ts @@ -6,8 +6,8 @@ import {colors, vars} from 'app/client/ui2018/cssVars'; import {icon} from 'app/client/ui2018/icons'; import {cssLink} from 'app/client/ui2018/links'; import {loadingSpinner} from 'app/client/ui2018/loaders'; -import {cssModalBody, cssModalTitle, IModalControl, modal, - cssModalButtons as modalButtons} from 'app/client/ui2018/modals'; +import {cssModalBody, cssModalTitle, cssModalWidth, IModalControl, + modal, cssModalButtons as modalButtons} from 'app/client/ui2018/modals'; import {ApiError} from 'app/common/ApiError'; import {FullUser} from 'app/common/LoginSessionAPI'; import {AuthMethod, ChallengeRequired, UserMFAPreferences} from 'app/common/UserAPI'; @@ -85,7 +85,7 @@ export class MFAConfig extends Disposable { private _buildButtons() { return cssButtons( dom.domComputed(this._mfaPrefs, mfaPrefs => { - if (!mfaPrefs) { return cssCenteredDiv(cssSmallLoadingSpinner()); } + if (!mfaPrefs) { return cssSmallSpinner(cssSmallLoadingSpinner()); } const {isSmsMfaEnabled, isSoftwareTokenMfaEnabled, phoneNumber} = mfaPrefs; return [ @@ -282,7 +282,7 @@ export class MFAConfig extends Disposable { } } }), - cssModal.cls(''), + cssModalWidth('fixed-wide'), ]; }); } @@ -339,11 +339,11 @@ export class MFAConfig extends Disposable { .catch(reportError) .finally(() => ctl.close()); - return cssCenteredDivFixedHeight(loadingSpinner()); + return cssSpinner(loadingSpinner()); } } }), - cssModal.cls(''), + cssModalWidth('fixed-wide'), ]; }); } @@ -371,7 +371,7 @@ export class MFAConfig extends Disposable { dom.domComputed(securityStep, (step) => { switch (step) { case 'loading': { - return cssCenteredDivFixedHeight(loadingSpinner()); + return cssSpinner(loadingSpinner()); } case 'password': { let formElement: HTMLFormElement; @@ -510,7 +510,7 @@ export class MFAConfig extends Disposable { dom.autoDispose(resendingListener), dom.autoDispose(multiHolder), dom.domComputed(isResendingCode, isLoading => { - if (isLoading) { return cssCenteredDivFixedHeight(loadingSpinner()); } + if (isLoading) { return cssSpinner(loadingSpinner()); } return [ cssModalTitle('Almost there!', testId('title')), @@ -618,7 +618,7 @@ export class MFAConfig extends Disposable { dom.autoDispose(pending.addListener(isPending => isPending && errorObs.set(null))), dom.autoDispose(holder), dom.domComputed(qrCode, code => { - if (code === null) { return cssCenteredDivFixedHeight(loadingSpinner()); } + if (code === null) { return cssSpinner(loadingSpinner()); } return [ cssModalTitle('Configure authenticator app', testId('title')), @@ -748,7 +748,7 @@ export class MFAConfig extends Disposable { dom.autoDispose(resendingListener), dom.autoDispose(multiHolder), dom.domComputed(isResendingCode, isLoading => { - if (isLoading) { return cssCenteredDivFixedHeight(loadingSpinner()); } + if (isLoading) { return cssSpinner(loadingSpinner()); } return [ cssModalTitle('Confirm your phone', testId('title')), @@ -939,14 +939,13 @@ const cssBackBtn = styled(cssTextBtn, ` `); const cssAuthMethods = styled('div', ` - display: flex; - flex-direction: column; + display: grid; + grid-auto-rows: 1fr; margin-top: 16px; gap: 8px; `); const cssAuthMethod = styled('div', ` - height: 120px; border: 1px solid ${colors.mediumGreyOpaque}; border-radius: 3px; cursor: pointer; @@ -965,7 +964,7 @@ const cssAuthMethodTitle = styled(cssIconAndText, ` const cssAuthMethodDesc = styled('div', ` color: #8a8a8a; - padding-left: 40px; + padding: 0px 16px 16px 40px; `); const cssInput = styled(input, ` @@ -992,10 +991,6 @@ const cssCodeInput = styled(cssInput, ` width: 200px; `); -const cssModal = styled('div', ` - width: 600px; -`); - const cssSmallLoadingSpinner = styled(loadingSpinner, ` width: ${spinnerSizePixels}; height: ${spinnerSizePixels}; @@ -1008,8 +1003,11 @@ const cssCenteredDiv = styled('div', ` align-items: center; `); -const cssCenteredDivFixedHeight = styled(cssCenteredDiv, ` +const cssSmallSpinner = cssCenteredDiv; + +const cssSpinner = styled(cssCenteredDiv, ` height: 200px; + min-width: 200px; `); const cssBoldSubHeading = styled('div', `