From 86681de595e1da8c70307a9ee42d695a0fe4d29a Mon Sep 17 00:00:00 2001 From: Dmitry S Date: Mon, 13 Mar 2023 03:03:59 -0400 Subject: [PATCH] (core) Add overflowTooltip() tool, and use for long tables in widget picker, and long page names. Summary: Usage is simply to call `overflowTooltip()` with no arguments, as an argument to an element whose text may overflow. On 'mouseenter', it'll check for overflow and show the element's .textContent in a tooltip. - Added for long table names in the widget picker (Add Page, Add Widget to Page). - Added for long page names in the left-panel list of pages. Test Plan: Added test cases for the new overflow tooltips Reviewers: jarek Reviewed By: jarek Differential Revision: https://phab.getgrist.com/D3814 --- app/client/ui/PageWidgetPicker.ts | 3 ++- app/client/ui/tooltips.ts | 36 +++++++++++++++++++++++++++++-- app/client/ui2018/pages.ts | 3 ++- test/nbrowser/Pages.ts | 20 +++++++++++++++++ 4 files changed, 58 insertions(+), 4 deletions(-) diff --git a/app/client/ui/PageWidgetPicker.ts b/app/client/ui/PageWidgetPicker.ts index 8f4b278f..81ce82fe 100644 --- a/app/client/ui/PageWidgetPicker.ts +++ b/app/client/ui/PageWidgetPicker.ts @@ -8,6 +8,7 @@ import { linkId, NoLink } from 'app/client/ui/selectBy'; import { withInfoTooltip } from 'app/client/ui/tooltips'; import { getWidgetTypes, IWidgetType } from 'app/client/ui/widgetTypes'; import { bigPrimaryButton } from "app/client/ui2018/buttons"; +import { overflowTooltip } from "app/client/ui/tooltips"; import { theme, vars } from "app/client/ui2018/cssVars"; import { icon } from "app/client/ui2018/icons"; import { spinnerModal } from 'app/client/ui2018/modals'; @@ -321,7 +322,7 @@ export class PageWidgetSelect extends Disposable { dom.forEach(this._tables, (table) => dom('div', cssEntryWrapper( cssEntry(cssIcon('TypeTable'), - cssLabel(dom.text(use => use(table.tableNameDef) || use(table.tableId))), + cssLabel(dom.text(table.tableNameDef), overflowTooltip()), dom.on('click', () => this._selectTable(table.id())), cssEntry.cls('-selected', (use) => use(this._value.table) === table.id()), testId('table-label') diff --git a/app/client/ui/tooltips.ts b/app/client/ui/tooltips.ts index af2bb767..c14f92a5 100644 --- a/app/client/ui/tooltips.ts +++ b/app/client/ui/tooltips.ts @@ -12,6 +12,7 @@ import {menuCssClass} from 'app/client/ui2018/menus'; import {dom, DomContents, DomElementArg, DomElementMethod, styled} from 'grainjs'; import Popper from 'popper.js'; import {cssMenu, defaultMenuOptions, IMenuOptions, setPopupToCreateDom} from 'popweasel'; +import merge = require('lodash/merge'); export interface ITipOptions { /** @@ -25,6 +26,12 @@ export interface ITipOptions { /** When set, a tooltip will replace any previous tooltip with the same key. */ key?: string; + + /** + * Optionally, popper modifiers (e.g. {offset: {offset: 8}}), + * See https://popper.js.org/docs/v1/#modifiers. + */ + modifiers?: Popper.Modifiers; } export interface ITransientTipOptions extends ITipOptions { @@ -66,6 +73,9 @@ export interface IHoverTipOptions extends ITransientTipOptions { * Should only be set to true if `openOnClick` is false. */ closeOnClick?: boolean; + + /** Whether to show the tooltip only when the ref element overflows horizontally. */ + overflowOnly?: boolean; } export type ITooltipContent = ITooltipContentFunc | DomContents; @@ -132,9 +142,13 @@ export function showTooltip( // Create a popper for positioning the tooltip content relative to refElem. const popperOptions: Popper.PopperOptions = { - modifiers: {preventOverflow: {boundariesElement: 'viewport'}}, + modifiers: merge( + { preventOverflow: {boundariesElement: 'viewport'} }, + options.modifiers + ), placement, }; + const popper = new Popper(refElem, content, popperOptions); // If refElem is disposed we close the tooltip. @@ -159,6 +173,20 @@ export function hoverTooltip(tipContent: ITooltipContent, options?: IHoverTipOpt return (elem) => setHoverTooltip(elem, tipContent, {...defaultOptions, ...options}); } +/** + * On hover, show the full text of this element when it overflows horizontally. It is intended + * mainly for styled with "text-overflow: ellipsis". + * E.g. dom('label', 'Long text...', overflowTooltip()). + */ +export function overflowTooltip(options?: IHoverTipOptions): DomElementMethod { + const defaultOptions: IHoverTipOptions = { + placement: 'bottom-start', + overflowOnly: true, + modifiers: {offset: {offset: '40, 0'}}, + }; + return (elem) => setHoverTooltip(elem, () => elem.textContent, {...defaultOptions, ...options}); +} + /** * Attach a tooltip to the given element, to be rendered on hover. */ @@ -167,7 +195,8 @@ export function setHoverTooltip( tipContent: ITooltipContent, options: IHoverTipOptions = {} ) { - const {key, openDelay = 200, timeoutMs, closeDelay = 100, openOnClick, closeOnClick = true} = options; + const {key, openDelay = 200, timeoutMs, closeDelay = 100, openOnClick, closeOnClick = true, + overflowOnly = false} = options; const tipContentFunc = typeof tipContent === 'function' ? tipContent : () => tipContent; @@ -204,6 +233,9 @@ export function setHoverTooltip( // We simulate hover effect by handling mouseenter/mouseleave. dom.onElem(refElem, 'mouseenter', () => { + if (overflowOnly && (refElem as HTMLElement).offsetWidth >= refElem.scrollWidth) { + return; + } if (!tipControl && !timer) { // If we're replacing a tooltip, open without delay. const delay = key && openTooltips.has(key) ? 0 : openDelay; diff --git a/app/client/ui2018/pages.ts b/app/client/ui2018/pages.ts index 5a7cef71..296afa14 100644 --- a/app/client/ui2018/pages.ts +++ b/app/client/ui2018/pages.ts @@ -4,7 +4,7 @@ import { cssEditorInput } from "app/client/ui/HomeLeftPane"; import { itemHeader, itemHeaderWrapper, treeViewContainer } from "app/client/ui/TreeViewComponentCss"; import { theme } from "app/client/ui2018/cssVars"; import { icon } from "app/client/ui2018/icons"; -import { hoverTooltip } from 'app/client/ui/tooltips'; +import { hoverTooltip, overflowTooltip } from 'app/client/ui/tooltips'; import { menu, menuItem, menuText } from "app/client/ui2018/menus"; import { dom, domComputed, DomElementArg, makeTestId, observable, Observable, styled } from "grainjs"; @@ -88,6 +88,7 @@ export function buildPageDom(name: Observable, actions: PageActions, ... dom.text(name), testId('label'), dom.on('click', (ev) => isTargetSelected(ev.target as HTMLElement) && isRenaming.set(true)), + overflowTooltip(), ), cssPageMenuTrigger( cssPageMenuIcon('Dots'), diff --git a/test/nbrowser/Pages.ts b/test/nbrowser/Pages.ts index c9144ec3..1b786e9b 100644 --- a/test/nbrowser/Pages.ts +++ b/test/nbrowser/Pages.ts @@ -259,6 +259,26 @@ describe('Pages', function() { assert.include(await gu.getPageNames(), 'People'); }); + it('should show tooltip for long page names on hover', async () => { + await gu.openPageMenu('People'); + await driver.find('.test-docpage-rename').doClick(); + await driver.find('.test-docpage-editor') + .sendKeys('People, Persons, Humans, Ladies & Gentlemen', Key.ENTER); + await gu.waitForServer(); + + await driver.findContent('.test-treeview-label', /People, Persons, Humans, Ladies & Gentlemen/).mouseMove(); + await driver.wait(() => driver.findWait('.test-tooltip', 1000).isDisplayed(), 3000); + assert.equal(await driver.find('.test-tooltip').getText(), + 'People, Persons, Humans, Ladies & Gentlemen'); + + await gu.undo(); + assert.deepEqual(await gu.getPageNames(), ['Interactions', 'Documents', 'People', 'User & Leads', 'Overview']); + + await driver.findContent('.test-treeview-label', /People/).mouseMove(); + await driver.sleep(500); + assert.equal(await driver.find('.test-tooltip').isPresent(), false); + }); + it('should not change page when clicking the input while renaming page', async () => { // check that initially People is selected assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /People/);