Skip to content

Commit

Permalink
fix(tooltip): allow multiple tooltips per grid cell (#1448)
Browse files Browse the repository at this point in the history
* fix(tooltip): allow multiple tooltips per grid cell
- prior to this PR, we could only have 1 tooltip per cell, but in some occasions, we might have a cell with multiple icons & tooltips, if that happens then we should have separate tooltips (with their dedicated & different tooltipt text) positioned on that inner icon
  • Loading branch information
ghiscoding authored Mar 31, 2024
1 parent 863933f commit 061c4a0
Show file tree
Hide file tree
Showing 18 changed files with 476 additions and 112 deletions.
4 changes: 4 additions & 0 deletions docs/events/Available-Events.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,13 @@ handleOnHeaderMenuCommand(e) {
- `onHeaderContextMenu`
- `onHeaderMouseEnter`
- `onHeaderMouseLeave`
- `onHeaderMouseOver`
- `onHeaderMouseOut`
- `onHeaderRowCellRendered`
- `onHeaderRowMouseEnter`
- `onHeaderRowMouseLeave`
- `onHeaderRowMouseOver`
- `onHeaderRowMouseOut`
- `onKeyDown`
- `onMouseEnter`
- `onMouseLeave`
Expand Down
3 changes: 2 additions & 1 deletion examples/vite-demo-vanilla-bundle/src/examples/example11.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
formatNumber,
} from '@slickgrid-universal/common';
import { BindingEventService } from '@slickgrid-universal/binding';
import { SlickCustomTooltip } from '@slickgrid-universal/custom-tooltip-plugin';
import { ExcelExportService } from '@slickgrid-universal/excel-export';
import { Slicker, type SlickVanillaGridBundle } from '@slickgrid-universal/vanilla-bundle';
import moment from 'moment-mini';
Expand Down Expand Up @@ -303,7 +304,7 @@ export default class Example11 {
excelExportOptions: {
exportWithFormatter: true
},
externalResources: [new ExcelExportService()],
externalResources: [new ExcelExportService(), new SlickCustomTooltip()],
enableFiltering: true,
rowSelectionOptions: {
// True (Single Selection), False (Multiple Selections)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export default class Example12 {
initializeGrid() {
this.columnDefinitions = [
{
id: 'title', name: '<span title="Task must always be followed by a number" class="color-info mdi mdi-alert-circle"></span> Title', field: 'title', sortable: true, type: FieldType.string, minWidth: 75,
id: 'title', name: '<span title="Task must always be followed by a number" class="color-warning-dark mdi mdi-alert-outline"></span> Title <span title="Title is always rendered as UPPERCASE" class="mdi mdi-information-outline"></span>', field: 'title', sortable: true, type: FieldType.string, minWidth: 75,
cssClass: 'text-bold text-uppercase',
filterable: true, columnGroup: 'Common Factor',
filter: { model: Filters.compoundInputText },
Expand Down
3 changes: 3 additions & 0 deletions examples/vite-demo-vanilla-bundle/src/examples/example16.scss
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@

// it's preferable to use CSS Variables (or SASS) but if you want to change colors of your tooltip for 1 column in particular you can do it this way
// e.g. change css of 5th column 4 (zero index: l4)
.l4 {
--slick-tooltip-color: #fff;
}
.l4 .header-tooltip-title,
.l4 .headerrow-tooltip-title {
color: #ffffff;
Expand Down
16 changes: 11 additions & 5 deletions examples/vite-demo-vanilla-bundle/src/examples/example16.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export default class Example16 {
// define tooltip options here OR for the entire grid via the grid options (cell tooltip options will have precedence over grid options)
customTooltip: {
useRegularTooltip: true, // note regular tooltip will try to find a "title" attribute in the cell formatter (it won't work without a cell formatter)
useRegularTooltipFromCellTextOnly: true,
},
},
{
Expand Down Expand Up @@ -168,7 +169,12 @@ export default class Example16 {
formatter: Formatters.percentCompleteBar,
sortable: true, filterable: true,
filter: { model: Filters.sliderRange, operator: '>=', filterOptions: { hideSliderNumbers: true } as SliderRangeOption },
customTooltip: { position: 'center', formatter: (_row, _cell, value) => `${value}%`, headerFormatter: null as any, headerRowFormatter: null as any },
customTooltip: {
position: 'center',
formatter: (_row, _cell, value) => typeof value === 'string' && value.includes('%') ? value : `${value}%`,
headerFormatter: undefined,
headerRowFormatter: undefined
},
},
{
id: 'start', name: 'Start', field: 'start', sortable: true,
Expand Down Expand Up @@ -459,12 +465,12 @@ export default class Example16 {

tooltipFormatter(row, cell, _value, column, dataContext, grid) {
const tooltipTitle = 'Custom Tooltip';
const effortDrivenHtml = Formatters.checkmarkMaterial(row, cell, dataContext.effortDriven, column, dataContext, grid);
const effortDrivenHtml = Formatters.checkmarkMaterial(row, cell, dataContext.effortDriven, column, dataContext, grid) as HTMLElement;

return `<div class="header-tooltip-title">${tooltipTitle}</div>
<div class="tooltip-2cols-row"><div>Id:</div> <div>${dataContext.id}</div></div>
<div class="tooltip-2cols-row"><div>Title:</div> <div>${dataContext.title}</div></div>
<div class="tooltip-2cols-row"><div>Effort Driven:</div> <div>${effortDrivenHtml}</div></div>
<div class="tooltip-2cols-row"><div>Effort Driven:</div> <div>${effortDrivenHtml.outerHTML || ''}</div></div>
<div class="tooltip-2cols-row"><div>Completion:</div> <div>${this.loadCompletionIcons(dataContext.percentComplete)}</div></div>
`;
}
Expand All @@ -474,9 +480,9 @@ export default class Example16 {

// use a 2nd Formatter to get the percent completion
// any properties provided from the `asyncPost` will end up in the `__params` property (unless a different prop name is provided via `asyncParamsPropName`)
const completionBar = Formatters.percentCompleteBarWithText(row, cell, dataContext.percentComplete, column, dataContext, grid);
const completionBar = Formatters.percentCompleteBarWithText(row, cell, dataContext.percentComplete, column, dataContext, grid) as HTMLElement;
const out = `<div class="color-sf-primary-dark header-tooltip-title">${tooltipTitle}</div>
<div class="tooltip-2cols-row"><div>Completion:</div> <div>${completionBar}</div></div>
<div class="tooltip-2cols-row"><div>Completion:</div> <div>${completionBar.outerHTML || ''}</div></div>
<div class="tooltip-2cols-row"><div>Lifespan:</div> <div>${dataContext.__params.lifespan.toFixed(2)}</div></div>
<div class="tooltip-2cols-row"><div>Ratio:</div> <div>${dataContext.__params.ratio.toFixed(2)}</div></div>
`;
Expand Down
12 changes: 7 additions & 5 deletions examples/vite-demo-vanilla-bundle/src/examples/example22.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
type GridOption,
Editors,
} from '@slickgrid-universal/common';
import { SlickCustomTooltip } from '@slickgrid-universal/custom-tooltip-plugin';
import { Slicker, type SlickVanillaGridBundle } from '@slickgrid-universal/vanilla-bundle';
import { ExampleGridOptions } from './example-grid-options';

Expand Down Expand Up @@ -173,33 +174,34 @@ export default class Example22 {
maxWidth: 100,
},
actionButtons: {
editButtonClassName: 'button-style padding-1px mr-2',
editButtonClassName: 'button-style padding-3px mr-2',
iconEditButtonClassName: 'mdi mdi-pencil',
// since no title and no titleKey is provided, it will fallback to the default text provided by the plugin
// if the title is provided but no titleKey, it will override the default text
// last but not least if a titleKey is provided, it will use the translation key to translate the text
// editButtonTitle: 'Edit row',

cancelButtonClassName: 'button-style padding-1px',
cancelButtonClassName: 'button-style padding-3px',
cancelButtonTitle: 'Cancel row',
cancelButtonTitleKey: 'RBE_BTN_CANCEL',
iconCancelButtonClassName: 'mdi mdi-undo color-danger',
cancelButtonPrompt: 'Are you sure you want to cancel your changes?',

updateButtonClassName: 'button-style padding-1px mr-2',
updateButtonClassName: 'button-style padding-3px mr-2',
updateButtonTitle: 'Update row',
updateButtonTitleKey: 'RBE_BTN_UPDATE',
iconUpdateButtonClassName: 'mdi mdi-check color-success',
updateButtonPrompt: 'Save changes?',

deleteButtonClassName: 'button-style padding-1px',
deleteButtonClassName: 'button-style padding-3px',
deleteButtonTitle: 'Delete row',
iconDeleteButtonClassName: 'mdi mdi-trash-can color-danger',
deleteButtonPrompt: 'Are you sure you want to delete this row?',
},
},
enableTranslate: true,
translater: this.translateService
translater: this.translateService,
externalResources: [new SlickCustomTooltip()]
};
}

Expand Down
80 changes: 80 additions & 0 deletions packages/common/src/core/__tests__/slickGrid.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5120,6 +5120,26 @@ describe('SlickGrid core file', () => {
expect(onHeaderMouseEnterSpy).not.toHaveBeenCalled();
});

it('should trigger onHeaderMouseOver notify when hovering a header', () => {
const columns = [{ id: 'name', field: 'name', name: 'Name' }, { id: 'age', field: 'age', name: 'Age' }] as Column[];
grid = new SlickGrid<any, Column>(container, items, columns, { ...defaultOptions, enableCellNavigation: true });
const onHeaderMouseOverSpy = jest.spyOn(grid.onHeaderMouseOver, 'notify');
container.querySelector('.slick-header-column')!.dispatchEvent(new CustomEvent('mouseover'));

expect(onHeaderMouseOverSpy).toHaveBeenCalled();
});

it('should NOT trigger onHeaderMouseOver notify when hovering a header when "slick-header-column" class is not found', () => {
const columns = [{ id: 'name', field: 'name', name: 'Name' }, { id: 'age', field: 'age', name: 'Age' }] as Column[];
grid = new SlickGrid<any, Column>(container, items, columns, { ...defaultOptions, enableCellNavigation: true });
const onHeaderMouseOverSpy = jest.spyOn(grid.onHeaderMouseOver, 'notify');
const headerRowElm = container.querySelector('.slick-header-column');
headerRowElm!.classList.remove('slick-header-column');
headerRowElm!.dispatchEvent(new CustomEvent('mouseover'));

expect(onHeaderMouseOverSpy).not.toHaveBeenCalled();
});

it('should trigger onHeaderMouseLeave notify when leaving the hovering of a header when "slick-header-column" class is not found', () => {
const columns = [{ id: 'name', field: 'name', name: 'Name' }, { id: 'age', field: 'age', name: 'Age' }] as Column[];
grid = new SlickGrid<any, Column>(container, items, columns, { ...defaultOptions, enableCellNavigation: true });
Expand All @@ -5140,6 +5160,26 @@ describe('SlickGrid core file', () => {
expect(onHeaderMouseLeaveSpy).not.toHaveBeenCalled();
});

it('should trigger onHeaderMouseOut notify when leaving the hovering of a header when "slick-header-column" class is not found', () => {
const columns = [{ id: 'name', field: 'name', name: 'Name' }, { id: 'age', field: 'age', name: 'Age' }] as Column[];
grid = new SlickGrid<any, Column>(container, items, columns, { ...defaultOptions, enableCellNavigation: true });
const onHeaderMouseOutSpy = jest.spyOn(grid.onHeaderMouseOut, 'notify');
container.querySelector('.slick-header-column')!.dispatchEvent(new CustomEvent('mouseout'));

expect(onHeaderMouseOutSpy).toHaveBeenCalled();
});

it('should NOT trigger onHeaderMouseOut notify when leaving the hovering of a header', () => {
const columns = [{ id: 'name', field: 'name', name: 'Name' }, { id: 'age', field: 'age', name: 'Age' }] as Column[];
grid = new SlickGrid<any, Column>(container, items, columns, { ...defaultOptions, enableCellNavigation: true });
const onHeaderMouseOutSpy = jest.spyOn(grid.onHeaderMouseOut, 'notify');
const headerRowElm = container.querySelector('.slick-header-column');
headerRowElm!.classList.remove('slick-header-column');
headerRowElm!.dispatchEvent(new CustomEvent('mouseout'));

expect(onHeaderMouseOutSpy).not.toHaveBeenCalled();
});

it('should trigger onHeaderRowMouseEnter notify when hovering a header', () => {
const columns = [{ id: 'name', field: 'name', name: 'Name' }, { id: 'age', field: 'age', name: 'Age' }] as Column[];
grid = new SlickGrid<any, Column>(container, items, columns, { ...defaultOptions, showHeaderRow: true, enableCellNavigation: true });
Expand All @@ -5149,6 +5189,15 @@ describe('SlickGrid core file', () => {
expect(onHeaderRowMouseEnterSpy).toHaveBeenCalled();
});

it('should trigger onHeaderRowMouseOver notify when hovering a header', () => {
const columns = [{ id: 'name', field: 'name', name: 'Name' }, { id: 'age', field: 'age', name: 'Age' }] as Column[];
grid = new SlickGrid<any, Column>(container, items, columns, { ...defaultOptions, showHeaderRow: true, enableCellNavigation: true });
const onHeaderRowMouseOverSpy = jest.spyOn(grid.onHeaderRowMouseOver, 'notify');
container.querySelector('.slick-headerrow-column')!.dispatchEvent(new CustomEvent('mouseover'));

expect(onHeaderRowMouseOverSpy).toHaveBeenCalled();
});

it('should update viewport top/left scrollLeft when scrolling in headerRow DOM element', () => {
const columns = [{ id: 'name', field: 'name', name: 'Name' }, { id: 'age', field: 'age', name: 'Age' }] as Column[];
grid = new SlickGrid<any, Column>(container, items, columns, { ...defaultOptions, showHeaderRow: true, enableCellNavigation: true });
Expand Down Expand Up @@ -5202,6 +5251,17 @@ describe('SlickGrid core file', () => {
expect(onHeaderRowMouseEnterSpy).not.toHaveBeenCalled();
});

it('should NOT trigger onHeaderRowMouseOver notify when hovering a header when "slick-headerrow-column" class is not found', () => {
const columns = [{ id: 'name', field: 'name', name: 'Name' }, { id: 'age', field: 'age', name: 'Age' }] as Column[];
grid = new SlickGrid<any, Column>(container, items, columns, { ...defaultOptions, showHeaderRow: true, enableCellNavigation: true });
const onHeaderRowMouseOverSpy = jest.spyOn(grid.onHeaderRowMouseOver, 'notify');
const headerRowElm = container.querySelector('.slick-headerrow-column');
headerRowElm!.classList.remove('slick-headerrow-column');
headerRowElm!.dispatchEvent(new CustomEvent('mouseover'));

expect(onHeaderRowMouseOverSpy).not.toHaveBeenCalled();
});

it('should trigger onHeaderRowMouseLeave notify when leaving the hovering of a header', () => {
const columns = [{ id: 'name', field: 'name', name: 'Name' }, { id: 'age', field: 'age', name: 'Age' }] as Column[];
grid = new SlickGrid<any, Column>(container, items, columns, { ...defaultOptions, showHeaderRow: true, enableCellNavigation: true });
Expand All @@ -5221,6 +5281,26 @@ describe('SlickGrid core file', () => {

expect(onHeaderRowMouseLeaveSpy).not.toHaveBeenCalled();
});

it('should trigger onHeaderRowMouseOut notify when leaving the hovering of a header', () => {
const columns = [{ id: 'name', field: 'name', name: 'Name' }, { id: 'age', field: 'age', name: 'Age' }] as Column[];
grid = new SlickGrid<any, Column>(container, items, columns, { ...defaultOptions, showHeaderRow: true, enableCellNavigation: true });
const onHeaderRowMouseOutSpy = jest.spyOn(grid.onHeaderRowMouseOut, 'notify');
container.querySelector('.slick-headerrow-column')!.dispatchEvent(new CustomEvent('mouseout'));

expect(onHeaderRowMouseOutSpy).toHaveBeenCalled();
});

it('should NOT trigger onHeaderRowMouseOut notify when leaving the hovering of a header when "slick-headerrow-column" class is not found', () => {
const columns = [{ id: 'name', field: 'name', name: 'Name' }, { id: 'age', field: 'age', name: 'Age' }] as Column[];
grid = new SlickGrid<any, Column>(container, items, columns, { ...defaultOptions, showHeaderRow: true, enableCellNavigation: true });
const onHeaderRowMouseOutSpy = jest.spyOn(grid.onHeaderRowMouseOut, 'notify');
const headerRowElm = container.querySelector('.slick-headerrow-column');
headerRowElm!.classList.remove('slick-headerrow-column');
headerRowElm!.dispatchEvent(new CustomEvent('mouseout'));

expect(onHeaderRowMouseOutSpy).not.toHaveBeenCalled();
});
});

describe('Footer Click', () => {
Expand Down
Loading

0 comments on commit 061c4a0

Please sign in to comment.