diff --git a/packages/common/src/filters/__tests__/compoundSliderFilter.spec.ts b/packages/common/src/filters/__tests__/compoundSliderFilter.spec.ts index 9a2eb4cba..c5878ff19 100644 --- a/packages/common/src/filters/__tests__/compoundSliderFilter.spec.ts +++ b/packages/common/src/filters/__tests__/compoundSliderFilter.spec.ts @@ -28,6 +28,7 @@ const gridStub = { render: jest.fn(), onHeaderMouseLeave: new SlickEvent(), onHeaderRowMouseEnter: new SlickEvent(), + onHeaderRowMouseLeave: new SlickEvent(), } as unknown as SlickGrid; describe('CompoundSliderFilter', () => { @@ -107,6 +108,7 @@ describe('CompoundSliderFilter', () => { it('should call "setValues" with "operator" set in the filter arguments and expect that value to be converted to number and in the callback when triggered', () => { const callbackSpy = jest.spyOn(filterArguments, 'callback'); const rowMouseEnterSpy = jest.spyOn(gridStub.onHeaderRowMouseEnter, 'notify'); + const rowMouseLeaveSpy = jest.spyOn(gridStub.onHeaderRowMouseLeave, 'notify'); const filterArgs = { ...filterArguments, operator: '>', grid: gridStub } as FilterArguments; filter.init(filterArgs); @@ -116,6 +118,7 @@ describe('CompoundSliderFilter', () => { expect(callbackSpy).toHaveBeenLastCalledWith(expect.anything(), { columnDef: mockColumn, operator: '>', searchTerms: [2], shouldTriggerQuery: true }); expect(rowMouseEnterSpy).toHaveBeenCalledWith({ column: mockColumn, grid: gridStub }, expect.anything()); + expect(rowMouseLeaveSpy).toHaveBeenCalledWith({ column: mockColumn, grid: gridStub }); }); it('should trigger an slider input change event and expect slider value to be updated and also "onHeaderRowMouseEnter" to be notified', () => { diff --git a/packages/common/src/filters/__tests__/singleSliderFilter.spec.ts b/packages/common/src/filters/__tests__/singleSliderFilter.spec.ts index de3ad71f1..08dbe1922 100644 --- a/packages/common/src/filters/__tests__/singleSliderFilter.spec.ts +++ b/packages/common/src/filters/__tests__/singleSliderFilter.spec.ts @@ -22,6 +22,7 @@ const gridStub = { render: jest.fn(), onHeaderMouseLeave: new SlickEvent(), onHeaderRowMouseEnter: new SlickEvent(), + onHeaderRowMouseLeave: new SlickEvent(), } as unknown as SlickGrid; describe('SingleSliderFilter', () => { @@ -77,6 +78,7 @@ describe('SingleSliderFilter', () => { it('should call "setValues" and expect that value, converted as a number, to be in the callback when triggered', () => { const callbackSpy = jest.spyOn(filterArgs, 'callback'); const rowMouseEnterSpy = jest.spyOn(gridStub.onHeaderRowMouseEnter, 'notify'); + const rowMouseLeaveSpy = jest.spyOn(gridStub.onHeaderRowMouseLeave, 'notify'); filter.init(filterArgs); filter.setValues(['2']); @@ -85,6 +87,7 @@ describe('SingleSliderFilter', () => { expect(callbackSpy).toHaveBeenLastCalledWith(new Event('change'), { columnDef: mockColumn, operator: 'GE', searchTerms: [2], shouldTriggerQuery: true }); expect(rowMouseEnterSpy).toHaveBeenCalledWith({ column: mockColumn, grid: gridStub }, expect.anything()); + expect(rowMouseLeaveSpy).toHaveBeenCalledWith({ column: mockColumn, grid: gridStub }); }); it('should trigger an slider input change event and expect slider value to be updated and also "onHeaderRowMouseEnter" to be notified', () => { diff --git a/packages/common/src/filters/__tests__/sliderRangeFilter.spec.ts b/packages/common/src/filters/__tests__/sliderRangeFilter.spec.ts index c57d33cc6..a16a7c220 100644 --- a/packages/common/src/filters/__tests__/sliderRangeFilter.spec.ts +++ b/packages/common/src/filters/__tests__/sliderRangeFilter.spec.ts @@ -22,6 +22,7 @@ const gridStub = { render: jest.fn(), onHeaderMouseLeave: new SlickEvent(), onHeaderRowMouseEnter: new SlickEvent(), + onHeaderRowMouseLeave: new SlickEvent(), } as unknown as SlickGrid; describe('SliderRangeFilter', () => { @@ -120,6 +121,7 @@ describe('SliderRangeFilter', () => { it('should call "setValues" and expect that value to be in the callback when triggered', () => { const callbackSpy = jest.spyOn(filterArguments, 'callback'); const rowMouseEnterSpy = jest.spyOn(gridStub.onHeaderRowMouseEnter, 'notify'); + const rowMouseLeaveSpy = jest.spyOn(gridStub.onHeaderRowMouseLeave, 'notify'); filter.init(filterArguments); filter.setValues(['2..80']); @@ -128,6 +130,7 @@ describe('SliderRangeFilter', () => { expect(callbackSpy).toHaveBeenLastCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'RangeInclusive', searchTerms: [2, 80], shouldTriggerQuery: true }); expect(rowMouseEnterSpy).toHaveBeenCalledWith({ column: mockColumn, grid: gridStub }, expect.anything()); + expect(rowMouseLeaveSpy).toHaveBeenCalledWith({ column: mockColumn, grid: gridStub }); }); it('should trigger an slider input change event and expect slider value to be updated and also "onHeaderRowMouseEnter" to be notified', () => { diff --git a/packages/common/src/filters/sliderFilter.ts b/packages/common/src/filters/sliderFilter.ts index 73605570a..69ba49f95 100644 --- a/packages/common/src/filters/sliderFilter.ts +++ b/packages/common/src/filters/sliderFilter.ts @@ -8,6 +8,7 @@ import type { Column, ColumnFilter, CurrentSliderOption, + DOMEvent, Filter, FilterArguments, FilterCallback, @@ -182,12 +183,22 @@ export class SliderFilter implements Filter { * @param leftValue number * @param rightValue number */ - renderSliderValues(leftValue?: number | string | undefined, rightValue?: number | string | undefined): void { - if (this._leftSliderNumberElm?.textContent && leftValue) { - this._leftSliderNumberElm.textContent = leftValue.toString(); + renderSliderValues(leftValue?: number | string, rightValue?: number | string, triggerTooltipMouseLeave = true): void { + const leftVal = leftValue?.toString() || ''; + const rightVal = rightValue?.toString() || ''; + + if (this._leftSliderNumberElm?.textContent) { + this._leftSliderNumberElm.textContent = leftVal; + } + if (this._rightSliderNumberElm?.textContent) { + this._rightSliderNumberElm.textContent = rightVal; } - if (this._rightSliderNumberElm?.textContent && rightValue) { - this._rightSliderNumberElm.textContent = rightValue.toString(); + this._sliderRangeContainElm.title = this.sliderType === 'double' ? `${leftVal} - ${rightVal}` : `${rightVal}`; + + // when changing slider values dynamically (typically via a button), we'll want to avoid tooltips showing up + // onHeaderRowMouseLeave will hide any tooltip + if (triggerTooltipMouseLeave) { + this.grid.onHeaderRowMouseLeave.notify({ column: this.columnDef, grid: this.grid }); } } @@ -363,13 +374,13 @@ export class SliderFilter implements Filter { // attach events this._bindEventService.bind(this._sliderTrackElm, 'click', this.sliderTrackClicked.bind(this) as EventListener); - this._bindEventService.bind(this._sliderRightInputElm, ['input', 'change'], this.slideRightInputChanged.bind(this)); + this._bindEventService.bind(this._sliderRightInputElm, ['input', 'change'], this.slideRightInputChanged.bind(this) as EventListener); this._bindEventService.bind(this._sliderRightInputElm, ['change', 'mouseup', 'touchend'], this.onValueChanged.bind(this) as EventListener); if (this.sliderType === 'compound' && this._selectOperatorElm) { this._bindEventService.bind(this._selectOperatorElm, ['change'], this.onValueChanged.bind(this) as EventListener); } else if (this.sliderType === 'double' && this._sliderLeftInputElm) { - this._bindEventService.bind(this._sliderLeftInputElm, ['input', 'change'], this.slideLeftInputChanged.bind(this)); + this._bindEventService.bind(this._sliderLeftInputElm, ['input', 'change'], this.slideLeftInputChanged.bind(this) as EventListener); this._bindEventService.bind(this._sliderLeftInputElm, ['change', 'mouseup', 'touchend'], this.onValueChanged.bind(this) as EventListener); } @@ -392,7 +403,7 @@ export class SliderFilter implements Filter { } /** handle value change event triggered, trigger filter callback & update "filled" class name */ - protected onValueChanged(e: MouseEvent): void { + protected onValueChanged(e: DOMEvent): void { const sliderRightVal = parseInt(this._sliderRightInputElm?.value ?? '', 10); let value; let searchTerms: SearchTerm[]; @@ -443,7 +454,7 @@ export class SliderFilter implements Filter { this._sliderRightInputElm?.classList[addRemoveCmd]('focus'); } - protected slideLeftInputChanged(e: Event): void { + protected slideLeftInputChanged(e: DOMEvent): void { const sliderLeftVal = parseInt(this._sliderLeftInputElm?.value ?? '', 10); const sliderRightVal = parseInt(this._sliderRightInputElm?.value ?? '', 10); @@ -467,7 +478,7 @@ export class SliderFilter implements Filter { this.sliderLeftOrRightChanged(e, sliderLeftVal, sliderRightVal); } - protected slideRightInputChanged(e: Event): void { + protected slideRightInputChanged(e: DOMEvent): void { const sliderLeftVal = parseInt(this._sliderLeftInputElm?.value ?? '', 10); const sliderRightVal = parseInt(this._sliderRightInputElm?.value ?? '', 10); @@ -478,7 +489,7 @@ export class SliderFilter implements Filter { this.sliderLeftOrRightChanged(e, sliderLeftVal, sliderRightVal); } - protected sliderLeftOrRightChanged(e: Event, sliderLeftVal: number, sliderRightVal: number): void { + protected sliderLeftOrRightChanged(e: DOMEvent, sliderLeftVal: number, sliderRightVal: number): void { this.updateTrackFilledColorWhenEnabled(); this.changeBothSliderFocuses(true); this._sliderRangeContainElm.title = this.sliderType === 'double' ? `${sliderLeftVal} - ${sliderRightVal}` : `${sliderRightVal}`; diff --git a/packages/custom-tooltip-plugin/src/__tests__/slickCustomTooltip.spec.ts b/packages/custom-tooltip-plugin/src/__tests__/slickCustomTooltip.spec.ts index 6bcfc7c8d..2db1fd99f 100644 --- a/packages/custom-tooltip-plugin/src/__tests__/slickCustomTooltip.spec.ts +++ b/packages/custom-tooltip-plugin/src/__tests__/slickCustomTooltip.spec.ts @@ -39,6 +39,7 @@ const gridStub = { onHeaderMouseOver: new SlickEvent(), onHeaderRowMouseEnter: new SlickEvent(), onHeaderRowMouseOver: new SlickEvent(), + onHeaderRowMouseLeave: new SlickEvent(), onMouseLeave: new SlickEvent(), onHeaderMouseOut: new SlickEvent(), onHeaderRowMouseOut: new SlickEvent(), @@ -309,7 +310,7 @@ describe('SlickCustomTooltip plugin', () => { 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 "onHeaderMouseOut" event is triggered', () => { + 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 "onMouseLeave" event is triggered', () => { const cellNode = document.createElement('div'); cellNode.className = 'slick-cell l2 r2'; cellNode.textContent = 'some text content'; @@ -339,6 +340,36 @@ describe('SlickCustomTooltip plugin', () => { expect(hideColumnSpy).toHaveBeenCalled(); }); + 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 "onHeaderRowMouseLeave" event is triggered', () => { + const cellNode = document.createElement('div'); + cellNode.className = 'slick-cell l2 r2'; + cellNode.textContent = 'some text content'; + cellNode.setAttribute('title', 'tooltip text'); + Object.defineProperty(cellNode, 'scrollWidth', { writable: true, configurable: true, value: 400 }); + 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' }); + const hideColumnSpy = jest.spyOn(plugin, 'hideTooltip'); + + plugin.init(gridStub, container); + plugin.setOptions({ useRegularTooltip: true, maxWidth: 85 }); + gridStub.onMouseEnter.notify({ grid: gridStub } as any, { ...new SlickEventData(), target: cellNode } as any); + + const tooltipElm = document.body.querySelector('.slick-custom-tooltip') as HTMLDivElement; + expect(tooltipElm).toBeTruthy(); + expect(tooltipElm).toEqual(plugin.tooltipElm); + expect(plugin.addonOptions).toBeTruthy(); + expect(tooltipElm.style.maxWidth).toBe('85px'); + expect(tooltipElm.textContent).toBe('some text content'); + expect(tooltipElm.classList.contains('arrow-down')).toBeTruthy(); + expect(tooltipElm.classList.contains('arrow-left-align')).toBeTruthy(); + + gridStub.onHeaderRowMouseLeave.notify({ grid: gridStub } as any); + expect(hideColumnSpy).toHaveBeenCalled(); + }); + it('should create a tooltip as regular tooltip with truncated text when tooltip option has "useRegularTooltip" enabled and the tooltip text is longer than that of "tooltipTextMaxLength"', () => { const cellNode = document.createElement('div'); cellNode.className = 'slick-cell l2 r2'; diff --git a/packages/custom-tooltip-plugin/src/slickCustomTooltip.ts b/packages/custom-tooltip-plugin/src/slickCustomTooltip.ts index dbf48a759..9aed54000 100644 --- a/packages/custom-tooltip-plugin/src/slickCustomTooltip.ts +++ b/packages/custom-tooltip-plugin/src/slickCustomTooltip.ts @@ -154,6 +154,7 @@ export class SlickCustomTooltip { .subscribe(grid.onHeaderRowMouseOver, (e, args) => this.handleOnHeaderMouseOverByType(e, args, 'slick-headerrow-column')) .subscribe(grid.onMouseLeave, this.hideTooltip.bind(this)) .subscribe(grid.onHeaderMouseOut, this.hideTooltip.bind(this)) + .subscribe(grid.onHeaderRowMouseLeave, this.hideTooltip.bind(this)) .subscribe(grid.onHeaderRowMouseOut, this.hideTooltip.bind(this)); } @@ -221,7 +222,7 @@ export class SlickCustomTooltip { const isHeaderRowType = selector === 'slick-headerrow-column'; // run the override function (when defined), if the result is false it won't go further - args = args || {}; + args ||= {}; args.cell = cell.cell; args.row = cell.row; args.columnDef = columnDef;