From 509a31d5630689c6c91cc2cef4e87b8dea72a243 Mon Sep 17 00:00:00 2001 From: Ghislain B Date: Wed, 2 Nov 2022 20:57:05 -0400 Subject: [PATCH] fix(filters): changing Slider value should update tooltip value (#788) * fix(filters): changing Slider value should update tooltip value - when using SlickCustomTooltip addon and changing slider value, it should update the tooltip value without having to leave the cell and rehovering to see the updated value, it turns out that calling `onHeaderRowMouseEnter` will have that effect as long as we add a small `setTimeout` delay * tests: add missing slider unit tests --- .../__tests__/compoundSliderFilter.spec.ts | 34 +++-- .../filters/__tests__/sliderFilter.spec.ts | 26 ++-- .../__tests__/sliderRangeFilter.spec.ts | 38 ++--- .../src/filters/compoundSliderFilter.ts | 11 +- packages/common/src/filters/sliderFilter.ts | 11 +- .../common/src/filters/sliderRangeFilter.ts | 130 +++++++++--------- 6 files changed, 144 insertions(+), 106 deletions(-) diff --git a/packages/common/src/filters/__tests__/compoundSliderFilter.spec.ts b/packages/common/src/filters/__tests__/compoundSliderFilter.spec.ts index e7cbc6b7e..19697a90d 100644 --- a/packages/common/src/filters/__tests__/compoundSliderFilter.spec.ts +++ b/packages/common/src/filters/__tests__/compoundSliderFilter.spec.ts @@ -6,6 +6,7 @@ import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; const containerId = 'demo-container'; declare const Slick: SlickNamespace; +jest.useFakeTimers(); // define a
container to simulate the grid container const template = `
`; @@ -25,6 +26,7 @@ const gridStub = { getHeaderRowColumn: jest.fn(), render: jest.fn(), onHeaderMouseLeave: new Slick.Event(), + onHeaderRowMouseEnter: new Slick.Event(), } as unknown as SlickGrid; describe('CompoundSliderFilter', () => { @@ -77,7 +79,8 @@ describe('CompoundSliderFilter', () => { }); it('should call "setValues" with "operator" set in the filter arguments and expect that value to be in the callback when triggered', () => { - const spyCallback = jest.spyOn(filterArguments, 'callback'); + const callbackSpy = jest.spyOn(filterArguments, 'callback'); + const rowMouseEnterSpy = jest.spyOn(gridStub.onHeaderRowMouseEnter, 'notify'); const filterArgs = { ...filterArguments, operator: '>', grid: gridStub } as FilterArguments; filter.init(filterArgs); @@ -85,11 +88,14 @@ describe('CompoundSliderFilter', () => { const filterElm = divContainer.querySelector('.input-group.search-filter.filter-duration input') as HTMLInputElement; filterElm.dispatchEvent(new CustomEvent('change')); - expect(spyCallback).toHaveBeenLastCalledWith(expect.anything(), { columnDef: mockColumn, operator: '>', searchTerms: ['2'], shouldTriggerQuery: true }); + jest.runAllTimers(); // fast-forward timer + + expect(callbackSpy).toHaveBeenLastCalledWith(expect.anything(), { columnDef: mockColumn, operator: '>', searchTerms: ['2'], shouldTriggerQuery: true }); + expect(rowMouseEnterSpy).toHaveBeenCalledWith({ column: mockColumn, grid: gridStub }, expect.anything()); }); it('should call "setValues" with "operator" set in the filter arguments and expect that value, converted as a string, to be in the callback when triggered', () => { - const spyCallback = jest.spyOn(filterArguments, 'callback'); + const callbackSpy = jest.spyOn(filterArguments, 'callback'); const filterArgs = { ...filterArguments, operator: '<=', grid: gridStub } as FilterArguments; filter.init(filterArgs); @@ -99,11 +105,11 @@ describe('CompoundSliderFilter', () => { const filterFilledElms = divContainer.querySelectorAll('.slider-container.search-filter.filter-duration.filled'); expect(filterFilledElms.length).toBe(1); - expect(spyCallback).toHaveBeenLastCalledWith(expect.anything(), { columnDef: mockColumn, operator: '<=', searchTerms: ['3'], shouldTriggerQuery: true }); + expect(callbackSpy).toHaveBeenLastCalledWith(expect.anything(), { columnDef: mockColumn, operator: '<=', searchTerms: ['3'], shouldTriggerQuery: true }); }); it('should trigger an operator change event and expect the callback to be called with the searchTerms and operator defined', () => { - const spyCallback = jest.spyOn(filterArguments, 'callback'); + const callbackSpy = jest.spyOn(filterArguments, 'callback'); filter.init(filterArguments); filter.setValues(9); @@ -112,11 +118,11 @@ describe('CompoundSliderFilter', () => { filterSelectElm.value = '<='; filterSelectElm.dispatchEvent(new CustomEvent('change')); - expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '<=', searchTerms: ['9'], shouldTriggerQuery: true }); + expect(callbackSpy).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '<=', searchTerms: ['9'], shouldTriggerQuery: true }); }); it('should be able to call "setValues" with a value and an extra operator and expect it to be set as new operator', () => { - const spyCallback = jest.spyOn(filterArguments, 'callback'); + const callbackSpy = jest.spyOn(filterArguments, 'callback'); filter.init(filterArguments); filter.setValues(['9'], OperatorType.greaterThanOrEqual); @@ -124,7 +130,7 @@ describe('CompoundSliderFilter', () => { const filterSelectElm = divContainer.querySelector('.search-filter.filter-duration select') as HTMLInputElement; filterSelectElm.dispatchEvent(new CustomEvent('change')); - expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '>=', searchTerms: ['9'], shouldTriggerQuery: true }); + expect(callbackSpy).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: '>=', searchTerms: ['9'], shouldTriggerQuery: true }); }); it('should be able to call "setValues" and set empty values and the input to not have the "filled" css class', () => { @@ -208,29 +214,29 @@ describe('CompoundSliderFilter', () => { it('should trigger a callback with the clear filter set when calling the "clear" method', () => { const filterArgs = { ...filterArguments, operator: '<=', searchTerms: [3], grid: gridStub } as FilterArguments; - const spyCallback = jest.spyOn(filterArguments, 'callback'); + const callbackSpy = jest.spyOn(filterArguments, 'callback'); filter.init(filterArgs); filter.clear(); expect(filter.getValues()).toBe(0); - expect(spyCallback).toHaveBeenLastCalledWith(undefined, { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: true }); + expect(callbackSpy).toHaveBeenLastCalledWith(undefined, { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: true }); }); it('should trigger a callback with the clear filter but without querying when when calling the "clear" method with False as argument', () => { const filterArgs = { ...filterArguments, operator: '<=', searchTerms: [3], grid: gridStub } as FilterArguments; - const spyCallback = jest.spyOn(filterArguments, 'callback'); + const callbackSpy = jest.spyOn(filterArguments, 'callback'); filter.init(filterArgs); filter.clear(false); expect(filter.getValues()).toBe(0); - expect(spyCallback).toHaveBeenLastCalledWith(undefined, { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: false }); + expect(callbackSpy).toHaveBeenLastCalledWith(undefined, { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: false }); }); it('should trigger a callback with the clear filter set when calling the "clear" method and expect min slider values being with values of "sliderStartValue" when defined through the filter params', () => { const filterArgs = { ...filterArguments, operator: '<=', searchTerms: [3], grid: gridStub, } as FilterArguments; - const spyCallback = jest.spyOn(filterArguments, 'callback'); + const callbackSpy = jest.spyOn(filterArguments, 'callback'); mockColumn.filter = { params: { sliderStartValue: 4, @@ -242,7 +248,7 @@ describe('CompoundSliderFilter', () => { filter.clear(false); expect(filter.getValues()).toEqual(4); - expect(spyCallback).toHaveBeenLastCalledWith(undefined, { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: false }); + expect(callbackSpy).toHaveBeenLastCalledWith(undefined, { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: false }); }); it('should create the input filter with all available operators in a select dropdown options as a prepend element', () => { diff --git a/packages/common/src/filters/__tests__/sliderFilter.spec.ts b/packages/common/src/filters/__tests__/sliderFilter.spec.ts index 1747d37bd..c2ad7aa0e 100644 --- a/packages/common/src/filters/__tests__/sliderFilter.spec.ts +++ b/packages/common/src/filters/__tests__/sliderFilter.spec.ts @@ -4,6 +4,7 @@ import { SliderFilter } from '../sliderFilter'; const containerId = 'demo-container'; declare const Slick: SlickNamespace; +jest.useFakeTimers(); // define a
container to simulate the grid container const template = `
`; @@ -19,6 +20,7 @@ const gridStub = { getHeaderRowColumn: jest.fn(), render: jest.fn(), onHeaderMouseLeave: new Slick.Event(), + onHeaderRowMouseEnter: new Slick.Event(), } as unknown as SlickGrid; describe('SliderFilter', () => { @@ -69,18 +71,22 @@ describe('SliderFilter', () => { }); it('should call "setValues" and expect that value to be in the callback when triggered', () => { - const spyCallback = jest.spyOn(filterArgs, 'callback'); + const callbackSpy = jest.spyOn(filterArgs, 'callback'); + const rowMouseEnterSpy = jest.spyOn(gridStub.onHeaderRowMouseEnter, 'notify'); filter.init(filterArgs); filter.setValues(['2']); const filterElm = divContainer.querySelector('.search-filter.slider-container.filter-duration input') as HTMLInputElement; filterElm.dispatchEvent(new CustomEvent('change')); - expect(spyCallback).toHaveBeenLastCalledWith(new CustomEvent('change'), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['2'], shouldTriggerQuery: true }); + jest.runAllTimers(); // fast-forward timer + + expect(callbackSpy).toHaveBeenLastCalledWith(new CustomEvent('change'), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['2'], shouldTriggerQuery: true }); + expect(rowMouseEnterSpy).toHaveBeenCalledWith({ column: mockColumn, grid: gridStub }, expect.anything()); }); it('should call "setValues" and expect that value, converted as a string, to be in the callback when triggered', () => { - const spyCallback = jest.spyOn(filterArgs, 'callback'); + const callbackSpy = jest.spyOn(filterArgs, 'callback'); filter.init(filterArgs); filter.setValues(3); @@ -92,7 +98,7 @@ describe('SliderFilter', () => { const filterFilledElms = divContainer.querySelectorAll('.search-filter.slider-container.filter-duration.filled'); expect(filterFilledElms.length).toBe(1); - expect(spyCallback).toHaveBeenLastCalledWith(new CustomEvent('change'), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['13'], shouldTriggerQuery: true }); + expect(callbackSpy).toHaveBeenLastCalledWith(new CustomEvent('change'), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['13'], shouldTriggerQuery: true }); }); it('should be able to call "setValues" and set empty values and the input to not have the "filled" css class', () => { @@ -176,28 +182,28 @@ describe('SliderFilter', () => { it('should trigger a callback with the clear filter set when calling the "clear" method', () => { filterArgs.searchTerms = [3]; - const spyCallback = jest.spyOn(filterArgs, 'callback'); + const callbackSpy = jest.spyOn(filterArgs, 'callback'); filter.init(filterArgs); filter.clear(); expect(filter.getValues()).toBe(0); - expect(spyCallback).toHaveBeenLastCalledWith(new Event('change'), { columnDef: mockColumn, clearFilterTriggered: true, searchTerms: [], shouldTriggerQuery: true }); + expect(callbackSpy).toHaveBeenLastCalledWith(new Event('change'), { columnDef: mockColumn, clearFilterTriggered: true, searchTerms: [], shouldTriggerQuery: true }); }); it('should trigger a callback with the clear filter but without querying when when calling the "clear" method with False as argument', () => { filterArgs.searchTerms = [3]; - const spyCallback = jest.spyOn(filterArgs, 'callback'); + const callbackSpy = jest.spyOn(filterArgs, 'callback'); filter.init(filterArgs); filter.clear(false); expect(filter.getValues()).toBe(0); - expect(spyCallback).toHaveBeenLastCalledWith(new Event('change'), { columnDef: mockColumn, clearFilterTriggered: true, searchTerms: [], shouldTriggerQuery: false }); + expect(callbackSpy).toHaveBeenLastCalledWith(new Event('change'), { columnDef: mockColumn, clearFilterTriggered: true, searchTerms: [], shouldTriggerQuery: false }); }); it('should trigger a callback with the clear filter set when calling the "clear" method and expect min slider values being with values of "sliderStartValue" when defined through the filter params', () => { - const spyCallback = jest.spyOn(filterArgs, 'callback'); + const callbackSpy = jest.spyOn(filterArgs, 'callback'); mockColumn.filter = { params: { sliderStartValue: 4, @@ -209,6 +215,6 @@ describe('SliderFilter', () => { filter.clear(false); expect(filter.getValues()).toEqual(4); - expect(spyCallback).toHaveBeenLastCalledWith(new Event('change'), { columnDef: mockColumn, clearFilterTriggered: true, searchTerms: [], shouldTriggerQuery: false }); + expect(callbackSpy).toHaveBeenLastCalledWith(new Event('change'), { columnDef: mockColumn, clearFilterTriggered: true, searchTerms: [], shouldTriggerQuery: false }); }); }); diff --git a/packages/common/src/filters/__tests__/sliderRangeFilter.spec.ts b/packages/common/src/filters/__tests__/sliderRangeFilter.spec.ts index 2a31d4c55..fe92dc65d 100644 --- a/packages/common/src/filters/__tests__/sliderRangeFilter.spec.ts +++ b/packages/common/src/filters/__tests__/sliderRangeFilter.spec.ts @@ -4,6 +4,7 @@ import { SliderRangeFilter } from '../sliderRangeFilter'; const containerId = 'demo-container'; declare const Slick: SlickNamespace; +jest.useFakeTimers(); // define a
container to simulate the grid container const template = `
`; @@ -19,6 +20,7 @@ const gridStub = { getHeaderRowColumn: jest.fn(), render: jest.fn(), onHeaderMouseLeave: new Slick.Event(), + onHeaderRowMouseEnter: new Slick.Event(), } as unknown as SlickGrid; describe('SliderRangeFilter', () => { @@ -89,18 +91,22 @@ describe('SliderRangeFilter', () => { }); it('should call "setValues" and expect that value to be in the callback when triggered', () => { - const spyCallback = jest.spyOn(filterArguments, 'callback'); + const callbackSpy = jest.spyOn(filterArguments, 'callback'); + const rowMouseEnterSpy = jest.spyOn(gridStub.onHeaderRowMouseEnter, 'notify'); filter.init(filterArguments); filter.setValues(['2..80']); const filterElms = divContainer.querySelectorAll('.search-filter.slider-container.filter-duration input'); filterElms[0].dispatchEvent(new CustomEvent('change')); - expect(spyCallback).toHaveBeenLastCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'RangeInclusive', searchTerms: [2, 80], shouldTriggerQuery: true }); + jest.runAllTimers(); // fast-forward timer + + expect(callbackSpy).toHaveBeenLastCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'RangeInclusive', searchTerms: [2, 80], shouldTriggerQuery: true }); + expect(rowMouseEnterSpy).toHaveBeenCalledWith({ column: mockColumn, grid: gridStub }, expect.anything()); }); it('should call "setValues" and expect that value to be in the callback when triggered', () => { - const spyCallback = jest.spyOn(filterArguments, 'callback'); + const callbackSpy = jest.spyOn(filterArguments, 'callback'); filter.init(filterArguments); filter.setValues([3, 84]); @@ -110,11 +116,11 @@ describe('SliderRangeFilter', () => { expect(sliderInputs[0].style.zIndex).toBe('0'); expect(sliderInputs[1].style.zIndex).toBe('1'); - expect(spyCallback).toHaveBeenLastCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'RangeInclusive', searchTerms: [3, 84], shouldTriggerQuery: true }); + expect(callbackSpy).toHaveBeenLastCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'RangeInclusive', searchTerms: [3, 84], shouldTriggerQuery: true }); }); it('should change z-index on left handle when it is by 20px near right handle so it shows over the right handle not below', () => { - const spyCallback = jest.spyOn(filterArguments, 'callback'); + const callbackSpy = jest.spyOn(filterArguments, 'callback'); filter.init(filterArguments); filter.setValues([50, 63]); @@ -124,11 +130,11 @@ describe('SliderRangeFilter', () => { expect(sliderInputs[0].style.zIndex).toBe('1'); expect(sliderInputs[1].style.zIndex).toBe('0'); - expect(spyCallback).toHaveBeenLastCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'RangeInclusive', searchTerms: [50, 63], shouldTriggerQuery: true }); + expect(callbackSpy).toHaveBeenLastCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'RangeInclusive', searchTerms: [50, 63], shouldTriggerQuery: true }); }); it('should change minValue to a lower value when it is to close to maxValue and "stopGapBetweenSliderHandles" is enabled so it will auto-change minValue to a lower value plus gap', () => { - const spyCallback = jest.spyOn(filterArguments, 'callback'); + const callbackSpy = jest.spyOn(filterArguments, 'callback'); const minVal = 56; const maxVal = 58; @@ -143,11 +149,11 @@ describe('SliderRangeFilter', () => { expect(sliderInputs[0].value).toBe(`${minVal - 5}`); expect(sliderInputs[1].value).toBe('58'); - expect(spyCallback).toHaveBeenLastCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'RangeInclusive', searchTerms: [51, 58], shouldTriggerQuery: true }); + expect(callbackSpy).toHaveBeenLastCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'RangeInclusive', searchTerms: [51, 58], shouldTriggerQuery: true }); }); it('should change maxValue to a lower value when it is to close to minValue and "stopGapBetweenSliderHandles" is enabled so it will auto-change maxValue to a lower value plus gap', () => { - const spyCallback = jest.spyOn(filterArguments, 'callback'); + const callbackSpy = jest.spyOn(filterArguments, 'callback'); const minVal = 56; const maxVal = 58; @@ -162,7 +168,7 @@ describe('SliderRangeFilter', () => { expect(sliderInputs[0].value).toBe('56'); expect(sliderInputs[1].value).toBe(`${minVal + 5}`); - expect(spyCallback).toHaveBeenLastCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'RangeInclusive', searchTerms: [56, 61], shouldTriggerQuery: true }); + expect(callbackSpy).toHaveBeenLastCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'RangeInclusive', searchTerms: [56, 61], shouldTriggerQuery: true }); }); it('should be able to call "setValues" and set empty values and the input to not have the "filled" css class', () => { @@ -259,28 +265,28 @@ describe('SliderRangeFilter', () => { it('should trigger a callback with the clear filter set when calling the "clear" method', () => { filterArguments.searchTerms = [3, 80]; - const spyCallback = jest.spyOn(filterArguments, 'callback'); + const callbackSpy = jest.spyOn(filterArguments, 'callback'); filter.init(filterArguments); filter.clear(); expect(filter.currentValues).toEqual([0, 100]); - expect(spyCallback).toHaveBeenLastCalledWith(undefined, { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: true }); + expect(callbackSpy).toHaveBeenLastCalledWith(undefined, { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: true }); }); it('should trigger a callback with the clear filter but without querying when when calling the "clear" method with False as argument', () => { filterArguments.searchTerms = [3, 80]; - const spyCallback = jest.spyOn(filterArguments, 'callback'); + const callbackSpy = jest.spyOn(filterArguments, 'callback'); filter.init(filterArguments); filter.clear(false); expect(filter.currentValues).toEqual([0, 100]); - expect(spyCallback).toHaveBeenLastCalledWith(undefined, { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: false }); + expect(callbackSpy).toHaveBeenLastCalledWith(undefined, { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: false }); }); it('should trigger a callback with the clear filter set when calling the "clear" method and expect min/max slider values being with values of "sliderStartValue" and "sliderEndValue" when defined through the filterOptions', () => { - const spyCallback = jest.spyOn(filterArguments, 'callback'); + const callbackSpy = jest.spyOn(filterArguments, 'callback'); mockColumn.filter = { filterOptions: { sliderStartValue: 4, @@ -292,7 +298,7 @@ describe('SliderRangeFilter', () => { filter.clear(false); expect(filter.currentValues).toEqual([4, 69]); - expect(spyCallback).toHaveBeenLastCalledWith(undefined, { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: false }); + expect(callbackSpy).toHaveBeenLastCalledWith(undefined, { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: false }); }); it('should enableSliderTrackColoring and trigger a change event and expect slider track to have background color', () => { diff --git a/packages/common/src/filters/compoundSliderFilter.ts b/packages/common/src/filters/compoundSliderFilter.ts index 78fa6aa27..f43201e91 100644 --- a/packages/common/src/filters/compoundSliderFilter.ts +++ b/packages/common/src/filters/compoundSliderFilter.ts @@ -10,6 +10,7 @@ import { GridOption, OperatorDetail, SlickGrid, + SlickNamespace, } from '../interfaces/index'; import { Constants } from '../constants'; import { OperatorString, OperatorType, SearchTerm } from '../enums/index'; @@ -19,6 +20,8 @@ import { mapOperatorToShorthandDesignation, } from '../services/utilities'; import { BindingEventService } from '../services/bindingEvent.service'; import { TranslaterService } from '../services/translater.service'; +declare const Slick: SlickNamespace; + export class CompoundSliderFilter implements Filter { protected _bindEventService: BindingEventService; protected _clearFilterTriggered = false; @@ -286,7 +289,11 @@ export class CompoundSliderFilter implements Filter { this._clearFilterTriggered = false; this._shouldTriggerQuery = true; - // trigger leave event to avoid having previous value still being displayed with custom tooltip feat - this.grid?.onHeaderMouseLeave.notify({ column: this.columnDef, grid: this.grid }); + // trigger mouse enter event on the filter for optionally hooked SlickCustomTooltip + // the minimum requirements for tooltip to work are the columnDef and targetElement + setTimeout(() => this.grid.onHeaderRowMouseEnter.notify( + { column: this.columnDef, grid: this.grid }, + { ...new Slick.EventData(), target: this.filterContainerElm } + )); } } diff --git a/packages/common/src/filters/sliderFilter.ts b/packages/common/src/filters/sliderFilter.ts index 86f9b7b41..d5051f9a0 100644 --- a/packages/common/src/filters/sliderFilter.ts +++ b/packages/common/src/filters/sliderFilter.ts @@ -10,10 +10,13 @@ import { FilterArguments, FilterCallback, SlickGrid, + SlickNamespace, } from '../interfaces/index'; import { BindingEventService } from '../services/bindingEvent.service'; import { createDomElement, emptyElement, } from '../services/domUtilities'; +declare const Slick: SlickNamespace; + export class SliderFilter implements Filter { protected _bindEventService: BindingEventService; protected _clearFilterTriggered = false; @@ -246,7 +249,11 @@ export class SliderFilter implements Filter { this._clearFilterTriggered = false; this._shouldTriggerQuery = true; - // trigger leave event to avoid having previous value still being displayed with custom tooltip feat - this.grid?.onHeaderMouseLeave.notify({ column: this.columnDef, grid: this.grid }); + // trigger mouse enter event on the filter for optionally hooked SlickCustomTooltip + // the minimum requirements for tooltip to work are the columnDef and targetElement + setTimeout(() => this.grid.onHeaderRowMouseEnter.notify( + { column: this.columnDef, grid: this.grid }, + { ...new Slick.EventData(), target: this.filterContainerElm } + )); } } diff --git a/packages/common/src/filters/sliderRangeFilter.ts b/packages/common/src/filters/sliderRangeFilter.ts index 6c0ff8a90..2e9c97a96 100644 --- a/packages/common/src/filters/sliderRangeFilter.ts +++ b/packages/common/src/filters/sliderRangeFilter.ts @@ -10,6 +10,7 @@ import { FilterCallback, GridOption, SlickGrid, + SlickNamespace, SliderRangeOption, } from '../interfaces/index'; import { BindingEventService } from '../services/bindingEvent.service'; @@ -22,6 +23,7 @@ interface CurrentSliderOption { sliderTrackBackground?: string; } +declare const Slick: SlickNamespace; const GAP_BETWEEN_SLIDER_HANDLES = 0; const Z_INDEX_MIN_GAP = 20; // gap in Px before we change z-index so that lowest/highest handle doesn't block each other @@ -36,12 +38,12 @@ export class SliderRangeFilter implements Filter { protected _argFilterContainerElm!: HTMLDivElement; protected _divContainerFilterElm!: HTMLDivElement; protected _filterContainerElm!: HTMLDivElement; - protected _lowestSliderNumberElm?: HTMLSpanElement; - protected _highestSliderNumberElm?: HTMLSpanElement; + protected _leftSliderNumberElm?: HTMLSpanElement; + protected _rightSliderNumberElm?: HTMLSpanElement; protected _sliderRangeContainElm!: HTMLDivElement; protected _sliderTrackElm!: HTMLDivElement; - protected _sliderOneElm!: HTMLInputElement; - protected _sliderTwoElm!: HTMLInputElement; + protected _sliderLeftElm!: HTMLInputElement; + protected _sliderRightElm!: HTMLInputElement; grid!: SlickGrid; searchTerms: SearchTerm[] = []; columnDef!: Column; @@ -78,7 +80,7 @@ export class SliderRangeFilter implements Filter { /** Getter for the Grid Options pulled through the Grid Object */ get gridOptions(): GridOption { - return (this.grid && this.grid.getOptions) ? this.grid.getOptions() : {}; + return this.grid?.getOptions() ?? {}; } /** Getter for the current Slider Options */ @@ -126,8 +128,8 @@ export class SliderRangeFilter implements Filter { const lowestValue = (this.getFilterOptionByName('sliderStartValue') ?? Constants.SLIDER_DEFAULT_MIN_VALUE) as number; const highestValue = (this.getFilterOptionByName('sliderEndValue') ?? Constants.SLIDER_DEFAULT_MAX_VALUE) as number; this._currentValues = [lowestValue, highestValue]; - this._sliderOneElm.value = `${lowestValue}`; - this._sliderTwoElm.value = `${highestValue}`; + this._sliderLeftElm.value = `${lowestValue}`; + this._sliderRightElm.value = `${highestValue}`; this.dispatchBothEvents(); if (!this.getFilterOptionByName('hideSliderNumbers')) { @@ -167,11 +169,11 @@ export class SliderRangeFilter implements Filter { * @param highestValue number */ renderSliderValues(lowestValue: number | string, highestValue: number | string) { - if (this._lowestSliderNumberElm?.textContent) { - this._lowestSliderNumberElm.textContent = lowestValue.toString(); + if (this._leftSliderNumberElm?.textContent) { + this._leftSliderNumberElm.textContent = lowestValue.toString(); } - if (this._highestSliderNumberElm?.textContent) { - this._highestSliderNumberElm.textContent = highestValue.toString(); + if (this._rightSliderNumberElm?.textContent) { + this._rightSliderNumberElm.textContent = highestValue.toString(); } } @@ -197,8 +199,8 @@ export class SliderRangeFilter implements Filter { if (Array.isArray(sliderValues) && sliderValues.length === 2) { if (!this.getFilterOptionByName('hideSliderNumbers')) { const [lowestValue, highestValue] = sliderValues; - this._sliderOneElm.value = String(lowestValue ?? Constants.SLIDER_DEFAULT_MIN_VALUE); - this._sliderTwoElm.value = String(highestValue ?? Constants.SLIDER_DEFAULT_MAX_VALUE); + this._sliderLeftElm.value = String(lowestValue ?? Constants.SLIDER_DEFAULT_MIN_VALUE); + this._sliderRightElm.value = String(highestValue ?? Constants.SLIDER_DEFAULT_MAX_VALUE); this.renderSliderValues(sliderValues[0], sliderValues[1]); } } @@ -239,14 +241,14 @@ export class SliderRangeFilter implements Filter { this._sliderRangeContainElm.title = `${defaultStartValue} - ${defaultEndValue}`; this._sliderTrackElm = createDomElement('div', { className: 'slider-track' }); - this._sliderOneElm = createDomElement('input', { + this._sliderLeftElm = createDomElement('input', { type: 'range', className: `slider-filter-input`, ariaLabel: this.columnFilter?.ariaLabel ?? `${toSentenceCase(columnId + '')} Search Filter`, defaultValue: `${defaultStartValue}`, value: `${defaultStartValue}`, min: `${minValue}`, max: `${maxValue}`, step: `${step}`, }); - this._sliderTwoElm = createDomElement('input', { + this._sliderRightElm = createDomElement('input', { type: 'range', className: `slider-filter-input`, ariaLabel: this.columnFilter?.ariaLabel ?? `${toSentenceCase(columnId + '')} Search Filter`, @@ -255,29 +257,29 @@ export class SliderRangeFilter implements Filter { }); this._bindEventService.bind(this._sliderTrackElm, 'click', this.sliderTrackClicked.bind(this) as EventListener); - this._bindEventService.bind(this._sliderOneElm, ['input', 'change'], this.slideOneInputChanged.bind(this)); - this._bindEventService.bind(this._sliderTwoElm, ['input', 'change'], this.slideTwoInputChanged.bind(this)); - this._bindEventService.bind(this._sliderOneElm, ['change', 'mouseup', 'touchend'], this.onValueChanged.bind(this) as EventListener); - this._bindEventService.bind(this._sliderTwoElm, ['change', 'mouseup', 'touchend'], this.onValueChanged.bind(this) as EventListener); + this._bindEventService.bind(this._sliderLeftElm, ['input', 'change'], this.slideLeftInputChanged.bind(this)); + this._bindEventService.bind(this._sliderRightElm, ['input', 'change'], this.slideRightInputChanged.bind(this)); + this._bindEventService.bind(this._sliderLeftElm, ['change', 'mouseup', 'touchend'], this.onValueChanged.bind(this) as EventListener); + this._bindEventService.bind(this._sliderRightElm, ['change', 'mouseup', 'touchend'], this.onValueChanged.bind(this) as EventListener); // create the DOM element const sliderNumberClass = this.getFilterOptionByName('hideSliderNumbers') ? '' : 'input-group'; this._divContainerFilterElm = createDomElement('div', { className: `${sliderNumberClass} search-filter slider-container slider-values filter-${columnId}`.trim() }); this._sliderRangeContainElm.append(this._sliderTrackElm); - this._sliderRangeContainElm.append(this._sliderOneElm); - this._sliderRangeContainElm.append(this._sliderTwoElm); + this._sliderRangeContainElm.append(this._sliderLeftElm); + this._sliderRangeContainElm.append(this._sliderRightElm); if (this.getFilterOptionByName('hideSliderNumbers')) { this._divContainerFilterElm.append(this._sliderRangeContainElm); } else { const lowestSliderContainerDivElm = createDomElement('div', { className: `input-group-addon input-group-prepend slider-range-value` }); - this._lowestSliderNumberElm = createDomElement('span', { className: `input-group-text lowest-range-${columnId}`, textContent: `${defaultStartValue}` }); - lowestSliderContainerDivElm.append(this._lowestSliderNumberElm); + this._leftSliderNumberElm = createDomElement('span', { className: `input-group-text lowest-range-${columnId}`, textContent: `${defaultStartValue}` }); + lowestSliderContainerDivElm.append(this._leftSliderNumberElm); const highestSliderContainerDivElm = createDomElement('div', { className: `input-group-addon input-group-append slider-range-value` }); - this._highestSliderNumberElm = createDomElement('span', { className: `input-group-text highest-range-${columnId}`, textContent: `${defaultEndValue}` }); - highestSliderContainerDivElm.append(this._highestSliderNumberElm); + this._rightSliderNumberElm = createDomElement('span', { className: `input-group-text highest-range-${columnId}`, textContent: `${defaultEndValue}` }); + highestSliderContainerDivElm.append(this._rightSliderNumberElm); this._divContainerFilterElm.append(lowestSliderContainerDivElm); this._divContainerFilterElm.append(this._sliderRangeContainElm); @@ -303,15 +305,15 @@ export class SliderRangeFilter implements Filter { } protected dispatchBothEvents() { - this._sliderOneElm.dispatchEvent(new Event('change')); - this._sliderTwoElm.dispatchEvent(new Event('change')); + this._sliderLeftElm.dispatchEvent(new Event('change')); + this._sliderRightElm.dispatchEvent(new Event('change')); } /** handle value change event triggered, trigger filter callback & update "filled" class name */ - protected onValueChanged(e: Event) { - const sliderOneVal = parseInt(this._sliderOneElm.value, 10); - const sliderTwoVal = parseInt(this._sliderTwoElm.value, 10); - const values = [sliderOneVal, sliderTwoVal]; + protected onValueChanged(e: MouseEvent) { + const sliderLeftVal = parseInt(this._sliderLeftElm.value, 10); + const sliderRightVal = parseInt(this._sliderRightElm.value, 10); + const values = [sliderLeftVal, sliderRightVal]; const value = values.join('..'); if (this._clearFilterTriggered) { @@ -326,59 +328,63 @@ export class SliderRangeFilter implements Filter { this._shouldTriggerQuery = true; this.changeBothSliderFocuses(false); - // trigger leave event to avoid having previous value still being displayed with custom tooltip feat - this.grid?.onHeaderMouseLeave.notify({ column: this.columnDef, grid: this.grid }); + // trigger mouse enter event on the filter for optionally hooked SlickCustomTooltip + // the minimum requirements for tooltip to work are the columnDef and targetElement + setTimeout(() => this.grid.onHeaderRowMouseEnter.notify( + { column: this.columnDef, grid: this.grid }, + { ...new Slick.EventData(), target: this._argFilterContainerElm } + )); } protected changeBothSliderFocuses(isAddingFocus: boolean) { const addRemoveCmd = isAddingFocus ? 'add' : 'remove'; - this._sliderOneElm.classList[addRemoveCmd]('focus'); - this._sliderTwoElm.classList[addRemoveCmd]('focus'); + this._sliderLeftElm.classList[addRemoveCmd]('focus'); + this._sliderRightElm.classList[addRemoveCmd]('focus'); } - protected slideOneInputChanged() { - const sliderOneVal = parseInt(this._sliderOneElm.value, 10); - const sliderTwoVal = parseInt(this._sliderTwoElm.value, 10); + protected slideLeftInputChanged() { + const sliderLeftVal = parseInt(this._sliderLeftElm.value, 10); + const sliderRightVal = parseInt(this._sliderRightElm.value, 10); - if (sliderTwoVal - sliderOneVal <= this.getFilterOptionByName('stopGapBetweenSliderHandles', GAP_BETWEEN_SLIDER_HANDLES)) { - this._sliderOneElm.value = String(sliderOneVal - this.getFilterOptionByName('stopGapBetweenSliderHandles', GAP_BETWEEN_SLIDER_HANDLES)); + if (sliderRightVal - sliderLeftVal <= this.getFilterOptionByName('stopGapBetweenSliderHandles', GAP_BETWEEN_SLIDER_HANDLES)) { + this._sliderLeftElm.value = String(sliderLeftVal - this.getFilterOptionByName('stopGapBetweenSliderHandles', GAP_BETWEEN_SLIDER_HANDLES)); } - this._sliderRangeContainElm.title = `${sliderOneVal} - ${sliderTwoVal}`; + this._sliderRangeContainElm.title = `${sliderLeftVal} - ${sliderRightVal}`; // change which handle has higher z-index to make them still usable, // ie when left handle reaches the end, it has to have higher z-index or else it will be stuck below // and we cannot move right because it cannot go below min value - if (+this._sliderOneElm.value >= +this._sliderTwoElm.value - Z_INDEX_MIN_GAP) { - this._sliderOneElm.style.zIndex = '1'; - this._sliderTwoElm.style.zIndex = '0'; + if (+this._sliderLeftElm.value >= +this._sliderRightElm.value - Z_INDEX_MIN_GAP) { + this._sliderLeftElm.style.zIndex = '1'; + this._sliderRightElm.style.zIndex = '0'; } else { - this._sliderOneElm.style.zIndex = '0'; - this._sliderTwoElm.style.zIndex = '1'; + this._sliderLeftElm.style.zIndex = '0'; + this._sliderRightElm.style.zIndex = '1'; } this.updateTrackFilledColor(); this.changeBothSliderFocuses(true); - if (!this.getFilterOptionByName('hideSliderNumbers') && this._lowestSliderNumberElm?.textContent) { - this._lowestSliderNumberElm.textContent = this._sliderOneElm.value; + if (!this.getFilterOptionByName('hideSliderNumbers') && this._leftSliderNumberElm?.textContent) { + this._leftSliderNumberElm.textContent = this._sliderLeftElm.value; } } - protected slideTwoInputChanged() { - const sliderOneVal = parseInt(this._sliderOneElm.value, 10); - const sliderTwoVal = parseInt(this._sliderTwoElm.value, 10); + protected slideRightInputChanged() { + const sliderLeftVal = parseInt(this._sliderLeftElm.value, 10); + const sliderRightVal = parseInt(this._sliderRightElm.value, 10); - if (sliderTwoVal - sliderOneVal <= this.getFilterOptionByName('stopGapBetweenSliderHandles', GAP_BETWEEN_SLIDER_HANDLES)) { - this._sliderTwoElm.value = String(sliderOneVal + this.getFilterOptionByName('stopGapBetweenSliderHandles', GAP_BETWEEN_SLIDER_HANDLES)); + if (sliderRightVal - sliderLeftVal <= this.getFilterOptionByName('stopGapBetweenSliderHandles', GAP_BETWEEN_SLIDER_HANDLES)) { + this._sliderRightElm.value = String(sliderLeftVal + this.getFilterOptionByName('stopGapBetweenSliderHandles', GAP_BETWEEN_SLIDER_HANDLES)); } this.updateTrackFilledColor(); this.changeBothSliderFocuses(true); - this._sliderRangeContainElm.title = `${sliderOneVal} - ${sliderTwoVal}`; + this._sliderRangeContainElm.title = `${sliderLeftVal} - ${sliderRightVal}`; - if (!this.getFilterOptionByName('hideSliderNumbers') && this._highestSliderNumberElm?.textContent) { - this._highestSliderNumberElm.textContent = this._sliderTwoElm.value; + if (!this.getFilterOptionByName('hideSliderNumbers') && this._rightSliderNumberElm?.textContent) { + this._rightSliderNumberElm.textContent = this._sliderRightElm.value; } } @@ -390,18 +396,18 @@ export class SliderRangeFilter implements Filter { // when tracker position is below 50% we'll auto-place the left slider thumb or else auto-place right slider thumb if (trackPercentPosition <= 50) { - this._sliderOneElm.value = `${trackPercentPosition}`; - this._sliderOneElm.dispatchEvent(new Event('change')); + this._sliderLeftElm.value = `${trackPercentPosition}`; + this._sliderLeftElm.dispatchEvent(new Event('change')); } else { - this._sliderTwoElm.value = `${trackPercentPosition}`; - this._sliderTwoElm.dispatchEvent(new Event('change')); + this._sliderRightElm.value = `${trackPercentPosition}`; + this._sliderRightElm.dispatchEvent(new Event('change')); } } protected updateTrackFilledColor() { if (this.getFilterOptionByName('enableSliderTrackColoring')) { - const percent1 = ((+this._sliderOneElm.value - +this._sliderOneElm.min) / (this.sliderRangeOptions?.maxValue ?? 0 - +this._sliderOneElm.min)) * 100; - const percent2 = ((+this._sliderTwoElm.value - +this._sliderTwoElm.min) / (this.sliderRangeOptions?.maxValue ?? 0 - +this._sliderTwoElm.min)) * 100; + const percent1 = ((+this._sliderLeftElm.value - +this._sliderLeftElm.min) / (this.sliderRangeOptions?.maxValue ?? 0 - +this._sliderLeftElm.min)) * 100; + const percent2 = ((+this._sliderRightElm.value - +this._sliderRightElm.min) / (this.sliderRangeOptions?.maxValue ?? 0 - +this._sliderRightElm.min)) * 100; const bg = 'linear-gradient(to right, %b %p1, %c %p1, %c %p2, %b %p2)' .replace(/%b/g, '#eee') .replace(/%c/g, (this.getFilterOptionByName('sliderTrackFilledColor') ?? 'var(--slick-slider-filter-thumb-color, #86bff8)') as string)