(core) Make mobile the default mode.

Summary:
- Make unsupported browser warning into an unobtrusive one-liner, similar in
  style to notifications.
- Move browser warning details into a support page, linked from "Learn more" link.
- Show different mobile and desktop warnings.
- Once dismissed, remember dismissal for a year rather than just for the session.
- Turn the Sign-In button (for anon users) into a menu (for the sake of exposing
  the Toggle Mobile Mode option)
- Improve styling of HomeIntro screens when on small screen.
- Flip the default for setting mobile viewport to true

Test Plan: Added minor unittest for localStorageBoolObs; fixed other affected tests.

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D2738
This commit is contained in:
Dmitry S 2021-02-25 11:11:59 -05:00
parent 31ffd21b4e
commit d8d1a91beb
8 changed files with 119 additions and 76 deletions

View File

@ -131,39 +131,58 @@ div.dev_warning {
#browser-check-problem {
display: none;
width: 100%;
height: 100%;
position: absolute;
z-index: 5000;
top: 0;
bottom: 0;
left: 0;
background: rgba(255, 255, 255, 0.9);
padding-top: 3em;
padding: 4px;
/* Copy common styles that are normally set from JS-generated CSS */
box-sizing: border-box;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";
font-size: 13px;
line-height: 16px;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
}
#browser-check-problem div.browser-check-wrapper {
position: absolute;
top: 30%;
width: 100%;
}
#browser-check-problem div.browser-check-message, #browser-check-problem div.browser-check-options {
margin: 0 auto;
max-width: 400px;
padding: 1em;
background: white;
}
#browser-check-problem div.browser-check-options {
text-align: center;
}
#browser-check-problem a {
display: inline-block;
background: white;
padding: 10px;
margin: 10px;
color: #16B378;
border: 1px solid #16B378;
.browser-check-wrapper {
margin: auto;
max-width: 600px;
padding: 8px 24px;
background-color: #040404;
border-radius: 4px;
border: none;
color: white;
box-shadow: 0 0 4px 0 white;
}
#browser-check-problem a:hover {
text-decoration: none;
color: #009058;
border: 1px solid #009058;
.browser-check-wrapper td {
vertical-align: middle;
padding: 8px 16px;
}
.browser-check-mobile {
display: none;
}
.browser-check-is-mobile .browser-check-mobile {
display: inline;
}
.browser-check-is-mobile .browser-check-desktop {
display: none;
}
.browser-check-wrapper a {
color: #16B378;
text-decoration: underline;
}
.browser-check-wrapper a:hover {
color: #b1ffe2;
}
.browser-check-close {
padding: 4px 8px;
border-radius: 4px;
background-color: #009058;
color: white;
cursor: pointer;
}
.browser-check-close:hover {
background-color: #16B378;
}

View File

