mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(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:
parent
b312b3b08b
commit
86681de595
@ -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')
|
||||||
|
@ -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;
|
||||||
|
@ -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'),
|
||||||
|
@ -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/);
|
||||||
|
Loading…
Reference in New Issue
Block a user