Skip to content

Commit

Permalink
fix: allow multiple tooltips per grid cell
Browse files Browse the repository at this point in the history
- for some unknown reasons the tooltips are shown correctly in the browser but not in Cypress, I had to use a few force click
  • Loading branch information
ghiscoding committed Mar 31, 2024
1 parent 9df1932 commit 85cabd7
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 48 deletions.
3 changes: 2 additions & 1 deletion src/app/examples/grid-base-row-editing.component.ts
@@ -1,6 +1,6 @@

import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { SlickCustomTooltip } from '@slickgrid-universal/custom-tooltip-plugin';
import { Subscription } from 'rxjs';

import {
Expand Down Expand Up @@ -198,6 +198,7 @@ export class GridBaseRowEditingComponent implements OnInit {
deleteButtonPrompt: 'Are you sure you want to delete this row?',
},
},
externalResources: [new SlickCustomTooltip()],
};
}

Expand Down
8 changes: 5 additions & 3 deletions src/app/examples/grid-composite-editor.component.ts
@@ -1,6 +1,7 @@
import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ExcelExportService } from '@slickgrid-universal/excel-export';
import { SlickCustomTooltip } from '@slickgrid-universal/custom-tooltip-plugin';
import { SlickCompositeEditor, SlickCompositeEditorComponent } from '@slickgrid-universal/composite-editor-component';

