mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Show Grist video tour after welcome questions
Summary: After the welcome questions are dismissed, a video tour modal will now be displayed. The video tour is also accessible via a tool button in the left panel of the home page, as well as a text button next to the Examples & Templates header. Test Plan: Browser tests. Reviewers: dsagal Reviewed By: dsagal Subscribers: dsagal Differential Revision: https://phab.getgrist.com/D3477
This commit is contained in:
parent
a91d493ffc
commit
abebe812db
@ -102,7 +102,17 @@ exports.groups = [{
|
||||
name: 'openWidgetConfiguration',
|
||||
keys: [],
|
||||
desc: 'Open Custom widget configuration screen',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'leftPanelOpen',
|
||||
keys: [],
|
||||
desc: 'Shortcut to open the left panel',
|
||||
},
|
||||
{
|
||||
name: 'videoTourToolsOpen',
|
||||
keys: [],
|
||||
desc: 'Shortcut to open video tour from home left panel',
|
||||
},
|
||||
]
|
||||
}, {
|
||||
group: 'Navigation',
|
||||
|
@ -10,6 +10,7 @@ import {getTimeFromNow, HomeModel, makeLocalViewSettings, ViewSettings} from 'ap
|
||||
import {getWorkspaceInfo, workspaceName} from 'app/client/models/WorkspaceInfo';
|
||||
import * as css from 'app/client/ui/DocMenuCss';
|
||||
import {buildHomeIntro} from 'app/client/ui/HomeIntro';
|
||||
import {createVideoTourTextButton} from 'app/client/ui/OpenVideoTour';
|
||||
import {buildUpgradeNudge} from 'app/client/ui/ProductUpgrades';
|
||||
import {buildPinnedDoc, createPinnedDocs} from 'app/client/ui/PinnedDocs';
|
||||
import {shadowScroll} from 'app/client/ui/shadowScroll';
|
||||
@ -25,7 +26,7 @@ import {IHomePage} from 'app/common/gristUrls';
|
||||
import {SortPref, ViewPref} from 'app/common/Prefs';
|
||||
import * as roles from 'app/common/roles';
|
||||
import {Document, Workspace} from 'app/common/UserAPI';
|
||||
import {Computed, computed, dom, DomContents, makeTestId, Observable, observable, styled} from 'grainjs';
|
||||
import {Computed, computed, dom, DomContents, makeTestId, Observable, observable} from 'grainjs';
|
||||
import sortBy = require('lodash/sortBy');
|
||||
import {buildTemplateDocs} from 'app/client/ui/TemplateDocs';
|
||||
import {localStorageBoolObs} from 'app/client/lib/localStorageObs';
|
||||
@ -67,7 +68,7 @@ function createLoadedDocMenu(home: HomeModel) {
|
||||
const flashDocId = observable<string|null>(null);
|
||||
return css.docList(
|
||||
showWelcomeQuestions(home.app.userPrefsObs),
|
||||
cssDocMenu(
|
||||
css.docMenu(
|
||||
dom.maybe(!home.app.currentFeatures.workspaces, () => [
|
||||
css.docListHeader('This service is not available right now'),
|
||||
dom('span', '(The organization needs a paid plan)')
|
||||
@ -208,8 +209,9 @@ function buildAllDocsTemplates(home: HomeModel, viewSettings: ViewSettings) {
|
||||
if (templates.length === 0) { return null; }
|
||||
|
||||
const hideTemplatesObs = localStorageBoolObs('hide-examples');
|
||||
return css.templatesDocBlock(
|
||||
return css.allDocsTemplates(css.templatesDocBlock(
|
||||
dom.autoDispose(hideTemplatesObs),
|
||||
css.templatesHeaderWrap(
|
||||
css.templatesHeader(
|
||||
'Examples & Templates',
|
||||
dom.domComputed(hideTemplatesObs, (collapsed) =>
|
||||
@ -218,6 +220,8 @@ function buildAllDocsTemplates(home: HomeModel, viewSettings: ViewSettings) {
|
||||
dom.on('click', () => hideTemplatesObs.set(!hideTemplatesObs.get())),
|
||||
testId('all-docs-templates-header'),
|
||||
),
|
||||
createVideoTourTextButton(),
|
||||
),
|
||||
dom.maybe((use) => !use(hideTemplatesObs), () => [
|
||||
buildTemplateDocs(home, templates, viewSettings),
|
||||
bigBasicButton(
|
||||
@ -228,7 +232,7 @@ function buildAllDocsTemplates(home: HomeModel, viewSettings: ViewSettings) {
|
||||
]),
|
||||
css.docBlock.cls((use) => '-' + use(home.currentView)),
|
||||
testId('all-docs-templates'),
|
||||
);
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
@ -586,7 +590,3 @@ function shouldShowTemplates(home: HomeModel, showIntro: boolean): boolean {
|
||||
// Show templates for all personal orgs, and for non-personal orgs when showing intro.
|
||||
return isPersonalOrg || showIntro;
|
||||
}
|
||||
|
||||
const cssDocMenu = styled('div', `
|
||||
flex-grow: 1;
|
||||
`);
|
||||
|
@ -8,6 +8,11 @@ import {bigBasicButton} from 'app/client/ui2018/buttons';
|
||||
// styles, which gives it priority.
|
||||
import 'popweasel';
|
||||
|
||||
export const docMenu = styled('div', `
|
||||
flex-grow: 1;
|
||||
max-width: 100%;
|
||||
`);
|
||||
|
||||
// The "&:after" clause forces some padding below all docs.
|
||||
export const docList = styled('div', `
|
||||
height: 100%;
|
||||
@ -33,16 +38,34 @@ export const docList = styled('div', `
|
||||
}
|
||||
`);
|
||||
|
||||
export const docListHeader = styled('div', `
|
||||
const listHeader = styled('div', `
|
||||
min-height: 32px;
|
||||
line-height: 32px;
|
||||
margin-bottom: 24px;
|
||||
color: ${colors.dark};
|
||||
font-size: ${vars.xxxlargeFontSize};
|
||||
font-weight: ${vars.headerControlTextWeight};
|
||||
`);
|
||||
|
||||
export const templatesHeader = styled(docListHeader, `
|
||||
export const docListHeader = styled(listHeader, `
|
||||
margin-bottom: 24px;
|
||||
`);
|
||||
|
||||
export const templatesHeaderWrap = styled('div', `
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
@media ${mediaSmall} {
|
||||
& {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
export const templatesHeader = styled(listHeader, `
|
||||
cursor: pointer;
|
||||
`);
|
||||
|
||||
@ -53,13 +76,18 @@ export const featuredTemplatesHeader = styled(docListHeader, `
|
||||
|
||||
export const otherSitesHeader = templatesHeader;
|
||||
|
||||
export const allDocsTemplates = styled('div', `
|
||||
display: flex;
|
||||
`);
|
||||
|
||||
export const docBlock = styled('div', `
|
||||
max-width: 550px;
|
||||
min-width: 300px;
|
||||
margin-bottom: 28px;
|
||||
|
||||
&-icons {
|
||||
max-width: unset;
|
||||
max-width: max-content;
|
||||
min-width: calc(min(550px, 100%));
|
||||
}
|
||||
`);
|
||||
|
||||
|
@ -6,7 +6,8 @@ import {HomeModel} from 'app/client/models/HomeModel';
|
||||
import {getWorkspaceInfo, workspaceName} from 'app/client/models/WorkspaceInfo';
|
||||
import {addNewButton, cssAddNewButton} from 'app/client/ui/AddNewButton';
|
||||
import {docImport, importFromPlugin} from 'app/client/ui/HomeImports';
|
||||
import {cssLinkText, cssPageEntry, cssPageIcon, cssPageLink} from 'app/client/ui/LeftPanelCommon';
|
||||
import {cssLinkText, cssPageEntry, cssPageIcon, cssPageLink, cssSpacer} from 'app/client/ui/LeftPanelCommon';
|
||||
import {createVideoTourToolsButton} from 'app/client/ui/OpenVideoTour';
|
||||
import {transientInput} from 'app/client/ui/transientInput';
|
||||
import {colors, testId} from 'app/client/ui2018/cssVars';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
@ -111,6 +112,8 @@ export function createHomeLeftPane(leftPanelOpen: Observable<boolean>, home: Hom
|
||||
testId('dm-trash'),
|
||||
),
|
||||
),
|
||||
cssSpacer(),
|
||||
createVideoTourToolsButton(),
|
||||
createHelpTools(home.app),
|
||||
)
|
||||
)
|
||||
|
@ -24,13 +24,11 @@ import {dom, DomContents, Observable, styled} from 'grainjs';
|
||||
* Creates the "help tools", a button/link to open HelpScout beacon, and one to open the
|
||||
* HelpCenter in a new tab.
|
||||
*/
|
||||
export function createHelpTools(appModel: AppModel, spacer = true): DomContents {
|
||||
export function createHelpTools(appModel: AppModel): DomContents {
|
||||
if (shouldHideUiElement("helpCenter")) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
spacer ? cssSpacer() : null,
|
||||
cssSplitPageEntry(
|
||||
return cssSplitPageEntry(
|
||||
cssPageEntryMain(
|
||||
cssPageLink(cssPageIcon('Help'),
|
||||
cssLinkText('Help Center'),
|
||||
@ -43,9 +41,8 @@ export function createHelpTools(appModel: AppModel, spacer = true): DomContents
|
||||
cssPageLink(cssPageIcon('FieldLink'),
|
||||
{href: commonUrls.help, target: '_blank'},
|
||||
),
|
||||
)
|
||||
),
|
||||
];
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -56,6 +53,7 @@ export function leftPanelBasic(appModel: AppModel, panelOpen: Observable<boolean
|
||||
cssScrollPane(
|
||||
cssTools(
|
||||
cssTools.cls('-collapsed', (use) => !use(panelOpen)),
|
||||
cssSpacer(),
|
||||
createHelpTools(appModel),
|
||||
)
|
||||
)
|
||||
|
142
app/client/ui/OpenVideoTour.ts
Normal file
142
app/client/ui/OpenVideoTour.ts
Normal file
@ -0,0 +1,142 @@
|
||||
import * as commands from 'app/client/components/commands';
|
||||
import {cssLinkText, cssPageEntryMain, cssPageIcon, cssPageLink} from 'app/client/ui/LeftPanelCommon';
|
||||
import {colors} from 'app/client/ui2018/cssVars';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {modal} from 'app/client/ui2018/modals';
|
||||
import {commonUrls, shouldHideUiElement} from 'app/common/gristUrls';
|
||||
import {dom, makeTestId, styled} from 'grainjs';
|
||||
|
||||
const testId = makeTestId('test-video-tour-');
|
||||
|
||||
/**
|
||||
* Opens a modal containing a video tour of Grist.
|
||||
*/
|
||||
export function openVideoTour(refElement: HTMLElement) {
|
||||
return modal(
|
||||
(ctl) => {
|
||||
return [
|
||||
cssModal.cls(''),
|
||||
cssCloseButton(
|
||||
cssCloseIcon('CrossBig'),
|
||||
dom.on('click', () => ctl.close()),
|
||||
testId('close'),
|
||||
),
|
||||
cssVideoWrap(
|
||||
cssVideo(
|
||||
{
|
||||
src: commonUrls.videoTour,
|
||||
title: 'YouTube video player',
|
||||
frameborder: '0',
|
||||
allow: 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture',
|
||||
allowfullscreen: '',
|
||||
},
|
||||
),
|
||||
),
|
||||
testId('modal'),
|
||||
];
|
||||
},
|
||||
{
|
||||
refElement,
|
||||
variant: 'collapsing',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a text button that shows the video tour on click.
|
||||
*/
|
||||
export function createVideoTourTextButton(): HTMLDivElement {
|
||||
const elem: HTMLDivElement = cssVideoTourTextButton(
|
||||
cssVideoIcon('Video'),
|
||||
'Grist Video Tour',
|
||||
dom.on('click', () => openVideoTour(elem)),
|
||||
testId('text-button'),
|
||||
);
|
||||
|
||||
return elem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the "Video Tour" button for the "Tools" section of the left panel.
|
||||
*
|
||||
* Shows the video tour on click.
|
||||
*/
|
||||
export function createVideoTourToolsButton(): HTMLDivElement | null {
|
||||
if (shouldHideUiElement('helpCenter')) { return null; }
|
||||
|
||||
let iconElement: HTMLElement;
|
||||
|
||||
const commandsGroup = commands.createGroup({
|
||||
videoTourToolsOpen: () => openVideoTour(iconElement),
|
||||
}, null, true);
|
||||
|
||||
return cssPageEntryMain(
|
||||
dom.autoDispose(commandsGroup),
|
||||
cssPageLink(
|
||||
iconElement = cssPageIcon('Video'),
|
||||
cssLinkText('Video Tour'),
|
||||
dom.cls('tour-help-center'),
|
||||
dom.on('click', () => openVideoTour(iconElement)),
|
||||
testId('tools-button'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const cssModal = styled('div', `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
max-width: 864px;
|
||||
`);
|
||||
|
||||
const cssVideoWrap = styled('div', `
|
||||
position: relative;
|
||||
padding-bottom: 56.25%;
|
||||
height: 0;
|
||||
`);
|
||||
|
||||
const cssVideo = styled('iframe', `
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`);
|
||||
|
||||
const cssVideoTourTextButton = styled('div', `
|
||||
color: ${colors.lightGreen};
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: ${colors.darkGreen};
|
||||
}
|
||||
`);
|
||||
|
||||
const cssVideoIcon = styled(icon, `
|
||||
background-color: ${colors.lightGreen};
|
||||
cursor: pointer;
|
||||
margin: 0px 4px 3px 0;
|
||||
|
||||
.${cssVideoTourTextButton.className}:hover > & {
|
||||
background-color: ${colors.darkGreen};
|
||||
}
|
||||
`);
|
||||
|
||||
const cssCloseButton = styled('div', `
|
||||
align-self: flex-end;
|
||||
margin: -8px;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
--icon-color: ${colors.slate};
|
||||
|
||||
&:hover {
|
||||
background-color: ${colors.mediumGreyOpaque};
|
||||
}
|
||||
`);
|
||||
|
||||
const cssCloseIcon = styled(icon, `
|
||||
padding: 12px;
|
||||
`);
|
@ -1,9 +1,10 @@
|
||||
/**
|
||||
* Note that it assumes the presence of cssVars.cssRootVars on <body>.
|
||||
*/
|
||||
import * as commands from 'app/client/components/commands';
|
||||
import {urlState} from "app/client/models/gristUrlState";
|
||||
import {resizeFlexVHandle} from 'app/client/ui/resizeHandle';
|
||||
import {transition} from 'app/client/ui/transitions';
|
||||
import {transition, TransitionWatcher} from 'app/client/ui/transitions';
|
||||
import {colors, cssHideForNarrowScreen, mediaNotSmall, mediaSmall} from 'app/client/ui2018/cssVars';
|
||||
import {isNarrowScreenObs} from 'app/client/ui2018/cssVars';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
@ -40,6 +41,7 @@ export function pagePanels(page: PageContents) {
|
||||
|
||||
let lastLeftOpen = left.panelOpen.get();
|
||||
let lastRightOpen = right?.panelOpen.get() || false;
|
||||
let leftPaneDom: HTMLElement;
|
||||
|
||||
// When switching to mobile mode, close panels; when switching to desktop, restore the
|
||||
// last desktop state.
|
||||
@ -60,12 +62,21 @@ export function pagePanels(page: PageContents) {
|
||||
}
|
||||
});
|
||||
|
||||
const commandsGroup = commands.createGroup({
|
||||
leftPanelOpen: () => new Promise((resolve) => {
|
||||
const watcher = new TransitionWatcher(leftPaneDom);
|
||||
watcher.onDispose(() => resolve(undefined));
|
||||
left.panelOpen.set(true);
|
||||
}),
|
||||
}, null, true);
|
||||
|
||||
return cssPageContainer(
|
||||
dom.autoDispose(sub1),
|
||||
dom.autoDispose(sub2),
|
||||
dom.autoDispose(commandsGroup),
|
||||
page.contentTop,
|
||||
cssContentMain(
|
||||
cssLeftPane(
|
||||
leftPaneDom = cssLeftPane(
|
||||
testId('left-panel'),
|
||||
cssTopHeader(left.header),
|
||||
left.content,
|
||||
@ -187,6 +198,7 @@ function toggleObs(boolObs: Observable<boolean>) {
|
||||
boolObs.set(!boolObs.get());
|
||||
}
|
||||
|
||||
const bottomFooterHeightPx = 48;
|
||||
const cssVBox = styled('div', `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -207,7 +219,7 @@ const cssPageContainer = styled(cssVBox, `
|
||||
|
||||
@media ${mediaSmall} {
|
||||
& {
|
||||
padding-bottom: 48px;
|
||||
padding-bottom: ${bottomFooterHeightPx}px;
|
||||
min-width: 240px;
|
||||
}
|
||||
.interface-light & {
|
||||
@ -232,7 +244,7 @@ export const cssLeftPane = styled(cssVBox, `
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
bottom: ${bottomFooterHeightPx}px;
|
||||
left: -${240 + 15}px; /* adds an extra 15 pixels to also hide the box shadow */
|
||||
visibility: hidden;
|
||||
box-shadow: 10px 0 5px rgba(0, 0, 0, 0.2);
|
||||
@ -279,7 +291,7 @@ const cssRightPane = styled(cssVBox, `
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
bottom: ${bottomFooterHeightPx}px;
|
||||
right: -${240 + 15}px; /* adds an extra 15 pixels to also hide the box shadow */
|
||||
box-shadow: -10px 0 5px rgba(0, 0, 0, 0.2);
|
||||
visibility: hidden;
|
||||
@ -324,7 +336,7 @@ const cssTopHeader = styled('div', `
|
||||
}
|
||||
`);
|
||||
const cssBottomFooter = styled ('div', `
|
||||
height: 48px;
|
||||
height: ${bottomFooterHeightPx}px;
|
||||
background-color: white;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
|
@ -121,6 +121,11 @@ const pinnedDocWrapper = styled('div', `
|
||||
&:hover {
|
||||
border: 1px solid ${colors.slate};
|
||||
}
|
||||
|
||||
/* TODO: Specify a gap on flexbox parents of pinnedDocWrapper instead. */
|
||||
&:last-child {
|
||||
margin-right: 0px;
|
||||
}
|
||||
`);
|
||||
|
||||
const pinnedDoc = styled('a', `
|
||||
|
@ -114,7 +114,7 @@ export function tools(owner: Disposable, gristDoc: GristDoc, leftPanelOpen: Obse
|
||||
)
|
||||
),
|
||||
),
|
||||
createHelpTools(docPageModel.appModel, false)
|
||||
createHelpTools(docPageModel.appModel),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import * as commands from 'app/client/components/commands';
|
||||
import {getUserPrefObs} from 'app/client/models/UserPrefs';
|
||||
import {colors, testId} from 'app/client/ui2018/cssVars';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
@ -29,9 +30,16 @@ export function showWelcomeQuestions(userPrefsObs: Observable<UserPrefs>) {
|
||||
{method: 'POST', body: JSON.stringify({use_cases, use_other})});
|
||||
}
|
||||
|
||||
|
||||
owner.onDispose(async () => {
|
||||
// Whichever way the modal is closed, don't show the questions again. (We set the value to
|
||||
// undefined to remove it from the JSON prefs object entirely; it's never used again.)
|
||||
owner.onDispose(() => showQuestions.set(undefined));
|
||||
showQuestions.set(undefined);
|
||||
|
||||
// Show the Grist video tour when the modal is closed.
|
||||
await commands.allCommands.leftPanelOpen.run();
|
||||
commands.allCommands.videoTourToolsOpen.run();
|
||||
});
|
||||
|
||||
return {
|
||||
title: [cssLogo(), dom('div', 'Welcome to Grist!')],
|
||||
|
@ -1,6 +1,7 @@
|
||||
import {FocusLayer} from 'app/client/lib/FocusLayer';
|
||||
import {reportError} from 'app/client/models/errors';
|
||||
import {cssInput} from 'app/client/ui/MakeCopyMenu';
|
||||
import {prepareForTransition, TransitionWatcher} from 'app/client/ui/transitions';
|
||||
import {bigBasicButton, bigPrimaryButton, cssButton} from 'app/client/ui2018/buttons';
|
||||
import {colors, mediaSmall, testId, vars} from 'app/client/ui2018/cssVars';
|
||||
import {loadingSpinner} from 'app/client/ui2018/loaders';
|
||||
@ -112,10 +113,25 @@ export class ModalControl extends Disposable implements IModalControl {
|
||||
}
|
||||
}
|
||||
|
||||
export interface IModalOptions {
|
||||
noEscapeKey?: boolean; // If set, escape key does not close the dialog
|
||||
noClickAway?: boolean; // If set, clicking into background does not close dialog.
|
||||
/**
|
||||
* The modal variant.
|
||||
*
|
||||
* Fade-in modals open with a fade-in background animation, and close immediately.
|
||||
*
|
||||
* Collapsing modals open with a expanding animation from a referenced DOM element, and
|
||||
* close with a collapsing animation into the referenced element.
|
||||
*/
|
||||
export type IModalVariant = 'fade-in' | 'collapsing';
|
||||
|
||||
export interface IModalOptions {
|
||||
// The modal variant. Defaults to "fade-in".
|
||||
variant?: IModalVariant;
|
||||
// Required for "collapsing" variant modals. This is the anchor element for animations.
|
||||
refElement?: HTMLElement;
|
||||
// If set, escape key does not close the dialog.
|
||||
noEscapeKey?: boolean;
|
||||
// If set, clicking into background does not close dialog.
|
||||
noClickAway?: boolean;
|
||||
// If given, call and wait for this before closing the dialog. If it returns false, don't close.
|
||||
// Error also prevents closing, and is reported as an unexpected error.
|
||||
beforeClose?: () => Promise<boolean>;
|
||||
@ -153,41 +169,78 @@ export function modal(
|
||||
createFn: (ctl: IModalControl, owner: MultiHolder) => DomElementArg,
|
||||
options: IModalOptions = {}
|
||||
): void {
|
||||
const {noEscapeKey, noClickAway, refElement = document.body, variant = 'fade-in'} = options;
|
||||
|
||||
function doClose() {
|
||||
if (!modalDom.isConnected) { return; }
|
||||
|
||||
variant === 'collapsing' ? collapseAndCloseModal() : closeModal();
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.body.removeChild(modalDom);
|
||||
// Ensure we run the disposers for the DOM contained in the modal.
|
||||
dom.domDispose(modalDom);
|
||||
}
|
||||
|
||||
function collapseAndCloseModal() {
|
||||
const watcher = new TransitionWatcher(dialogDom);
|
||||
watcher.onDispose(() => closeModal());
|
||||
modalDom.classList.add(cssModalBacker.className + '-collapsing');
|
||||
collapseModal();
|
||||
}
|
||||
|
||||
function expandModal() {
|
||||
prepareForTransition(dialogDom, () => collapseModal());
|
||||
Object.assign(dialogDom.style, {
|
||||
transform: '',
|
||||
opacity: '',
|
||||
visibility: 'visible',
|
||||
});
|
||||
}
|
||||
|
||||
function collapseModal() {
|
||||
const rect = dialogDom.getBoundingClientRect();
|
||||
const collapsedRect = refElement.getBoundingClientRect();
|
||||
const originX = (collapsedRect.left + collapsedRect.width / 2) - rect.left;
|
||||
const originY = (collapsedRect.top + collapsedRect.height / 2) - rect.top;
|
||||
Object.assign(dialogDom.style, {
|
||||
transform: `scale(${collapsedRect.width / rect.width}, ${collapsedRect.height / rect.height})`,
|
||||
transformOrigin: `${originX}px ${originY}px`,
|
||||
opacity: '0',
|
||||
});
|
||||
}
|
||||
|
||||
let close = doClose;
|
||||
let dialogDom: HTMLElement;
|
||||
|
||||
const modalDom = cssModalBacker(
|
||||
dom.create((owner) => {
|
||||
const focus = () => dialog.focus();
|
||||
const focus = () => dialogDom.focus();
|
||||
const ctl = ModalControl.create(owner, doClose, focus);
|
||||
close = () => ctl.close();
|
||||
|
||||
const dialog = cssModalDialog(
|
||||
dialogDom = cssModalDialog(
|
||||
createFn(ctl, owner),
|
||||
cssModalDialog.cls('-collapsing', variant === 'collapsing'),
|
||||
dom.on('click', (ev) => ev.stopPropagation()),
|
||||
options.noEscapeKey ? null : dom.onKeyDown({ Escape: close }),
|
||||
testId('modal-dialog')
|
||||
noEscapeKey ? null : dom.onKeyDown({ Escape: close }),
|
||||
testId('modal-dialog'),
|
||||
);
|
||||
FocusLayer.create(owner, {
|
||||
defaultFocusElem: dialog,
|
||||
defaultFocusElem: dialogDom,
|
||||
allowFocus: (elem) => (elem !== document.body),
|
||||
// Pause mousetrap keyboard shortcuts while the modal is shown. Without this, arrow keys
|
||||
// will navigate in a grid underneath the modal, and Enter may open a cell there.
|
||||
pauseMousetrap: true
|
||||
});
|
||||
return dialog;
|
||||
return dialogDom;
|
||||
}),
|
||||
options.noClickAway ? null : dom.on('click', () => close()),
|
||||
noClickAway ? null : dom.on('click', () => close()),
|
||||
);
|
||||
|
||||
|
||||
document.body.appendChild(modalDom);
|
||||
if (variant === 'collapsing') { expandModal(); }
|
||||
}
|
||||
|
||||
export interface ISaveModalOptions {
|
||||
@ -436,6 +489,11 @@ const cssModalDialog = styled('div', `
|
||||
&-fixed-wide {
|
||||
width: 600px;
|
||||
}
|
||||
&-collapsing {
|
||||
transition-property: opacity, transform;
|
||||
transition-duration: 0.4s;
|
||||
transition-timing-function: ease-in-out;
|
||||
}
|
||||
@media ${mediaSmall} {
|
||||
& {
|
||||
width: unset;
|
||||
@ -471,6 +529,10 @@ const cssFadeIn = keyframes(`
|
||||
from {background-color: transparent}
|
||||
`);
|
||||
|
||||
const cssFadeOut = keyframes(`
|
||||
from {background-color: ${colors.backdrop}}
|
||||
`);
|
||||
|
||||
const cssModalBacker = styled('div', `
|
||||
position: fixed;
|
||||
display: flex;
|
||||
@ -485,6 +547,11 @@ const cssModalBacker = styled('div', `
|
||||
overflow-y: auto;
|
||||
animation-name: ${cssFadeIn};
|
||||
animation-duration: 0.4s;
|
||||
|
||||
&-collapsing {
|
||||
animation-name: ${cssFadeOut};
|
||||
background-color: transparent;
|
||||
}
|
||||
`);
|
||||
|
||||
const cssSpinner = styled('div', `
|
||||
|
@ -64,6 +64,7 @@ export const commonUrls = {
|
||||
|
||||
efcrConnect: 'https://efc-r.com/connect',
|
||||
efcrHelp: 'https://www.nioxus.info/eFCR-Help',
|
||||
videoTour: 'https://www.youtube.com/embed/qnr2Pfnxdlc?autoplay=1',
|
||||
};
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user