@ -48,11 +48,15 @@ function createStorage(): Storage {
/**
* Helper to create a boolean observable whose state is stored in localStorage.
*
* Optionally, a default value of true will make the observable start off as true. Note that the
* same default value should be used for an observable every time it's created.
*/
export function localStorageBoolObs(key: string): Observable<boolean> {
export function localStorageBoolObs(key: string, defValue = false): Observable<boolean> {
const store = getStorage();
const obs = Observable.create(null, Boolean(store.getItem(key)));
obs.addListener((val) => val ? store.setItem(key, 'true') : store.removeItem(key));
const storedNegation = defValue ? 'false' : 'true';
const obs = Observable.create(null, store.getItem(key) === storedNegation ? !defValue : defValue);
obs.addListener((val) => val === defValue ? store.removeItem(key) : store.setItem(key, storedNegation));
return obs;
}

View File

@ -6,7 +6,7 @@ import {showDocSettingsModal} from 'app/client/ui/DocumentSettings';
import {showProfileModal} from 'app/client/ui/ProfileDialog';
import {createUserImage} from 'app/client/ui/UserImage';
import * as viewport from 'app/client/ui/viewport';
import {primaryButtonLink} from 'app/client/ui2018/buttons';
import {primaryButton} from 'app/client/ui2018/buttons';
import {colors, mediaDeviceNotSmall, testId, vars} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {menu, menuDivider, menuItem, menuItemLink, menuSubHeader} from 'app/client/ui2018/menus';
@ -39,9 +39,9 @@ export class AccountWidget extends Disposable {
cssUserIcon(createUserImage(user, 'medium', testId('user-icon')),
menu(() => this._makeAccountMenu(user), {placement: 'bottom-end'}),
) :
primaryButtonLink('Sign in',
{href: getLoginOrSignupUrl(), style: 'margin: 8px'},
testId('user-signin'))
cssSignInButton('Sign in', icon('Collapse'), testId('user-signin'),
menu(() => this._makeAccountMenu(user), {placement: 'bottom-end'}),
)
)
),
testId('dm-account'),
@ -63,7 +63,7 @@ export class AccountWidget extends Disposable {
* Renders the content of the account menu, with a list of available orgs, settings, and sign-out.
* Note that `user` should NOT be anonymous (none of the items are really relevant).
*/
private _makeAccountMenu(user: FullUser): DomElementArg[] {
private _makeAccountMenu(user: FullUser|null): DomElementArg[] {
// Opens the user-manager for the org.
const manageUsers = async (org: Organization) => {
const api = this._appModel.api;
@ -78,7 +78,31 @@ export class AccountWidget extends Disposable {
const currentOrg = this._appModel.currentOrg;
const gristDoc = this._docPageModel ? this._docPageModel.gristDoc.get() : null;
const isBillingManager = Boolean(currentOrg && currentOrg.billingAccount &&
(currentOrg.billingAccount.isManager || user.email === SUPPORT_EMAIL));
(currentOrg.billingAccount.isManager || user?.email === SUPPORT_EMAIL));
// The 'Document Settings' item, when there is an open document.
const documentSettingsItem = (gristDoc ?
menuItem(() => showDocSettingsModal(gristDoc.docInfo, this._docPageModel!), 'Document Settings',
testId('dm-doc-settings')) :
null);
// The item to toggle mobile mode (presence of viewport meta tag).
const mobileModeToggle = menuItem(viewport.toggleViewport,
cssSmallDeviceOnly.cls(''), // Only show this toggle on small devices.
'Toggle Mobile Mode',
cssCheckmark('Tick', dom.show(viewport.viewportEnabled)),
testId('usermenu-toggle-mobile'),
);
if (!user) {
return [
menuItemLink({href: getLoginOrSignupUrl()}, 'Sign in'),
menuDivider(),
documentSettingsItem,
menuItemLink({href: commonUrls.plans}, 'Pricing'),
mobileModeToggle,
];
}
return [
cssUserInfo(
@ -89,11 +113,7 @@ export class AccountWidget extends Disposable {
),
menuItem(() => showProfileModal(this._appModel), 'Profile Settings'),
// Enable 'Document Settings' when there is an open document.
(gristDoc ?
menuItem(() => showDocSettingsModal(gristDoc.docInfo, this._docPageModel!), 'Document Settings',
testId('dm-doc-settings')) :
null),
documentSettingsItem,
// Show 'Organization Settings' when on a home page of a valid org.
(!this._docPageModel && currentOrg && !currentOrg.owner ?
@ -112,12 +132,7 @@ export class AccountWidget extends Disposable {
) :
menuItemLink({href: commonUrls.plans}, 'Upgrade Plan'),
menuItem(viewport.toggleViewport,
cssSmallDeviceOnly.cls(''), // Only show this toggle on small devices.
'Toggle Mobile Mode',
cssCheckmark('Tick', dom.show(viewport.viewportEnabled)),
testId('usermenu-toggle-mobile'),
),
mobileModeToggle,
// TODO Add section ("Here right now") listing icons of other users currently on this doc.
// (See Invision "Panels" near the bottom.)
@ -246,3 +261,9 @@ const cssSmallDeviceOnly = styled(menuItem, `
}
}
`);
const cssSignInButton = styled(primaryButton, `
display: flex;
margin: 8px;
gap: 4px;
`);

View File

@ -216,17 +216,9 @@ export const spinner = styled('div', `
`);
export const prefSelectors = styled('div', `
position: absolute;
top: 32px;
right: 64px;
float: right;
display: flex;
align-items: center;
@media ${mediaSmall} {
& {
right: 24px;
}
}
`);
export const sortSelector = styled('div', `

View File

@ -6,7 +6,7 @@ import {examples} from 'app/client/ui/ExampleInfo';
import {createDocAndOpen, importDocAndOpen} from 'app/client/ui/HomeLeftPane';
import {buildPinnedDoc} from 'app/client/ui/PinnedDocs';
import {bigBasicButton} from 'app/client/ui2018/buttons';
import {colors, testId} from 'app/client/ui2018/cssVars';
import {colors, mediaXSmall, testId} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {cssLink} from 'app/client/ui2018/links';
import {commonUrls} from 'app/common/gristUrls';
@ -127,6 +127,12 @@ function buildExampleItem(doc: Document, home: HomeModel, view: 'list'|'icons')
const cssIntroSplit = styled(css.docBlock, `
display: flex;
align-items: center;
@media ${mediaXSmall} {
& {
display: block;
}
}
`);
const cssIntroLeft = styled('div', `
@ -134,6 +140,7 @@ const cssIntroLeft = styled('div', `
overflow: hidden;
max-height: 150px;
text-align: center;
margin: 32px 0;
`);
const cssIntroRight = styled('div', `

View File

@ -2,7 +2,7 @@ import {isIOS} from 'app/client/lib/browserInfo';
import {localStorageBoolObs} from 'app/client/lib/localStorageObs';
import {dom} from 'grainjs';
export const viewportEnabled = localStorageBoolObs('viewportEnabled');
export const viewportEnabled = localStorageBoolObs('viewportEnabled', true);
export function toggleViewport() {
viewportEnabled.set(!viewportEnabled.get());

View File

@ -157,10 +157,12 @@ export const testId: TestId = makeTestId('test-');
// Min width for normal screen layout (in px). Note: <768px is bootstrap's definition of small
// screen (covers phones, including landscape, but not tablets).
const mediumScreenWidth = 768;
const smallScreenWidth = 576; // Anything below this is extra-small (e.g. portrait phones).
// Fractional width for max-query follows https://getbootstrap.com/docs/4.0/layout/overview/#responsive-breakpoints
export const mediaSmall = `(max-width: ${mediumScreenWidth - 0.02}px)`;
export const mediaNotSmall = `(min-width: ${mediumScreenWidth}px)`;
export const mediaXSmall = `(max-width: ${smallScreenWidth - 0.02}px)`;
export const mediaDeviceNotSmall = `(min-device-width: ${mediumScreenWidth}px)`;

View File

@ -39,22 +39,20 @@
</div>
<div id="browser-check-problem" style="display: none;">
<div class="browser-check-wrapper">
<div class="browser-check-message">
<h4>
Grist may not work well in your browser.
</h4>
<p>
The best experience is with modern Firefox or Chrome on the desktop.
Other modern browsers will work to the degree they are standards compliant.
</p>
</div>
<div class="browser-check-options">
<a href="#" id="browser-check-problem-dismiss">Try it anyway</a>
<a href="https://www.mozilla.org/en-US/firefox/new/">Get Firefox</a>
<a href="https://www.google.com/chrome/">Get Chrome</a>
</div>
</div>
<table class="browser-check-wrapper"><tr>
<td class="browser-check-message">
<span class="browser-check-desktop">
Grist works best on modern Firefox or Chrome.
</span>
<span class="browser-check-mobile">
Grist mobile support is a work in progress.
</span>
<a href="https://support.getgrist.com/browser-support" target="_blank">Learn more</a>
</td>
<td>
<div class="browser-check-close" id="browser-check-problem-dismiss">Dismiss</div>
</td>
</tr></table>
</div>
<!-- INSERT CONFIG -->