Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(tooltip): allow multiple tooltips per grid cell #1448

Merged
merged 9 commits into from
Mar 31, 2024
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
Loading