diff --git a/app/client/app.css b/app/client/app.css index 1b468e69..b707a258 100644 --- a/app/client/app.css +++ b/app/client/app.css @@ -134,7 +134,7 @@ div.dev_warning { display: none; width: 100%; position: absolute; - z-index: 5000; + z-index: var(--grist-browser-check-z-index); bottom: 0; left: 0; padding: 4px; diff --git a/app/client/components/duplicatePage.ts b/app/client/components/duplicatePage.ts index 393f9e1f..db49c071 100644 --- a/app/client/components/duplicatePage.ts +++ b/app/client/components/duplicatePage.ts @@ -24,15 +24,15 @@ export async function duplicatePage(gristDoc: GristDoc, pageId: number) { let inputEl: HTMLInputElement; setTimeout(() => { inputEl.focus(); inputEl.select(); }, 100); - confirmModal('Duplicate page', 'Save', () => makeDuplicate(gristDoc, pageId, inputEl.value), ( - dom('div', [ + confirmModal('Duplicate page', 'Save', () => makeDuplicate(gristDoc, pageId, inputEl.value), { + explanation: dom('div', [ cssField( cssLabel("Name"), inputEl = cssInput({value: pageName + ' (copy)'}), ), t("Note that this does not copy data, but creates another view of the same data."), - ]) - )); + ]), + }); } async function makeDuplicate(gristDoc: GristDoc, pageId: number, pageName: string = '') { diff --git a/app/client/models/DocPageModel.ts b/app/client/models/DocPageModel.ts index 9cc386f8..eef9d567 100644 --- a/app/client/models/DocPageModel.ts +++ b/app/client/models/DocPageModel.ts @@ -262,15 +262,17 @@ export class DocPageModelImpl extends Disposable implements DocPageModel { t("Error accessing document"), t("Reload"), async () => window.location.reload(true), - isDocOwner - ? t("You can try reloading the document, or using recovery mode. " + - "Recovery mode opens the document to be fully accessible to " + - "owners, and inaccessible to others. It also disables " + - "formulas. [{{error}}]", {error: err.message}) - : isDenied - ? t('Sorry, access to this document has been denied. [{{error}}]', {error: err.message}) - : t("Document owners can attempt to recover the document. [{{error}}]", {error: err.message}), { + explanation: ( + isDocOwner + ? t("You can try reloading the document, or using recovery mode. " + + "Recovery mode opens the document to be fully accessible to " + + "owners, and inaccessible to others. It also disables " + + "formulas. [{{error}}]", {error: err.message}) + : isDenied + ? t('Sorry, access to this document has been denied. [{{error}}]', {error: err.message}) + : t("Document owners can attempt to recover the document. [{{error}}]", {error: err.message}) + ), hideCancel: true, extraButtons: !(isDocOwner && !isDenied) ? null : bigBasicButton( t("Enter recovery mode"), diff --git a/app/client/ui/ApiKey.ts b/app/client/ui/ApiKey.ts index 4ec2cabf..3d6f313d 100644 --- a/app/client/ui/ApiKey.ts +++ b/app/client/ui/ApiKey.ts @@ -115,8 +115,12 @@ export class ApiKey extends Disposable { confirmModal( t("Remove API Key"), t("Remove"), () => this._onDelete(), - t("You're about to delete an API key. This will cause all future requests " + - "using this API key to be rejected. Do you still want to delete?") + { + explanation: t( + "You're about to delete an API key. This will cause all future requests " + + "using this API key to be rejected. Do you still want to delete?" + ), + } ); } } diff --git a/app/client/ui/App.css b/app/client/ui/App.css index 61221b38..99ff9561 100644 --- a/app/client/ui/App.css +++ b/app/client/ui/App.css @@ -19,7 +19,7 @@ body { left: 10%; height: 80%; width: 80%; - z-index: 999; + z-index: var(--grist-modal-z-index); padding: 1rem; diff --git a/app/client/ui/DocMenu.ts b/app/client/ui/DocMenu.ts index 847ba9cc..c4a3eabc 100644 --- a/app/client/ui/DocMenu.ts +++ b/app/client/ui/DocMenu.ts @@ -476,7 +476,8 @@ export function makeDocOptionsMenu(home: HomeModel, doc: Document, renaming: Obs function deleteDoc() { confirmModal(t("Delete {{name}}", {name: doc.name}), t("Delete"), () => home.deleteDoc(doc.id, false).catch(reportError), - t("Document will be moved to Trash.")); + {explanation: t("Document will be moved to Trash.")} + ); } async function manageUsers() { @@ -529,7 +530,8 @@ export function makeRemovedDocOptionsMenu(home: HomeModel, doc: Document, worksp function hardDeleteDoc() { confirmModal(t("Permanently Delete \"{{name}}\"?", {name: doc.name}), t("Delete Forever"), () => home.deleteDoc(doc.id, true).catch(reportError), - t("Document will be permanently deleted.")); + {explanation: t("Document will be permanently deleted.")} + ); } return [ diff --git a/app/client/ui/DocTutorial.ts b/app/client/ui/DocTutorial.ts index 3693ee3f..f24b8426 100644 --- a/app/client/ui/DocTutorial.ts +++ b/app/client/ui/DocTutorial.ts @@ -3,9 +3,9 @@ import {urlState} from 'app/client/models/gristUrlState'; import {renderer} from 'app/client/ui/DocTutorialRenderer'; import {cssPopupBody, FloatingPopup} from 'app/client/ui/FloatingPopup'; import {sanitizeHTML} from 'app/client/ui/sanitizeHTML'; -import {hoverTooltip} from 'app/client/ui/tooltips'; +import {hoverTooltip, setHoverTooltip} from 'app/client/ui/tooltips'; import {basicButton, primaryButton} from 'app/client/ui2018/buttons'; -import {mediaXSmall, theme} from 'app/client/ui2018/cssVars'; +import {mediaXSmall, theme, vars} from 'app/client/ui2018/cssVars'; import {icon} from 'app/client/ui2018/icons'; import {loadingSpinner} from 'app/client/ui2018/loaders'; import {confirmModal, modal} from 'app/client/ui2018/modals'; @@ -25,6 +25,8 @@ interface DocTutorialSlide { const testId = makeTestId('test-doc-tutorial-'); +const TOOLTIP_KEY = 'docTutorialTooltip'; + export class DocTutorial extends FloatingPopup { private _appModel = this._gristDoc.docPageModel.appModel; private _currentDoc = this._gristDoc.docPageModel.currentDoc.get(); @@ -44,7 +46,7 @@ export class DocTutorial extends FloatingPopup { }); constructor(private _gristDoc: GristDoc) { - super(); + super({stopClickPropagationOnMove: true}); } public async start() { @@ -77,7 +79,7 @@ export class DocTutorial extends FloatingPopup { this._openLightbox((ev.target as HTMLImageElement).src); }), - this._restartGIFs(), + this._initializeImages(), ], testId('popup-body'), ); @@ -94,14 +96,17 @@ export class DocTutorial extends FloatingPopup { return [ cssFooterButtonsLeft( cssPopupFooterButton(icon('Undo'), - hoverTooltip('Restart Tutorial', {key: 'docTutorialTooltip'}), + hoverTooltip('Restart Tutorial', {key: TOOLTIP_KEY}), dom.on('click', () => this._restartTutorial()), testId('popup-restart'), ), ), cssProgressBar( range(slides.length).map((i) => cssProgressBarDot( - {title: slides[i].slideTitle}, + hoverTooltip(slides[i].slideTitle, { + closeOnClick: false, + key: TOOLTIP_KEY, + }), cssProgressBarDot.cls('-current', i === slideIndex), i === slideIndex ? null : dom.on('click', () => this._changeSlide(i)), testId(`popup-slide-${i + 1}`), @@ -251,11 +256,19 @@ export class DocTutorial extends FloatingPopup { confirmModal( 'Do you want to restart the tutorial? All progress will be lost.', 'Restart', - doRestart + doRestart, + { + modalOptions: { + backerDomArgs: [ + // Stack modal above the tutorial popup. + dom.style('z-index', vars.tutorialModalZIndex.toString()), + ], + }, + } ); } - private _restartGIFs() { + private _initializeImages() { return (element: HTMLElement) => { setTimeout(() => { const imgs = element.querySelectorAll('img'); @@ -263,6 +276,16 @@ export class DocTutorial extends FloatingPopup { // Re-assigning src to itself is a neat way to restart a GIF. // eslint-disable-next-line no-self-assign img.src = img.src; + + setHoverTooltip(img, 'Click to expand', { + key: TOOLTIP_KEY, + modifiers: { + flip: { + boundariesElement: 'scrollParent', + }, + }, + placement: 'bottom', + }); } }, 0); }; @@ -281,6 +304,11 @@ export class DocTutorial extends FloatingPopup { dom.on('click', (ev, elem) => void (ev.target === elem ? ctl.close() : null)), testId('lightbox'), ]; + }, { + backerDomArgs: [ + // Stack modal above the tutorial popup. + dom.style('z-index', vars.tutorialModalZIndex.toString()), + ], }); } } diff --git a/app/client/ui/FloatingPopup.ts b/app/client/ui/FloatingPopup.ts index 8a740199..cb0c74bd 100644 --- a/app/client/ui/FloatingPopup.ts +++ b/app/client/ui/FloatingPopup.ts @@ -1,5 +1,5 @@ import {hoverTooltip} from 'app/client/ui/tooltips'; -import {isNarrowScreen, isNarrowScreenObs, theme} from 'app/client/ui2018/cssVars'; +import {isNarrowScreen, isNarrowScreenObs, theme, vars} from 'app/client/ui2018/cssVars'; import {icon} from 'app/client/ui2018/icons'; import {Disposable, dom, DomArg, DomContents, IDisposable, makeTestId, Observable, styled} from 'grainjs'; @@ -16,11 +16,15 @@ export interface PopupOptions { onClose?: () => void; closeButton?: boolean; autoHeight?: boolean; + /** Defaults to false. */ + stopClickPropagationOnMove?: boolean; } export class FloatingPopup extends Disposable { protected _isMinimized = Observable.create(this, false); + private _isFinishingMove = false; private _popupElement: HTMLElement | null = null; + private _popupMinimizeButtonElement: HTMLElement | null = null; private _startX: number; private _startY: number; @@ -33,6 +37,25 @@ export class FloatingPopup extends Disposable { constructor(protected _options: PopupOptions = {}, private _args: DomArg[] = []) { super(); + if (_options.stopClickPropagationOnMove){ + // weasel.js registers a 'click' listener that closes any open popups that + // are outside the click target. We capture the click event here, stopping + // propagation in a few scenarios where closing popups is undesirable. + window.addEventListener('click', (ev) => { + if (this._isFinishingMove) { + ev.stopPropagation(); + this._isFinishingMove = false; + return; + } + + if (this._popupMinimizeButtonElement?.contains(ev.target as Node)) { + ev.stopPropagation(); + this._minimizeOrMaximize(); + return; + } + }, {capture: true}); + } + this._handleMouseDown = this._handleMouseDown.bind(this); this._handleMouseMove = this._handleMouseMove.bind(this); this._handleMouseUp = this._handleMouseUp.bind(this); @@ -181,6 +204,7 @@ export class FloatingPopup extends Disposable { } private _handleMouseUp() { + this._isFinishingMove = true; document.removeEventListener('mousemove', this._handleMouseMove); document.removeEventListener('mouseup', this._handleMouseUp); document.body.removeEventListener('mouseleave', this._handleMouseUp); @@ -221,6 +245,11 @@ export class FloatingPopup extends Disposable { this._popupElement!.style.top = `${newTop}px`; } + private _minimizeOrMaximize() { + this._isMinimized.set(!this._isMinimized.get()); + this._repositionPopup(); + } + private _buildPopup() { const body = cssPopup( {tabIndex: '-1'}, @@ -231,7 +260,8 @@ export class FloatingPopup extends Disposable { return cssResizeTopLayer( cssTopHandle(testId('resize-handle')), dom.on('mousedown', () => this._resize = true), - dom.on('dblclick', () => { + dom.on('dblclick', (e) => { + e.stopImmediatePropagation(); this._popupElement?.style.setProperty('--height', `${POPUP_MAX_HEIGHT}px`); this._repositionPopup(); }) @@ -262,23 +292,27 @@ export class FloatingPopup extends Disposable { }), testId('close'), ), - cssPopupHeaderButton( + this._popupMinimizeButtonElement = cssPopupHeaderButton( isMinimized ? icon('Maximize'): icon('Minimize'), hoverTooltip(isMinimized ? 'Maximize' : 'Minimize', {key: 'docTutorialTooltip'}), - dom.on('click', () => { - this._isMinimized.set(!this._isMinimized.get()); - this._repositionPopup(); - }), + dom.on('click', () => this._minimizeOrMaximize()), testId('minimize-maximize'), ), + // Disable dragging when a button in the header is clicked. + dom.on('mousedown', ev => ev.stopPropagation()), + dom.on('touchstart', ev => ev.stopPropagation()), ) ]; }), dom.on('mousedown', this._handleMouseDown), dom.on('touchstart', this._handleTouchStart), + dom.on('dblclick', () => this._minimizeOrMaximize()), testId('header'), ), - dom.maybe(use => !use(this._isMinimized), () => this._buildContent()), + cssPopupContent( + this._buildContent(), + cssPopupContent.cls('-minimized', this._isMinimized), + ), () => { window.addEventListener('resize', this._handleWindowResize); }, dom.onDispose(() => { document.removeEventListener('mousemove', this._handleMouseMove); @@ -344,7 +378,7 @@ const cssPopup = styled('div', ` flex-direction: column; border: 2px solid ${theme.accentBorder}; border-radius: 5px; - z-index: 999; + z-index: ${vars.floatingPopupZIndex}; --height: ${POPUP_MAX_HEIGHT}px; height: ${POPUP_HEIGHT}; width: ${POPUP_WIDTH}; @@ -441,10 +475,10 @@ const cssPopupHeaderButton = styled('div', ` const cssResizeTopLayer = styled('div', ` position: absolute; - top: 0; + top: -6px; left: 0; right: 0; - bottom: 70%; + bottom: 28px; z-index: 500; cursor: ns-resize; `); @@ -465,3 +499,14 @@ const cssBottomHandle = styled(cssTopHandle, ` bottom: 0; left: 0; `); + +const cssPopupContent = styled('div', ` + display: flex; + flex-direction: column; + flex-grow: 1; + overflow: hidden; + + &-minimized { + display: none; + } +`); diff --git a/app/client/ui/HomeLeftPane.ts b/app/client/ui/HomeLeftPane.ts index 96104e63..bb8cd1b8 100644 --- a/app/client/ui/HomeLeftPane.ts +++ b/app/client/ui/HomeLeftPane.ts @@ -217,7 +217,7 @@ function workspaceMenu(home: HomeModel, ws: Workspace, renaming: Observable home.deleteWorkspace(ws.id, false), - t("Workspace will be moved to Trash.")); + {explanation: t("Workspace will be moved to Trash.")}); } async function manageWorkspaceUsers() { diff --git a/app/client/ui/MakeCopyMenu.ts b/app/client/ui/MakeCopyMenu.ts index 6978c5d1..229fcedc 100644 --- a/app/client/ui/MakeCopyMenu.ts +++ b/app/client/ui/MakeCopyMenu.ts @@ -59,7 +59,7 @@ export async function replaceTrunkWithFork(user: FullUser|null, doc: Document, a } catch (e) { reportError(e); // For example: no write access on trunk. } - }, warningText); + }, {explanation: warningText}); } // Show message in a modal with a `Sign up` button that redirects to the login page. diff --git a/app/client/ui/NotifyUI.ts b/app/client/ui/NotifyUI.ts index ba3c7151..779f6ec6 100644 --- a/app/client/ui/NotifyUI.ts +++ b/app/client/ui/NotifyUI.ts @@ -258,13 +258,13 @@ const cssDropdownStatusText = styled('div', ` color: ${theme.lightText}; `); -// z-index below is set above other assorted children of which include z-index such as 999 -// and 1050 (for new-style and old-style modals, for example). +// z-index below is set above other assorted children of , which includes +// indexes such as 999 for modals. const cssSnackbarWrapper = styled('div', ` position: fixed; bottom: 8px; right: 8px; - z-index: 1100; + z-index: ${vars.notificationZIndex}; display: flex; flex-direction: column; diff --git a/app/client/ui/OnBoardingPopups.ts b/app/client/ui/OnBoardingPopups.ts index 20840ef4..bf0285af 100644 --- a/app/client/ui/OnBoardingPopups.ts +++ b/app/client/ui/OnBoardingPopups.ts @@ -326,7 +326,7 @@ const Container = styled('div', ` align-self: center; border: 2px solid ${theme.accentBorder}; border-radius: 3px; - z-index: 1000; + z-index: ${vars.onboardingPopupZIndex}; max-width: 490px; position: relative; background-color: ${theme.popupBg}; @@ -423,7 +423,7 @@ const Overlay = styled('div', ` height: 100%; top: 0; left: 0; - z-index: 999; + z-index: ${vars.onboardingBackdropZIndex}; overflow-y: auto; `); diff --git a/app/client/ui/UserManager.ts b/app/client/ui/UserManager.ts index 5b12fb1f..05b19fac 100644 --- a/app/client/ui/UserManager.ts +++ b/app/client/ui/UserManager.ts @@ -105,9 +105,14 @@ export function showUserManagerModal(userApi: UserAPI, options: IUserManagerOpti confirmModal( `You are about to remove your own access to this ${name}`, 'Remove my access', tryToSaveChanges, - 'Once you have removed your own access, ' + - 'you will not be able to get it back without assistance ' + - `from someone else with sufficient access to the ${name}.`); + { + explanation: ( + 'Once you have removed your own access, ' + + 'you will not be able to get it back without assistance ' + + `from someone else with sufficient access to the ${name}.` + ), + } + ); } else { tryToSaveChanges().catch(reportError); } diff --git a/app/client/ui/tooltips.ts b/app/client/ui/tooltips.ts index c14f92a5..9055e0fa 100644 --- a/app/client/ui/tooltips.ts +++ b/app/client/ui/tooltips.ts @@ -404,7 +404,7 @@ const cssColumnInfoTooltipButton = styled('div', ` const cssTooltip = styled('div', ` position: absolute; - z-index: 5000; /* should be higher than a modal */ + z-index: ${vars.tooltipZIndex}; /* should be higher than a modal */ background-color: ${theme.tooltipBg}; border-radius: 3px; box-shadow: 0 0 2px rgba(0,0,0,0.5); diff --git a/app/client/ui2018/cssVars.ts b/app/client/ui2018/cssVars.ts index 3958fef3..2595271e 100644 --- a/app/client/ui2018/cssVars.ts +++ b/app/client/ui2018/cssVars.ts @@ -134,6 +134,18 @@ export const vars = { logoBg: new CustomProp('logo-bg', '#040404'), logoSize: new CustomProp('logo-size', '22px 22px'), toastBg: new CustomProp('toast-bg', '#040404'), + + /* Z indexes */ + menuZIndex: new CustomProp('menu-z-index', '999'), + modalZIndex: new CustomProp('modal-z-index', '999'), + onboardingBackdropZIndex: new CustomProp('onboarding-backdrop-z-index', '999'), + onboardingPopupZIndex: new CustomProp('onboarding-popup-z-index', '1000'), + floatingPopupZIndex: new CustomProp('floating-popup-z-index', '1002'), + tutorialModalZIndex: new CustomProp('tutorial-modal-z-index', '1003'), + notificationZIndex: new CustomProp('notification-z-index', '1100'), + browserCheckZIndex: new CustomProp('browser-check-z-index', '5000'), + tooltipZIndex: new CustomProp('tooltip-z-index', '5000'), + // TODO: Add properties for remaining hard-coded z-indexes. }; /** diff --git a/app/client/ui2018/menus.ts b/app/client/ui2018/menus.ts index a3efd59a..6ae329c3 100644 --- a/app/client/ui2018/menus.ts +++ b/app/client/ui2018/menus.ts @@ -65,7 +65,7 @@ export const cssMenuElem = styled('div', ` padding: 8px 0px 16px 0px; box-shadow: 0 2px 20px 0 ${theme.menuShadow}; min-width: 160px; - z-index: 999; + z-index: ${vars.menuZIndex}; --weaseljs-selected-background-color: ${theme.menuItemSelectedBg}; --weaseljs-menu-item-padding: 8px 24px; background-color: ${theme.menuBg}; diff --git a/app/client/ui2018/modals.ts b/app/client/ui2018/modals.ts index 69564815..bd2f1ce3 100644 --- a/app/client/ui2018/modals.ts +++ b/app/client/ui2018/modals.ts @@ -137,6 +137,8 @@ export interface IModalOptions { noEscapeKey?: boolean; // If set, clicking into background does not close dialog. noClickAway?: boolean; + // DOM arguments to pass to the modal backer. + backerDomArgs?: DomElementArg[]; // 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; @@ -174,7 +176,13 @@ export function modal( createFn: (ctl: IModalControl, owner: MultiHolder) => DomElementArg, options: IModalOptions = {} ): void { - const {noEscapeKey, noClickAway, refElement = document.body, variant = 'fade-in'} = options; + const { + noEscapeKey, + noClickAway, + refElement = document.body, + variant = 'fade-in', + backerDomArgs = [], + } = options; function doClose() { if (!modalDom.isConnected) { return; } @@ -242,6 +250,7 @@ export function modal( return dialogDom; }), noClickAway ? null : dom.on('click', () => close()), + ...backerDomArgs, ); document.body.appendChild(modalDom); @@ -293,7 +302,10 @@ export interface ISaveModalOptions { * 3. saveFunc: () => doSomething().catch((e) => { alert("BOOM"); throw new StayOpen(); }) * If doSomething fails, an alert is shown, and the dialog stays open. */ -export function saveModal(createFunc: (ctl: IModalControl, owner: MultiHolder) => ISaveModalOptions) { +export function saveModal( + createFunc: (ctl: IModalControl, owner: MultiHolder) => ISaveModalOptions, + modalOptions?: IModalOptions +) { return modal((ctl, owner) => { const options = createFunc(ctl, owner); @@ -321,7 +333,14 @@ export function saveModal(createFunc: (ctl: IModalControl, owner: MultiHolder) = options.width && cssModalWidth(options.width), options.modalArgs, ]; - }); + }, modalOptions); +} + +export interface ConfirmModalOptions { + explanation?: DomElementArg, + hideCancel?: boolean; + extraButtons?: DomContents; + modalOptions?: IModalOptions; } /** @@ -333,8 +352,7 @@ export function confirmModal( title: DomElementArg, btnText: DomElementArg, onConfirm: () => Promise, - explanation?: DomElementArg, - {hideCancel, extraButtons}: {hideCancel?: boolean, extraButtons?: DomContents} = {}, + {explanation, hideCancel, extraButtons, modalOptions}: ConfirmModalOptions = {}, ): void { return saveModal((ctl, owner): ISaveModalOptions => ({ title, @@ -344,7 +362,7 @@ export function confirmModal( hideCancel, width: 'normal', extraButtons, - })); + }), modalOptions); } @@ -592,7 +610,7 @@ const cssModalBacker = styled('div', ` top: 0; left: 0; padding: 16px; - z-index: 999; + z-index: ${vars.modalZIndex}; background-color: ${theme.modalBackdrop}; overflow-y: auto; animation-name: ${cssFadeIn}; diff --git a/test/nbrowser/DocTutorial.ts b/test/nbrowser/DocTutorial.ts index f6ac4f42..198d0a94 100644 --- a/test/nbrowser/DocTutorial.ts +++ b/test/nbrowser/DocTutorial.ts @@ -354,7 +354,12 @@ describe('DocTutorial', function () { it('supports navigating to a specific slide', async function() { const slide3 = await driver.find('.test-doc-tutorial-popup-slide-3'); - assert.equal(await slide3.getAttribute('title'), 'Adding Columns and Rows'); + await slide3.mouseMove(); + await gu.waitToPass( + async () => assert.isTrue(await driver.find('.test-tooltip').isDisplayed()), + 500 + ); + assert.equal(await driver.find('.test-tooltip').getText(), 'Adding Columns and Rows'); await slide3.click(); await driver.find('.test-doc-tutorial-popup-slide-3').click(); assert.equal( @@ -368,7 +373,12 @@ describe('DocTutorial', function () { ); const slide1 = await driver.find('.test-doc-tutorial-popup-slide-1'); - assert.equal(await slide1.getAttribute('title'), 'Intro'); + await slide1.mouseMove(); + await gu.waitToPass( + async () => assert.isTrue(await driver.find('.test-tooltip').isDisplayed()), + 500 + ); + assert.equal(await driver.find('.test-tooltip').getText(), 'Intro'); await slide1.click(); assert.equal( await driver.find('.test-doc-tutorial-popup h1').getText(), @@ -392,11 +402,11 @@ describe('DocTutorial', function () { assert.isFalse(await driver.find('.test-doc-tutorial-lightbox').isPresent()); }); - it('can be minimized and maximized', async function() { + it('can be minimized and maximized by clicking a button in the header', async function() { await driver.find('.test-floating-popup-minimize-maximize').click(); assert.isTrue(await driver.find('.test-doc-tutorial-popup-header').isDisplayed()); - assert.isFalse(await driver.find('.test-doc-tutorial-popup-body').isPresent()); - assert.isFalse(await driver.find('.test-doc-tutorial-popup-footer').isPresent()); + assert.isFalse(await driver.find('.test-doc-tutorial-popup-body').isDisplayed()); + assert.isFalse(await driver.find('.test-doc-tutorial-popup-footer').isDisplayed()); await driver.find('.test-floating-popup-minimize-maximize').click(); assert.isTrue(await driver.find('.test-doc-tutorial-popup-header').isDisplayed()); @@ -404,6 +414,18 @@ describe('DocTutorial', function () { assert.isTrue(await driver.find('.test-doc-tutorial-popup-footer').isDisplayed()); }); + it('can be minimized and maximized by double clicking the header', async function() { + await driver.withActions(a => a.doubleClick(driver.find('.test-floating-popup-title'))); + assert.isTrue(await driver.find('.test-doc-tutorial-popup-header').isDisplayed()); + assert.isFalse(await driver.find('.test-doc-tutorial-popup-body').isDisplayed()); + assert.isFalse(await driver.find('.test-doc-tutorial-popup-footer').isDisplayed()); + + await driver.withActions(a => a.doubleClick(driver.find('.test-floating-popup-title'))); + assert.isTrue(await driver.find('.test-doc-tutorial-popup-header').isDisplayed()); + assert.isTrue(await driver.find('.test-doc-tutorial-popup-body').isDisplayed()); + assert.isTrue(await driver.find('.test-doc-tutorial-popup-footer').isDisplayed()); + }); + it('does not play an easter egg when opening an anchor link encoded with rr', async function() { await gu.getCell({rowNum: 1, col: 0}).click(); const link = await gu.getAnchor();