Skip to content

Commit

Permalink
feat(tooltip): add a new "center" position option to SlickCustomToolt…
Browse files Browse the repository at this point in the history
…ip (#787)
  • Loading branch information
ghiscoding committed Nov 1, 2022
1 parent 51df99d commit b019de5
Show file tree
Hide file tree
Showing 7 changed files with 63 additions and 31 deletions.
Expand Up @@ -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;
}
Expand Up @@ -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,
Expand Down
Expand Up @@ -52,12 +52,12 @@ export interface CustomTooltipOption<T = any> {
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;
Expand Down
3 changes: 2 additions & 1 deletion packages/common/src/styles/_variables.scss
Expand Up @@ -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 */
Expand Down
3 changes: 3 additions & 0 deletions packages/common/src/styles/slick-plugins.scss
Expand Up @@ -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);
}
}

// ---------------------------------------------------------
Expand Down
Expand Up @@ -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"', () => {
Expand All @@ -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', () => {
Expand All @@ -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();
Expand All @@ -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', () => {
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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);
});
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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();
});
});
18 changes: 13 additions & 5 deletions packages/custom-tooltip-plugin/src/slickCustomTooltip.ts
Expand Up @@ -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;
Expand All @@ -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)
Expand Down

0 comments on commit b019de5

Please sign in to comment.