import {
Expand Down Expand Up @@ -64,7 +65,7 @@ const customEditableInputFormatter: Formatter = (_row, _cell, value, columnDef,
const gridOptions = grid && grid.getOptions && grid.getOptions();
const isEditableLine = gridOptions.editable && columnDef.editor;
value = (value === null || value === undefined) ? '' : value;
return isEditableLine ? { text: value, addClasses: 'editable-field', toolTip: 'Click to Edit' } : value;
return isEditableLine ? { text: value, addClasses: 'editable-field' } : value;
};

// you can create custom validator to pass to an inline editor
Expand Down Expand Up @@ -131,7 +132,8 @@ export class GridCompositeEditorComponent implements OnDestroy, OnInit {
prepareGrid() {
this.columnDefinitions = [
{
id: 'title', name: '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 fa fa-exclamation-triangle"></span> Title <span title="Title is always rendered as UPPERCASE" class="fa fa-info-circle"></span>',
field: 'title', sortable: true, type: FieldType.string, minWidth: 75,
cssClass: 'text-uppercase fw-bold', columnGroup: 'Common Factor',
filterable: true, filter: { model: Filters.compoundInputText },
editor: {
Expand Down Expand Up @@ -397,7 +399,7 @@ export class GridCompositeEditorComponent implements OnDestroy, OnInit {
excelExportOptions: {
exportWithFormatter: false
},
externalResources: [new ExcelExportService(), this.compositeEditorInstance],
externalResources: [new ExcelExportService(), new SlickCustomTooltip(), this.compositeEditorInstance],
enableFiltering: true,
rowSelectionOptions: {
// True (Single Selection), False (Multiple Selections)
Expand Down
3 changes: 3 additions & 0 deletions src/app/examples/grid-custom-tooltip.component.scss
Expand Up @@ -35,6 +35,9 @@ $slick-button-border-color: #ababab !default;

// 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 src/app/examples/grid-custom-tooltip.component.ts
Expand Up @@ -110,6 +110,7 @@ export class GridCustomTooltipComponent implements OnInit {
// 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 @@ -158,7 +159,12 @@ export class GridCustomTooltipComponent implements OnInit {
formatter: Formatters.percentCompleteBar,
sortable: true, filterable: true,
filter: { model: Filters.slider, operator: '>=' },
customTooltip: { useRegularTooltip: true, },
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 @@ -434,12 +440,12 @@ export class GridCustomTooltipComponent implements OnInit {

tooltipFormatter(row: number, cell: number, value: any, column: Column, dataContext: any, grid: SlickGrid) {
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 @@ -449,9 +455,9 @@ export class GridCustomTooltipComponent implements OnInit {

// 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
21 changes: 19 additions & 2 deletions test/cypress/e2e/example30.cy.ts
Expand Up @@ -2,7 +2,7 @@ import { changeTimezone, zeroPadding } from '../plugins/utilities';

describe('Example 30 Composite Editor Modal', () => {
const fullPreTitles = ['', 'Common Factor', 'Analysis', 'Period', 'Item', ''];
const fullTitles = ['', 'Title', 'Duration', 'Cost', '% Complete', 'Complexity', 'Start', 'Completed', 'Finish', 'Product', 'Country of Origin', 'Action'];
const fullTitles = ['', ' Title ', 'Duration', 'Cost', '% Complete', 'Complexity', 'Start', 'Completed', 'Finish', 'Product', 'Country of Origin', 'Action'];

const GRID_ROW_HEIGHT = 35;
const EDITABLE_CELL_RGB_COLOR = 'rgba(227, 240, 251, 0.57)';
Expand Down Expand Up @@ -32,6 +32,23 @@ describe('Example 30 Composite Editor Modal', () => {
.each(($child, index) => expect($child.text()).to.eq(fullTitles[index]));
});

it('should display 2 different tooltips when hovering icons on "Title" column', () => {
cy.get('.slick-column-name').as('title-column');
cy.get('@title-column')
.find('.fa-exclamation-triangle')
.trigger('mouseover');

cy.get('.slick-custom-tooltip').should('be.visible');
cy.get('.slick-custom-tooltip .tooltip-body').contains('Task must always be followed by a number');

cy.get('@title-column')
.find('.fa-info-circle')
.trigger('mouseover');

cy.get('.slick-custom-tooltip').should('be.visible');
cy.get('.slick-custom-tooltip .tooltip-body').contains('Title is always rendered as UPPERCASE');
});

it('should have "TASK 0" (uppercase) incremented by 1 after each row', () => {
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).contains('TASK 0', { matchCase: false })
.should('have.css', 'text-transform', 'uppercase');
Expand Down Expand Up @@ -288,7 +305,7 @@ describe('Example 30 Composite Editor Modal', () => {
cy.get('.slick-editor-modal').should('not.exist');
});

it('should have new TASK 8888 displayed on first row', () => {
it('should have new TASK 8899 displayed on first row', () => {
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).contains('TASK 8899', { matchCase: false });
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', '33 days');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(4)`).should('contain', '17');
Expand Down
15 changes: 9 additions & 6 deletions test/cypress/e2e/example32.cy.ts
Expand Up @@ -36,6 +36,9 @@ describe('Example 32 - Regular & Custom Tooltips', () => {
cy.get('.slick-custom-tooltip').should('be.visible');
cy.get('.slick-custom-tooltip').contains('Task 2 - (async tooltip)');

cy.get('.tooltip-2cols-row:nth(0)').find('div:nth(0)').contains('Completion:');
cy.get('.tooltip-2cols-row:nth(0)').find('div').should('have.class', 'percent-complete-bar-with-text');

cy.get('.tooltip-2cols-row:nth(1)').find('div:nth(0)').contains('Lifespan:');
cy.get('.tooltip-2cols-row:nth(1)').find('div:nth(1)').contains(/\d+$/); // use regexp to make sure it's a number

Expand Down Expand Up @@ -167,7 +170,7 @@ describe('Example 32 - Regular & Custom Tooltips', () => {

it('should mouse over header-row (filter) 2nd column Title and expect a tooltip to show rendered from an headerRowFormatter', () => {
cy.get(`.slick-headerrow-columns .slick-headerrow-column:nth(1)`).as('checkbox0-filter');
cy.get('@checkbox0-filter').trigger('mouseenter');
cy.get('@checkbox0-filter').trigger('mouseover');

cy.get('.slick-custom-tooltip').should('be.visible');
cy.get('.slick-custom-tooltip').contains('Custom Tooltip - Header Row (filter)');
Expand All @@ -180,15 +183,15 @@ describe('Example 32 - Regular & Custom Tooltips', () => {

it('should mouse over header-row (filter) Finish column and NOT expect any tooltip to show since it is disabled on that column', () => {
cy.get(`.slick-headerrow-columns .slick-headerrow-column:nth(8)`).as('finish-filter');
cy.get('@finish-filter').trigger('mouseenter');
cy.get('@finish-filter').trigger('mouseover');

cy.get('.slick-custom-tooltip').should('not.exist');
cy.get('@finish-filter').trigger('mouseout');
});

it('should mouse over header-row (filter) Prerequisite column and expect to see tooltip of selected filter options', () => {
cy.get(`.slick-headerrow-columns .slick-headerrow-column:nth(10)`).as('checkbox10-header');
cy.get('@checkbox10-header').trigger('mouseenter');
cy.get('@checkbox10-header').trigger('mouseover');

cy.get('.filter-prerequisites .ms-choice span').contains('15 of 500 selected');
cy.get('.slick-custom-tooltip').should('be.visible');
Expand All @@ -199,15 +202,15 @@ describe('Example 32 - Regular & Custom Tooltips', () => {

it('should mouse over header title on 1st column with checkbox and NOT expect any tooltip to show since it is disabled on that column', () => {
cy.get(`.slick-header-columns .slick-header-column:nth(0)`).as('checkbox-header');
cy.get('@checkbox-header').trigger('mouseenter');
cy.get('@checkbox-header').trigger('mouseover');

cy.get('.slick-custom-tooltip').should('not.exist');
cy.get('@checkbox-header').trigger('mouseout');
});

it('should mouse over header title on 2nd column with Title name and expect a tooltip to show rendered from an headerFormatter', () => {
cy.get(`.slick-header-columns .slick-header-column:nth(1)`).as('checkbox0-header');
cy.get('@checkbox0-header').trigger('mouseenter');
cy.get('@checkbox0-header').trigger('mouseover');

cy.get('.slick-custom-tooltip').should('be.visible');
cy.get('.slick-custom-tooltip').contains('Custom Tooltip - Header');
Expand All @@ -220,7 +223,7 @@ describe('Example 32 - Regular & Custom Tooltips', () => {

it('should mouse over header title on 2nd column with Finish name and NOT expect any tooltip to show since it is disabled on that column', () => {
cy.get(`.slick-header-columns .slick-header-column:nth(8)`).as('finish-header');
cy.get('@finish-header').trigger('mouseenter');
cy.get('@finish-header').trigger('mouseover');

cy.get('.slick-custom-tooltip').should('not.exist');
cy.get('@finish-header').trigger('mouseout');
Expand Down
65 changes: 35 additions & 30 deletions test/cypress/e2e/example35.cy.ts
Expand Up @@ -22,38 +22,38 @@ describe('Example 35 - Row Based Editing', () => {

it('should only allow to toggle a single row into editmode on single mode', () => {
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] .action-btns--edit`).click();
cy.get('.action-btns--edit').eq(1).click();
cy.get('.action-btns--edit:nth(0)').click({ force: true });

cy.get('.slick-row.slick-rbe-editmode').should('have.length', 1);
});

it('should allow to toggle a multiple rows into editmode on multiple mode', () => {
cy.reload();
cy.get('[data-test="single-multi-toggle"]').click();
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] .action-btns--edit`).click();
cy.get('.action-btns--edit').eq(1).click();
cy.get('.action-btns--edit').eq(2).click();
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] .action-btns--edit`).click({ force: true });
cy.get('.action-btns--edit').eq(1).click({ force: true });
cy.get('.action-btns--edit').eq(2).click({ force: true });

cy.get('.slick-row.slick-rbe-editmode').should('have.length', 3);
});

it('should not display editor in rows not being in editmode', () => {
cy.reload();
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l2.r2`).click();
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l2.r2`).click({ force: true });

cy.get('input').should('have.length', 0);

cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] .action-btns--edit`).click();
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] .action-btns--edit`).click({ force: true });

cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l2.r2`).click();
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l2.r2`).click({ force: true });

cy.get('input').should('have.length', 1);
});

it('should highlight modified cells and maintain proper index on sorting', () => {
cy.reload();

cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] .action-btns--edit`).click();
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] .action-btns--edit`).click({ force: true });

cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l0.r0`).click().type('abc{enter}');
cy.get('.slick-cell').first().should('have.class', 'slick-rbe-unsaved-cell');
Expand All @@ -66,7 +66,7 @@ describe('Example 35 - Row Based Editing', () => {
it('should stay in editmode if saving failed', () => {
cy.reload();

cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] .action-btns--edit`).click();
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] .action-btns--edit`).click({ force: true });

cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l1.r1`).click().type('50{enter}');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l2.r2`).click().type('50');
Expand All @@ -83,12 +83,12 @@ describe('Example 35 - Row Based Editing', () => {
it('should save changes on update button click', () => {
cy.reload();

cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] .action-btns--edit`).click();
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] .action-btns--edit`).click({ force: true });

cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l1.r1`).click().type('30{enter}');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l2.r2`).type('30');

cy.get('.action-btns--update').first().click();
cy.get('.action-btns--update').first().click({ force: true });

cy.get('[data-test="fetch-result"]')
.should('contain', 'success');
Expand All @@ -98,27 +98,27 @@ describe('Example 35 - Row Based Editing', () => {
});

it('should cleanup status when starting a new edit mode', () => {
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] .action-btns--edit`).click();
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] .action-btns--edit`).click({ force: true });

cy.get('[data-test="fetch-result"]').should('be.empty');

cy.get('.action-btns--cancel').first().click();
cy.get('.action-btns--cancel').first().click({ force: true });
});

it('should revert changes on cancel click', () => {
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] .action-btns--edit`).click();
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] .action-btns--edit`).click({ force: true });

cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l1.r1`).click().type('50{enter}');
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l2.r2`).type('50{enter}');

cy.get('.action-btns--cancel').first().click();
cy.get('.action-btns--cancel').first().click({ force: true });

cy.get('.slick-cell.l1.r1').first().should('contain', '30');
cy.get('.slick-cell.l2.r2').first().should('contain', '30');
});

it('should delete a row when clicking it', () => {
cy.get('.action-btns--delete').first().click();
cy.get('.action-btns--delete').first().click({ force: true });

cy.on('window:confirm', () => true);

Expand All @@ -143,20 +143,25 @@ describe('Example 35 - Row Based Editing', () => {
cy.get('[data-test="toggle-language"]').click();
cy.get('[data-test="selected-locale"]').should('contain', 'fr.json');

// this seems to be a bug in Cypress, it doesn't seem to be able to click on the button
// but at least it triggers a rerender, which makes it refetch the actual button instead of a cached one
cy.get('.action-btns--update').first().click({ force: true });
cy.get('.action-btns--edit').first().click({ force: true });

cy.get('.action-btns--update')
.first()
.should(($btn) => {
expect($btn.attr('title')).to.equal('Mettre à jour la ligne actuelle');
});
cy.get('.action-btns--cancel').first().as('cancel-btn');
cy.get('@cancel-btn').should(($btn) => {
expect($btn.attr('title')).to.equal('Annuler la ligne actuelle');
});
cy.get('@cancel-btn').trigger('mouseover', { position: 'top' });
cy.get('.slick-custom-tooltip').should('be.visible');
cy.get('.slick-custom-tooltip .tooltip-body').contains('Annuler la ligne actuelle');

cy.get('.action-btns--cancel')
.first()
.should(($btn) => {
expect($btn.attr('title')).to.equal('Annuler la ligne actuelle');
});
cy.get('.action-btns--update').first().as('update-btn');
cy.get('@update-btn').should(($btn) => {
expect($btn.attr('title')).to.equal('Mettre à jour la ligne actuelle');
});

cy.get('@update-btn').trigger('mouseover', { position: 'top' });

cy.get('.slick-custom-tooltip').should('be.visible');
cy.get('.slick-custom-tooltip .tooltip-body').contains('Mettre à jour la ligne actuelle');
cy.get('@update-btn').first().click({ force: true });
});
});
});
2 changes: 1 addition & 1 deletion test/mockSlickEvent.ts
@@ -1,4 +1,4 @@
import { Handler, SlickEvent, SlickEventData } from '@slickgrid-universal/common';
import type { Handler, SlickEvent, SlickEventData } from '@slickgrid-universal/common';
type MergeTypes<A, B> = { [key in keyof A]: key extends keyof B ? B[key] : A[key]; } & B;

// @ts-ignore
Expand Down

0 comments on commit 85cabd7

Please sign in to comment.