(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
This commit is contained in:
Dmitry S 2023-03-13 03:03:59 -04:00
parent b312b3b08b
commit 86681de595
4 changed files with 58 additions and 4 deletions

View File

@ -8,6 +8,7 @@ import { linkId, NoLink } from 'app/client/ui/selectBy';
import { withInfoTooltip } from 'app/client/ui/tooltips'; import { withInfoTooltip } from 'app/client/ui/tooltips';
import { getWidgetTypes, IWidgetType } from 'app/client/ui/widgetTypes'; import { getWidgetTypes, IWidgetType } from 'app/client/ui/widgetTypes';
import { bigPrimaryButton } from "app/client/ui2018/buttons"; import { bigPrimaryButton } from "app/client/ui2018/buttons";
import { overflowTooltip } from "app/client/ui/tooltips";
import { theme, vars } from "app/client/ui2018/cssVars"; import { theme, vars } from "app/client/ui2018/cssVars";
import { icon } from "app/client/ui2018/icons"; import { icon } from "app/client/ui2018/icons";
import { spinnerModal } from 'app/client/ui2018/modals'; import { spinnerModal } from 'app/client/ui2018/modals';
@ -321,7 +322,7 @@ export class PageWidgetSelect extends Disposable {
dom.forEach(this._tables, (table) => dom('div', dom.forEach(this._tables, (table) => dom('div',
cssEntryWrapper( cssEntryWrapper(
cssEntry(cssIcon('TypeTable'), 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())), dom.on('click', () => this._selectTable(table.id())),
cssEntry.cls('-selected', (use) => use(this._value.table) === table.id()), cssEntry.cls('-selected', (use) => use(this._value.table) === table.id()),
testId('table-label') testId('table-label')

View File

@ -12,6 +12,7 @@ import {menuCssClass} from 'app/client/ui2018/menus';
import {dom, DomContents, DomElementArg, DomElementMethod, styled} from 'grainjs'; import {dom, DomContents, DomElementArg, DomElementMethod, styled} from 'grainjs';
import Popper from 'popper.js'; import Popper from 'popper.js';
import {cssMenu, defaultMenuOptions, IMenuOptions, setPopupToCreateDom} from 'popweasel'; import {cssMenu, defaultMenuOptions, IMenuOptions, setPopupToCreateDom} from 'popweasel';
import merge = require('lodash/merge');
export interface ITipOptions { export interface ITipOptions {
/** /**
@ -25,6 +26,12 @@ export interface ITipOptions {
/** When set, a tooltip will replace any previous tooltip with the same key. */ /** When set, a tooltip will replace any previous tooltip with the same key. */
key?: string; 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 { export interface ITransientTipOptions extends ITipOptions {
@ -66,6 +73,9 @@ export interface IHoverTipOptions extends ITransientTipOptions {
* Should only be set to true if `openOnClick` is false. * Should only be set to true if `openOnClick` is false.
*/ */
closeOnClick?: boolean; closeOnClick?: boolean;
/** Whether to show the tooltip only when the ref element overflows horizontally. */
overflowOnly?: boolean;
} }
export type ITooltipContent = ITooltipContentFunc | DomContents; export type ITooltipContent = ITooltipContentFunc | DomContents;
@ -132,9 +142,13 @@ export function showTooltip(
// Create a popper for positioning the tooltip content relative to refElem. // Create a popper for positioning the tooltip content relative to refElem.
const popperOptions: Popper.PopperOptions = { const popperOptions: Popper.PopperOptions = {
modifiers: {preventOverflow: {boundariesElement: 'viewport'}}, modifiers: merge(
{ preventOverflow: {boundariesElement: 'viewport'} },
options.modifiers
),
placement, placement,
}; };
const popper = new Popper(refElem, content, popperOptions); const popper = new Popper(refElem, content, popperOptions);
// If refElem is disposed we close the tooltip. // 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}); 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. * Attach a tooltip to the given element, to be rendered on hover.
*/ */
@ -167,7 +195,8 @@ export function setHoverTooltip(
tipContent: ITooltipContent, tipContent: ITooltipContent,
options: IHoverTipOptions = {} 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; const tipContentFunc = typeof tipContent === 'function' ? tipContent : () => tipContent;
@ -204,6 +233,9 @@ export function setHoverTooltip(
// We simulate hover effect by handling mouseenter/mouseleave. // We simulate hover effect by handling mouseenter/mouseleave.
dom.onElem(refElem, 'mouseenter', () => { dom.onElem(refElem, 'mouseenter', () => {
if (overflowOnly && (refElem as HTMLElement).offsetWidth >= refElem.scrollWidth) {
return;
}
if (!tipControl && !timer) { if (!tipControl && !timer) {
// If we're replacing a tooltip, open without delay. // If we're replacing a tooltip, open without delay.
const delay = key && openTooltips.has(key) ? 0 : openDelay; const delay = key && openTooltips.has(key) ? 0 : openDelay;

View File

@ -4,7 +4,7 @@ import { cssEditorInput } from "app/client/ui/HomeLeftPane";
import { itemHeader, itemHeaderWrapper, treeViewContainer } from "app/client/ui/TreeViewComponentCss"; import { itemHeader, itemHeaderWrapper, treeViewContainer } from "app/client/ui/TreeViewComponentCss";
import { theme } from "app/client/ui2018/cssVars"; import { theme } from "app/client/ui2018/cssVars";
import { icon } from "app/client/ui2018/icons"; 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 { menu, menuItem, menuText } from "app/client/ui2018/menus";
import { dom, domComputed, DomElementArg, makeTestId, observable, Observable, styled } from "grainjs"; import { dom, domComputed, DomElementArg, makeTestId, observable, Observable, styled } from "grainjs";
@ -88,6 +88,7 @@ export function buildPageDom(name: Observable<string>, actions: PageActions, ...
dom.text(name), dom.text(name),
testId('label'), testId('label'),
dom.on('click', (ev) => isTargetSelected(ev.target as HTMLElement) && isRenaming.set(true)), dom.on('click', (ev) => isTargetSelected(ev.target as HTMLElement) && isRenaming.set(true)),
overflowTooltip(),
), ),
cssPageMenuTrigger( cssPageMenuTrigger(
cssPageMenuIcon('Dots'), cssPageMenuIcon('Dots'),

View File

@ -259,6 +259,26 @@ describe('Pages', function() {
assert.include(await gu.getPageNames(), 'People'); 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 () => { it('should not change page when clicking the input while renaming page', async () => {
// check that initially People is selected // check that initially People is selected
assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /People/); assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /People/);