diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example16.scss b/examples/webpack-demo-vanilla-bundle/src/examples/example16.scss index 6647a6977..19875cc7e 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example16.scss +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example16.scss @@ -56,6 +56,3 @@ $control-height: 2.4em; .l4.slick-custom-tooltip.arrow-right-align::after { margin-left: calc(100% - 20px - 15px); // 20px is (arrow size * 2), 15px is your extra side margin } -.l6.slick-custom-tooltip.arrow-left-align::after { - margin-left: 4px; -} \ No newline at end of file diff --git a/examples/webpack-demo-vanilla-bundle/src/examples/example16.ts b/examples/webpack-demo-vanilla-bundle/src/examples/example16.ts index e9e4460bd..8d4f2b106 100644 --- a/examples/webpack-demo-vanilla-bundle/src/examples/example16.ts +++ b/examples/webpack-demo-vanilla-bundle/src/examples/example16.ts @@ -154,7 +154,7 @@ export class Example16 { formatter: Formatters.percentCompleteBar, sortable: true, filterable: true, filter: { model: Filters.slider, operator: '>=' }, - customTooltip: { useRegularTooltip: true, }, + customTooltip: { useRegularTooltip: true, position: 'center' }, }, { id: 'start', name: 'Start', field: 'start', sortable: true, diff --git a/packages/common/src/interfaces/customTooltipOption.interface.ts b/packages/common/src/interfaces/customTooltipOption.interface.ts index 45f762e24..f9a40a3c7 100644 --- a/packages/common/src/interfaces/customTooltipOption.interface.ts +++ b/packages/common/src/interfaces/customTooltipOption.interface.ts @@ -52,12 +52,12 @@ export interface CustomTooltipOption { offsetTopBottom?: number; /** - * Defaults to "auto", allows to align the tooltip to the best logical position in the window, by default it will show on top but if it calculates that it doesn't have enough space it will revert to bottom. + * Defaults to "auto" (note that "center" will never be used by "auto"), allows to align the tooltip to the best logical position in the window, by default it will show on top but if it calculates that it doesn't have enough space it will revert to bottom. * We can assume that in 80% of the time the default position is top left, the default is "auto" but we can also override this and use a specific align side. - * Most of the time positioning of the tooltip will be to the "right-align" of the cell is ok but if our column is completely on the right side then we'll want to change the position to "left-align" align. + * Most of the time, the positioning of the tooltip will be "right-align" of the cell which is typically ok unless your column is completely on the right side and so we'll want to change the position to "left-align" in that case. * Same goes for the top/bottom position, Most of the time positioning the tooltip to the "top" but if we are showing a tooltip from a cell on the top of the grid then we might need to reposition to "bottom" instead. */ - position?: 'auto' | 'top' | 'bottom' | 'left-align' | 'right-align'; + position?: 'auto' | 'top' | 'bottom' | 'left-align' | 'right-align' | 'center'; /** defaults to False, when set to True it will skip custom tooltip formatter and instead will parse through the regular cell formatter and try to find a `title` to show regular tooltip */ useRegularTooltip?: boolean; diff --git a/packages/common/src/styles/_variables.scss b/packages/common/src/styles/_variables.scss index 0ddf5edeb..fcf9e8ec6 100644 --- a/packages/common/src/styles/_variables.scss +++ b/packages/common/src/styles/_variables.scss @@ -847,7 +847,8 @@ $slick-tooltip-arrow-color: darken($slick-toolti $slick-tooltip-arrow-size: 8px !default; $slick-tooltip-down-arrow-top-margin: 100% !default; $slick-tooltip-up-arrow-top-margin: -($slick-tooltip-arrow-size * 2) !default; -$slick-tooltip-arrow-side-margin: 9px !default; +$slick-tooltip-arrow-side-margin: 3px !default; +$slick-tooltip-arrow-center-margin: calc(50% - #{$slick-tooltip-arrow-size}) !default; $slick-tooltip-right-arrow-side-margin: calc(100% - #{($slick-tooltip-arrow-size * 2)} - #{$slick-tooltip-arrow-side-margin}) !default; /** Empty Data Warning element */ diff --git a/packages/common/src/styles/slick-plugins.scss b/packages/common/src/styles/slick-plugins.scss index bc07acfe1..d60030a7c 100644 --- a/packages/common/src/styles/slick-plugins.scss +++ b/packages/common/src/styles/slick-plugins.scss @@ -393,6 +393,9 @@ li.hidden { &.tooltip-arrow.arrow-right-align::after { margin-left: var(--slick-tooltip-right-arrow-side-margin, $slick-tooltip-right-arrow-side-margin); } + &.tooltip-arrow.arrow-center-align::after { + margin-left: var(--slick-tooltip-arrow-center-margin, $slick-tooltip-arrow-center-margin); + } } // --------------------------------------------------------- diff --git a/packages/custom-tooltip-plugin/src/__tests__/slickCustomTooltip.spec.ts b/packages/custom-tooltip-plugin/src/__tests__/slickCustomTooltip.spec.ts index 20b771c85..5ae81ac31 100644 --- a/packages/custom-tooltip-plugin/src/__tests__/slickCustomTooltip.spec.ts +++ b/packages/custom-tooltip-plugin/src/__tests__/slickCustomTooltip.spec.ts @@ -161,8 +161,31 @@ describe('SlickCustomTooltip plugin', () => { const tooltipElm = document.body.querySelector('.slick-custom-tooltip') as HTMLDivElement; expect(tooltipElm).toBeTruthy(); expect(tooltipElm.textContent).toBe('tooltip text'); - expect(tooltipElm.classList.contains('arrow-down')); - expect(tooltipElm.classList.contains('arrow-left-align')); + expect(tooltipElm.classList.contains('arrow-down')).toBeTruthy(); + expect(tooltipElm.classList.contains('arrow-left-align')).toBeTruthy(); + }); + + it('should create a centered tooltip, when position is set to "center"', () => { + const cellNode = document.createElement('div'); + cellNode.className = 'slick-cell'; + cellNode.setAttribute('title', 'tooltip text'); + const mockColumns = [{ id: 'firstName', field: 'firstName', }] as Column[]; + jest.spyOn(gridStub, 'getCellFromEvent').mockReturnValue({ cell: 0, row: 1 }); + jest.spyOn(gridStub, 'getCellNode').mockReturnValue(cellNode); + jest.spyOn(gridStub, 'getColumns').mockReturnValue(mockColumns); + jest.spyOn(dataviewStub, 'getItem').mockReturnValue({ firstName: 'John', lastName: 'Doe' }); + + plugin.init(gridStub, container); + plugin.setOptions({ useRegularTooltip: true, position: 'center' }); + gridStub.onMouseEnter.notify({ grid: gridStub }); + + const tooltipElm = document.body.querySelector('.slick-custom-tooltip') as HTMLDivElement; + expect(tooltipElm).toBeTruthy(); + expect(tooltipElm.textContent).toBe('tooltip text'); + expect(tooltipElm.classList.contains('arrow-down')).toBeTruthy(); + expect(tooltipElm.classList.contains('arrow-center-align')).toBeTruthy(); + expect(tooltipElm.classList.contains('arrow-left-align')).toBeFalsy(); + expect(tooltipElm.classList.contains('arrow-right-align')).toBeFalsy(); }); it('should create a tooltip with truncated text when tooltip option has "useRegularTooltip" enabled and the tooltipt text is longer than that of "tooltipTextMaxLength"', () => { @@ -182,8 +205,8 @@ describe('SlickCustomTooltip plugin', () => { const tooltipElm = document.body.querySelector('.slick-custom-tooltip') as HTMLDivElement; expect(tooltipElm).toBeTruthy(); expect(tooltipElm.textContent).toBe('some very extra long...'); - expect(tooltipElm.classList.contains('arrow-down')); - expect(tooltipElm.classList.contains('arrow-left-align')); + expect(tooltipElm.classList.contains('arrow-down')).toBeTruthy(); + expect(tooltipElm.classList.contains('arrow-left-align')).toBeTruthy(); }); it('should create a tooltip as regular tooltip with coming from text content when it is filled & also expect "hideTooltip" to be called after leaving the cell when "onHeaderMouseLeave" event is triggered', () => { @@ -209,8 +232,8 @@ describe('SlickCustomTooltip plugin', () => { expect(plugin.cellAddonOptions).toBeTruthy(); expect(tooltipElm.style.maxWidth).toBe('85px'); expect(tooltipElm.textContent).toBe('some text content'); - expect(tooltipElm.classList.contains('arrow-down')); - expect(tooltipElm.classList.contains('arrow-left-align')); + expect(tooltipElm.classList.contains('arrow-down')).toBeTruthy(); + expect(tooltipElm.classList.contains('arrow-left-align')).toBeTruthy(); gridStub.onMouseLeave.notify({ grid: gridStub }); expect(hideColumnSpy).toHaveBeenCalled(); @@ -236,8 +259,8 @@ describe('SlickCustomTooltip plugin', () => { const tooltipElm = document.body.querySelector('.slick-custom-tooltip') as HTMLDivElement; expect(tooltipElm).toBeTruthy(); expect(tooltipElm.textContent).toBe('some very extra long...'); - expect(tooltipElm.classList.contains('arrow-down')); - expect(tooltipElm.classList.contains('arrow-left-align')); + expect(tooltipElm.classList.contains('arrow-down')).toBeTruthy(); + expect(tooltipElm.classList.contains('arrow-left-align')).toBeTruthy(); }); it('should create a tooltip with only the tooltip formatter output when tooltip option has "useRegularTooltip" & "useRegularTooltipFromFormatterOnly" enabled and column definition has a regular formatter with a "title" attribute filled', () => { @@ -258,8 +281,8 @@ describe('SlickCustomTooltip plugin', () => { expect(tooltipElm).toBeTruthy(); expect(tooltipElm.textContent).toBe('formatter tooltip text'); expect(tooltipElm.style.maxHeight).toBe('100px'); - expect(tooltipElm.classList.contains('arrow-down')); - expect(tooltipElm.classList.contains('arrow-left-align')); + expect(tooltipElm.classList.contains('arrow-down')).toBeTruthy(); + expect(tooltipElm.classList.contains('arrow-left-align')).toBeTruthy(); }); it('should throw an error when trying to create an async tooltip without "asyncPostFormatter" defined', () => { @@ -310,7 +333,7 @@ describe('SlickCustomTooltip plugin', () => { setTimeout(() => { tooltipElm = document.body.querySelector('.slick-custom-tooltip') as HTMLDivElement; expect(tooltipElm.textContent).toBe('async post text with ratio: 1.2'); - expect(tooltipElm.classList.contains('arrow-down')); + expect(tooltipElm.classList.contains('arrow-down')).toBeTruthy(); done(); }, 0); }); @@ -436,7 +459,7 @@ describe('SlickCustomTooltip plugin', () => { expect(tooltipElm).toBeTruthy(); expect(tooltipElm.textContent).toBe('loading...'); - cancellablePromise.promise.catch(e => { + cancellablePromise!.promise.catch(e => { tooltipElm = document.body.querySelector('.slick-custom-tooltip') as HTMLDivElement; expect(tooltipElm.textContent).toBe('loading...'); expect(e.toString()).toBe('promise error'); @@ -466,8 +489,8 @@ describe('SlickCustomTooltip plugin', () => { const tooltipElm = document.body.querySelector('.slick-custom-tooltip') as HTMLDivElement; expect(tooltipElm).toBeTruthy(); expect(tooltipElm.textContent).toBe('name title tooltip'); - expect(tooltipElm.classList.contains('arrow-down')); - expect(tooltipElm.classList.contains('arrow-left-align')); + expect(tooltipElm.classList.contains('arrow-down')).toBeTruthy(); + expect(tooltipElm.classList.contains('arrow-left-align')).toBeTruthy(); }); it('should create a tooltip on the header column when "useRegularTooltip" enabled and "onHeaderMouseEnter" is triggered', () => { @@ -495,8 +518,8 @@ describe('SlickCustomTooltip plugin', () => { const tooltipElm = document.body.querySelector('.slick-custom-tooltip') as HTMLDivElement; expect(tooltipElm).toBeTruthy(); expect(tooltipElm.textContent).toBe('header tooltip text'); - expect(tooltipElm.classList.contains('arrow-down')); - expect(tooltipElm.classList.contains('arrow-left-align')); + expect(tooltipElm.classList.contains('arrow-down')).toBeTruthy(); + expect(tooltipElm.classList.contains('arrow-left-align')).toBeTruthy(); }); it('should create a tooltip on the header column when "useRegularTooltip" enabled and "onHeaderRowMouseEnter" is triggered', () => { @@ -524,7 +547,7 @@ describe('SlickCustomTooltip plugin', () => { const tooltipElm = document.body.querySelector('.slick-custom-tooltip') as HTMLDivElement; expect(tooltipElm).toBeTruthy(); expect(tooltipElm.textContent).toBe('header row tooltip text'); - expect(tooltipElm.classList.contains('arrow-down')); - expect(tooltipElm.classList.contains('arrow-left-align')); + expect(tooltipElm.classList.contains('arrow-down')).toBeTruthy(); + expect(tooltipElm.classList.contains('arrow-left-align')).toBeTruthy(); }); }); diff --git a/packages/custom-tooltip-plugin/src/slickCustomTooltip.ts b/packages/custom-tooltip-plugin/src/slickCustomTooltip.ts index 048829b45..678efd296 100644 --- a/packages/custom-tooltip-plugin/src/slickCustomTooltip.ts +++ b/packages/custom-tooltip-plugin/src/slickCustomTooltip.ts @@ -393,7 +393,7 @@ export class SlickCustomTooltip { if (this._tooltipElm) { this._cellNodeElm = this._cellNodeElm || this._grid.getCellNode(cell.row, cell.cell) as HTMLDivElement; const cellPosition = getHtmlElementOffset(this._cellNodeElm) || { top: 0, left: 0 }; - const containerWidth = this._cellNodeElm.offsetWidth; + const cellContainerWidth = this._cellNodeElm.offsetWidth; const calculatedTooltipHeight = this._tooltipElm.getBoundingClientRect().height; const calculatedTooltipWidth = this._tooltipElm.getBoundingClientRect().width; const calculatedBodyWidth = document.body.offsetWidth || window.innerWidth; @@ -402,17 +402,25 @@ export class SlickCustomTooltip { let newPositionTop = (cellPosition.top || 0) - this._tooltipElm.offsetHeight - (this._cellAddonOptions?.offsetTopBottom ?? 0); let newPositionLeft = (cellPosition.left || 0) - (this._cellAddonOptions?.offsetLeft ?? 0); - // user could explicitely use a "left-align" arrow position, (when user knows his column is completely on the right) + // user could explicitely use a "left-align" arrow position, (when user knows his column is completely on the right in the grid) // or when using "auto" and we detect not enough available space then we'll position to the "left" of the cell // NOTE the class name is for the arrow and is inverse compare to the tooltip itself, so if user ask for "left-align", then the arrow will in fact be "arrow-right-align" const position = this._cellAddonOptions?.position ?? 'auto'; - if (position === 'left-align' || ((position === 'auto' || position !== 'right-align') && (newPositionLeft + calculatedTooltipWidth) > calculatedBodyWidth)) { - newPositionLeft -= (calculatedTooltipWidth - containerWidth - (this._cellAddonOptions?.offsetRight ?? 0)); + if (position === 'center') { + newPositionLeft += (cellContainerWidth / 2) - (calculatedTooltipWidth / 2) + (this._cellAddonOptions?.offsetLeft ?? 0); + this._tooltipElm.classList.remove('arrow-left-align'); + this._tooltipElm.classList.remove('arrow-right-align'); + this._tooltipElm.classList.add('arrow-center-align'); + + } else if (position === 'left-align' || ((position === 'auto' || position !== 'right-align') && (newPositionLeft + calculatedTooltipWidth) > calculatedBodyWidth)) { + newPositionLeft -= (calculatedTooltipWidth - cellContainerWidth - (this._cellAddonOptions?.offsetRight ?? 0)); + this._tooltipElm.classList.remove('arrow-center-align'); this._tooltipElm.classList.remove('arrow-left-align'); this._tooltipElm.classList.add('arrow-right-align'); } else { - this._tooltipElm.classList.add('arrow-left-align'); + this._tooltipElm.classList.remove('arrow-center-align'); this._tooltipElm.classList.remove('arrow-right-align'); + this._tooltipElm.classList.add('arrow-left-align'); } // do the same calculation/reposition with top/bottom (default is top of the cell or in other word starting from the cell going down)