(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 { #browser-check-problem {
display: none; display: none;
width: 100%; width: 100%;
height: 100%;
position: absolute; position: absolute;
z-index: 5000; z-index: 5000;
top: 0; bottom: 0;
left: 0; left: 0;
background: rgba(255, 255, 255, 0.9); padding: 4px;
padding-top: 3em;
/* 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 { .browser-check-wrapper {
position: absolute; margin: auto;
top: 30%; max-width: 600px;
width: 100%; padding: 8px 24px;
} background-color: #040404;
#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;
border-radius: 4px; border-radius: 4px;
border: none;
color: white;
box-shadow: 0 0 4px 0 white;
} }
#browser-check-problem a:hover { .browser-check-wrapper td {
text-decoration: none; vertical-align: middle;
color: #009058; padding: 8px 16px;
border: 1px solid #009058; }
.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. * 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 store = getStorage();
const obs = Observable.create(null, Boolean(store.getItem(key))); const storedNegation = defValue ? 'false' : 'true';
obs.addListener((val) => val ? store.setItem(key, 'true') : store.removeItem(key)); 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; return obs;
} }

View File

@ -6,7 +6,7 @@ import {showDocSettingsModal} from 'app/client/ui/DocumentSettings';
import {showProfileModal} from 'app/client/ui/ProfileDialog'; import {showProfileModal} from 'app/client/ui/ProfileDialog';
import {createUserImage} from 'app/client/ui/UserImage'; import {createUserImage} from 'app/client/ui/UserImage';
import * as viewport from 'app/client/ui/viewport'; 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 {colors, mediaDeviceNotSmall, testId, vars} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons'; import {icon} from 'app/client/ui2018/icons';
import {menu, menuDivider, menuItem, menuItemLink, menuSubHeader} from 'app/client/ui2018/menus'; 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')), cssUserIcon(createUserImage(user, 'medium', testId('user-icon')),
menu(() => this._makeAccountMenu(user), {placement: 'bottom-end'}), menu(() => this._makeAccountMenu(user), {placement: 'bottom-end'}),
) : ) :
primaryButtonLink('Sign in', cssSignInButton('Sign in', icon('Collapse'), testId('user-signin'),
{href: getLoginOrSignupUrl(), style: 'margin: 8px'}, menu(() => this._makeAccountMenu(user), {placement: 'bottom-end'}),
testId('user-signin')) )
) )
), ),
testId('dm-account'), 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. * 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). * 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. // Opens the user-manager for the org.
const manageUsers = async (org: Organization) => { const manageUsers = async (org: Organization) => {
const api = this._appModel.api; const api = this._appModel.api;
@ -78,7 +78,31 @@ export class AccountWidget extends Disposable {
const currentOrg = this._appModel.currentOrg; const currentOrg = this._appModel.currentOrg;
const gristDoc = this._docPageModel ? this._docPageModel.gristDoc.get() : null; const gristDoc = this._docPageModel ? this._docPageModel.gristDoc.get() : null;
const isBillingManager = Boolean(currentOrg && currentOrg.billingAccount && 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 [ return [
cssUserInfo( cssUserInfo(
@ -89,11 +113,7 @@ export class AccountWidget extends Disposable {
), ),
menuItem(() => showProfileModal(this._appModel), 'Profile Settings'), menuItem(() => showProfileModal(this._appModel), 'Profile Settings'),
// Enable 'Document Settings' when there is an open document. documentSettingsItem,
(gristDoc ?
menuItem(() => showDocSettingsModal(gristDoc.docInfo, this._docPageModel!), 'Document Settings',
testId('dm-doc-settings')) :
null),
// Show 'Organization Settings' when on a home page of a valid org. // Show 'Organization Settings' when on a home page of a valid org.
(!this._docPageModel && currentOrg && !currentOrg.owner ? (!this._docPageModel && currentOrg && !currentOrg.owner ?
@ -112,12 +132,7 @@ export class AccountWidget extends Disposable {
) : ) :
menuItemLink({href: commonUrls.plans}, 'Upgrade Plan'), menuItemLink({href: commonUrls.plans}, 'Upgrade Plan'),
menuItem(viewport.toggleViewport, mobileModeToggle,
cssSmallDeviceOnly.cls(''), // Only show this toggle on small devices.
'Toggle Mobile Mode',
cssCheckmark('Tick', dom.show(viewport.viewportEnabled)),
testId('usermenu-toggle-mobile'),
),
// TODO Add section ("Here right now") listing icons of other users currently on this doc. // TODO Add section ("Here right now") listing icons of other users currently on this doc.
// (See Invision "Panels" near the bottom.) // (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', ` export const prefSelectors = styled('div', `
position: absolute; float: right;
top: 32px;
right: 64px;
display: flex; display: flex;
align-items: center; align-items: center;
@media ${mediaSmall} {
& {
right: 24px;
}
}
`); `);
export const sortSelector = styled('div', ` 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 {createDocAndOpen, importDocAndOpen} from 'app/client/ui/HomeLeftPane';
import {buildPinnedDoc} from 'app/client/ui/PinnedDocs'; import {buildPinnedDoc} from 'app/client/ui/PinnedDocs';
import {bigBasicButton} from 'app/client/ui2018/buttons'; 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 {icon} from 'app/client/ui2018/icons';
import {cssLink} from 'app/client/ui2018/links'; import {cssLink} from 'app/client/ui2018/links';
import {commonUrls} from 'app/common/gristUrls'; import {commonUrls} from 'app/common/gristUrls';
@ -127,6 +127,12 @@ function buildExampleItem(doc: Document, home: HomeModel, view: 'list'|'icons')
const cssIntroSplit = styled(css.docBlock, ` const cssIntroSplit = styled(css.docBlock, `
display: flex; display: flex;
align-items: center; align-items: center;
@media ${mediaXSmall} {
& {
display: block;
}
}
`); `);
const cssIntroLeft = styled('div', ` const cssIntroLeft = styled('div', `
@ -134,6 +140,7 @@ const cssIntroLeft = styled('div', `
overflow: hidden; overflow: hidden;
max-height: 150px; max-height: 150px;
text-align: center; text-align: center;
margin: 32px 0;
`); `);
const cssIntroRight = styled('div', ` 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 {localStorageBoolObs} from 'app/client/lib/localStorageObs';
import {dom} from 'grainjs'; import {dom} from 'grainjs';
export const viewportEnabled = localStorageBoolObs('viewportEnabled'); export const viewportEnabled = localStorageBoolObs('viewportEnabled', true);
export function toggleViewport() { export function toggleViewport() {
viewportEnabled.set(!viewportEnabled.get()); 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 // Min width for normal screen layout (in px). Note: <768px is bootstrap's definition of small
// screen (covers phones, including landscape, but not tablets). // screen (covers phones, including landscape, but not tablets).
const mediumScreenWidth = 768; 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 // 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 mediaSmall = `(max-width: ${mediumScreenWidth - 0.02}px)`;
export const mediaNotSmall = `(min-width: ${mediumScreenWidth}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)`; export const mediaDeviceNotSmall = `(min-device-width: ${mediumScreenWidth}px)`;

View File

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