From eceace571fd93bcc3f3867e1cda2b6224bb0bcd1 Mon Sep 17 00:00:00 2001 From: jajugoguma Date: Wed, 29 Dec 2021 11:24:22 +0900 Subject: [PATCH 1/8] fix: editors with drop-down layer to follow scrolling --- packages/toast-ui.grid/src/editor/checkbox.ts | 21 ++++- .../toast-ui.grid/src/editor/datePicker.ts | 21 ++++- packages/toast-ui.grid/src/editor/dom.ts | 47 ++++++++++-- packages/toast-ui.grid/src/editor/select.ts | 21 ++++- packages/toast-ui.grid/src/helper/common.ts | 4 + .../toast-ui.grid/src/view/editingLayer.tsx | 76 +++++++++++++++++-- .../toast-ui.grid/types/editor/index.d.ts | 17 +++++ 7 files changed, 183 insertions(+), 24 deletions(-) diff --git a/packages/toast-ui.grid/src/editor/checkbox.ts b/packages/toast-ui.grid/src/editor/checkbox.ts index 766c63d4e..415fcc594 100644 --- a/packages/toast-ui.grid/src/editor/checkbox.ts +++ b/packages/toast-ui.grid/src/editor/checkbox.ts @@ -1,10 +1,16 @@ -import { CellEditor, CellEditorProps, PortalEditingKeydown } from '@t/editor'; +import { + CellEditor, + CellEditorProps, + GridRectForDropDownLayerPos, + LayerPos, + PortalEditingKeydown, +} from '@t/editor'; import { CellValue, ListItem } from '@t/store/data'; import { getListItems } from '../helper/editor'; import { cls, hasClass } from '../helper/dom'; import { getKeyStrokeString, isArrowKey } from '../helper/keyboard'; import { findIndex, isNil } from '../helper/common'; -import { getContainerElement, setLayerPosition, setOpacity } from './dom'; +import { getContainerElement, setLayerPosition, setOpacity, moveLayer } from './dom'; const LAYER_CLASSNAME = cls('editor-checkbox-list-layer'); const LIST_ITEM_CLASSNAME = cls('editor-checkbox'); @@ -27,6 +33,8 @@ export class CheckboxEditor implements CellEditor { private elementIds: string[] = []; + private initLayerPos: LayerPos | null = null; + public constructor(props: CellEditorProps) { const { columnInfo, width, formattedValue, portalEditingKeydown } = props; const el = document.createElement('div'); @@ -159,6 +167,12 @@ export class CheckboxEditor implements CellEditor { this.layer.querySelector('input')) as HTMLInputElement; } + public moveDropdownLayer(gridRect: GridRectForDropDownLayerPos) { + if (this.initLayerPos) { + moveLayer(this.layer, this.initLayerPos, gridRect); + } + } + public getElement() { return this.el; } @@ -189,7 +203,7 @@ export class CheckboxEditor implements CellEditor { // To prevent wrong stacked z-index context, layer append to grid container getContainerElement(this.el).appendChild(this.layer); // @ts-ignore - setLayerPosition(this.el, this.layer); + this.initLayerPos = setLayerPosition(this.el, this.layer); const checkedInput = this.getCheckedInput(); if (checkedInput) { @@ -204,5 +218,6 @@ export class CheckboxEditor implements CellEditor { this.layer.removeEventListener('mouseover', this.onMouseover); this.layer.removeEventListener('keydown', this.onKeydown); getContainerElement(this.el).removeChild(this.layer); + this.initLayerPos = null; } } diff --git a/packages/toast-ui.grid/src/editor/datePicker.ts b/packages/toast-ui.grid/src/editor/datePicker.ts index 4db027221..fefc96b16 100644 --- a/packages/toast-ui.grid/src/editor/datePicker.ts +++ b/packages/toast-ui.grid/src/editor/datePicker.ts @@ -1,9 +1,9 @@ import TuiDatePicker from 'tui-date-picker'; import { Dictionary } from '@t/options'; -import { CellEditor, CellEditorProps } from '@t/editor'; +import { CellEditor, CellEditorProps, GridRectForDropDownLayerPos, LayerPos } from '@t/editor'; import { cls } from '../helper/dom'; import { deepMergedCopy, isNumber, isString, isNil } from '../helper/common'; -import { setLayerPosition, getContainerElement, setOpacity } from './dom'; +import { setLayerPosition, getContainerElement, setOpacity, moveLayer } from './dom'; export class DatePickerEditor implements CellEditor { public el: HTMLDivElement; @@ -16,6 +16,8 @@ export class DatePickerEditor implements CellEditor { private iconEl?: HTMLElement; + private initLayerPos: LayerPos | null = null; + private createInputElement() { const inputEl = document.createElement('input'); inputEl.className = cls('content-text'); @@ -102,6 +104,12 @@ export class DatePickerEditor implements CellEditor { this.datePickerEl.on('close', () => this.focus()); } + public moveDropdownLayer(gridRect: GridRectForDropDownLayerPos) { + if (this.initLayerPos) { + moveLayer(this.layer, this.initLayerPos, gridRect); + } + } + public getElement() { return this.el; } @@ -118,7 +126,13 @@ export class DatePickerEditor implements CellEditor { this.datePickerEl.open(); // `this.layer.firstElementChild` is real datePicker layer(it is need to get total height) - setLayerPosition(this.el, this.layer, this.layer.firstElementChild as HTMLElement, true); + this.initLayerPos = setLayerPosition( + this.el, + this.layer, + this.layer.firstElementChild as HTMLElement, + true + ); + // To show the layer which has appropriate position setOpacity(this.layer, 1); } @@ -129,5 +143,6 @@ export class DatePickerEditor implements CellEditor { } this.datePickerEl.destroy(); getContainerElement(this.el).removeChild(this.layer); + this.initLayerPos = null; } } diff --git a/packages/toast-ui.grid/src/editor/dom.ts b/packages/toast-ui.grid/src/editor/dom.ts index b68c24a41..2f1069142 100644 --- a/packages/toast-ui.grid/src/editor/dom.ts +++ b/packages/toast-ui.grid/src/editor/dom.ts @@ -1,9 +1,22 @@ +import { GridRectForDropDownLayerPos, LayerPos } from '@t/editor'; +import { isBetween } from '../helper/common'; import { findParentByClassName } from '../helper/dom'; const INDENT = 5; const SCROLL_BAR_WIDTH = 17; const SCROLL_BAR_HEIGHT = 17; +function isHidden( + top: number, + left: number, + { bodyHeight, bodyWidth, headerHeight, leftSideWidth }: GridRectForDropDownLayerPos +) { + return !( + isBetween(top, headerHeight, bodyHeight + headerHeight) && + isBetween(left, leftSideWidth, bodyWidth) + ); +} + export function setOpacity(el: HTMLElement, opacity: number | string) { el.style.opacity = String(opacity); } @@ -12,6 +25,23 @@ export function getContainerElement(el: HTMLElement) { return findParentByClassName(el, 'container')!; } +export function moveLayer( + layerEl: HTMLElement, + initLayerPos: LayerPos, + gridRect: GridRectForDropDownLayerPos +) { + if (initLayerPos) { + const { top, left } = initLayerPos; + const { initBodyScrollTop, initBodyScrollLeft, bodyScrollTop, bodyScrollLeft } = gridRect; + const newTop = top + initBodyScrollTop - bodyScrollTop; + const newLeft = left + initBodyScrollLeft - bodyScrollLeft; + + layerEl.style.display = isHidden(newTop, newLeft, gridRect) ? 'none' : ''; + layerEl.style.top = `${newTop}px`; + layerEl.style.left = `${newLeft}px`; + } +} + export function setLayerPosition( innerEl: HTMLElement, layerEl: HTMLElement, @@ -34,15 +64,18 @@ export function setLayerPosition( const totalHeight = layerHeight + childElHeight; const totalWidth = layerWidth || childElWidth; - layerEl.style.top = `${ + const newLayerTop = (layerTop + totalHeight > innerHeight - SCROLL_BAR_WIDTH ? innerHeight - totalHeight - INDENT - SCROLL_BAR_WIDTH - : layerTop) - containerRect.top - }px`; - - layerEl.style.left = `${ + : layerTop) - containerRect.top; + const newLayerLeft = (left + totalWidth > innerWidth - SCROLL_BAR_HEIGHT ? innerWidth - totalWidth - INDENT - SCROLL_BAR_HEIGHT - : left) - containerRect.left - }px`; + : left) - containerRect.left; + + layerEl.style.top = `${newLayerTop}px`; + layerEl.style.left = `${newLayerLeft}px`; + layerEl.style.display = ''; + + return { top: newLayerTop, left: newLayerLeft }; } diff --git a/packages/toast-ui.grid/src/editor/select.ts b/packages/toast-ui.grid/src/editor/select.ts index 297b0826d..4294bd113 100644 --- a/packages/toast-ui.grid/src/editor/select.ts +++ b/packages/toast-ui.grid/src/editor/select.ts @@ -1,10 +1,16 @@ import SelectBox from '@toast-ui/select-box'; import '@toast-ui/select-box/dist/toastui-select-box.css'; -import { CellEditor, CellEditorProps, PortalEditingKeydown } from '@t/editor'; +import { + CellEditor, + CellEditorProps, + GridRectForDropDownLayerPos, + LayerPos, + PortalEditingKeydown, +} from '@t/editor'; import { CellValue, ListItem } from '@t/store/data'; import { getListItems } from '../helper/editor'; import { cls } from '../helper/dom'; -import { setLayerPosition, getContainerElement, setOpacity } from './dom'; +import { setLayerPosition, getContainerElement, setOpacity, moveLayer } from './dom'; import { getKeyStrokeString } from '../helper/keyboard'; import { includes, isNil } from '../helper/common'; @@ -21,6 +27,8 @@ export class SelectEditor implements CellEditor { private portalEditingKeydown: PortalEditingKeydown; + private initLayerPos: LayerPos | null = null; + public constructor(props: CellEditorProps) { const { width, formattedValue, portalEditingKeydown } = props; const el = document.createElement('div'); @@ -93,6 +101,12 @@ export class SelectEditor implements CellEditor { this.selectBoxEl.input.focus(); } + public moveDropdownLayer(gridRect: GridRectForDropDownLayerPos) { + if (this.initLayerPos) { + moveLayer(this.layer, this.initLayerPos, gridRect); + } + } + public getElement() { return this.el; } @@ -106,7 +120,7 @@ export class SelectEditor implements CellEditor { // To prevent wrong stacked z-index context, layer append to grid container getContainerElement(this.el).appendChild(this.layer); // @ts-ignore - setLayerPosition(this.el, this.layer, this.selectBoxEl.dropdown.el); + this.initLayerPos = setLayerPosition(this.el, this.layer, this.selectBoxEl.dropdown.el); this.focusSelectBox(); this.isMounted = true; // To show the layer which has appropriate position @@ -117,5 +131,6 @@ export class SelectEditor implements CellEditor { this.selectBoxEl.destroy(); this.layer.removeEventListener('keydown', this.onKeydown); getContainerElement(this.el).removeChild(this.layer); + this.initLayerPos = null; } } diff --git a/packages/toast-ui.grid/src/helper/common.ts b/packages/toast-ui.grid/src/helper/common.ts index 49897a7cb..6a9f42aa0 100644 --- a/packages/toast-ui.grid/src/helper/common.ts +++ b/packages/toast-ui.grid/src/helper/common.ts @@ -433,3 +433,7 @@ export function convertDataToText(data: string[][], delimiter: string) { export function silentSplice(arr: T[], start: number, deleteCount: number, ...items: T[]): T[] { return Array.prototype.splice.call(arr, start, deleteCount, ...items); } + +export function isBetween(value: number, start: number, end: number) { + return value >= start && value <= end; +} diff --git a/packages/toast-ui.grid/src/view/editingLayer.tsx b/packages/toast-ui.grid/src/view/editingLayer.tsx index b23bf0fab..ab0012fef 100644 --- a/packages/toast-ui.grid/src/view/editingLayer.tsx +++ b/packages/toast-ui.grid/src/view/editingLayer.tsx @@ -12,6 +12,11 @@ import { findProp, isNull } from '../helper/common'; import { getInstance } from '../instance'; import Grid from '../grid'; +interface InitBodyScroll { + initBodyScrollTop: number; + initBodyScrollLeft: number; +} + interface StoreProps { active: boolean; grid: Grid; @@ -23,6 +28,12 @@ interface StoreProps { allColumnMap: Dictionary; editingAddress: EditingAddress; cellPosRect?: Rect | null; + bodyScrollTop: number; + bodyScrollLeft: number; + bodyHeight: number; + bodyWidth: number; + headerHeight: number; + leftSideWidth: number; } interface OwnProps { @@ -36,6 +47,8 @@ export class EditingLayerComp extends Component { private contentEl?: HTMLElement; + private initBodyScroll: InitBodyScroll = { initBodyScrollTop: 0, initBodyScrollLeft: 0 }; + private moveTabFocus(ev: KeyboardEvent, command: TabCommandType) { const { dispatch } = this.props; @@ -100,7 +113,15 @@ export class EditingLayerComp extends Component { } private createEditor() { - const { allColumnMap, filteredViewData, editingAddress, grid, cellPosRect } = this.props; + const { + allColumnMap, + filteredViewData, + editingAddress, + grid, + cellPosRect, + bodyScrollTop, + bodyScrollLeft, + } = this.props; const { rowKey, columnName } = editingAddress!; const { right, left } = cellPosRect!; @@ -129,19 +150,43 @@ export class EditingLayerComp extends Component { // To access the actual mounted DOM elements setTimeout(() => { cellEditor.mounted!(); + this.initBodyScroll = { + initBodyScrollTop: bodyScrollTop, + initBodyScrollLeft: bodyScrollLeft, + }; }); } } } public componentDidUpdate(prevProps: Props) { - if ( - !prevProps.active && - this.props.active && - this.props.editingAddress?.columnName === this.props.focusedColumnName - ) { + const { + active, + editingAddress, + focusedColumnName, + bodyHeight, + bodyWidth, + bodyScrollTop, + bodyScrollLeft, + headerHeight, + leftSideWidth, + } = this.props; + + if (!prevProps.active && active && editingAddress?.columnName === focusedColumnName) { this.createEditor(); } + + if (this.editor?.moveDropdownLayer) { + this.editor.moveDropdownLayer({ + bodyHeight, + bodyWidth, + bodyScrollTop, + bodyScrollLeft, + headerHeight, + leftSideWidth, + ...this.initBodyScroll, + }); + } } public componentWillReceiveProps(nextProps: Props) { @@ -192,7 +237,7 @@ export class EditingLayerComp extends Component { } export const EditingLayer = connect((store, { side }) => { - const { data, column, id, focus, dimension } = store; + const { data, column, id, focus, dimension, viewport, columnCoords } = store; const { editingAddress, side: focusSide, @@ -201,6 +246,15 @@ export const EditingLayer = connect((store, { side }) => { forcedDestroyEditing, cellPosRect, } = focus; + const { scrollTop, scrollLeft } = viewport; + const { + cellBorderWidth, + bodyHeight, + width, + scrollXHeight, + scrollYWidth, + headerHeight, + } = dimension; return { grid: getInstance(id), @@ -209,9 +263,15 @@ export const EditingLayer = connect((store, { side }) => { focusedColumnName, forcedDestroyEditing, cellPosRect, - cellBorderWidth: dimension.cellBorderWidth, + cellBorderWidth, editingAddress, filteredViewData: data.filteredViewData, allColumnMap: column.allColumnMap, + bodyScrollTop: scrollTop, + bodyScrollLeft: scrollLeft, + bodyHeight: bodyHeight - scrollXHeight, + bodyWidth: width - scrollYWidth, + headerHeight, + leftSideWidth: side === 'L' ? 0 : columnCoords.areaWidth.L, }; }, true)(EditingLayerComp); diff --git a/packages/toast-ui.grid/types/editor/index.d.ts b/packages/toast-ui.grid/types/editor/index.d.ts index 847a67a2b..c5ba3f797 100644 --- a/packages/toast-ui.grid/types/editor/index.d.ts +++ b/packages/toast-ui.grid/types/editor/index.d.ts @@ -22,6 +22,7 @@ export interface CellEditorProps { export interface CellEditor { getElement(): HTMLElement | undefined; getValue(): CellValue; + moveDropdownLayer?(gridRect: GridRectForDropDownLayerPos): void; mounted?(): void; beforeDestroy?(): void; el?: HTMLElement; @@ -35,3 +36,19 @@ export interface ListItemOptions { export interface CellEditorClass { new (props: CellEditorProps): CellEditor; } + +export interface GridRectForDropDownLayerPos { + initBodyScrollTop: number; + initBodyScrollLeft: number; + bodyHeight: number; + bodyWidth: number; + bodyScrollTop: number; + bodyScrollLeft: number; + headerHeight: number; + leftSideWidth: number; +} + +export interface LayerPos { + top: number; + left: number; +} From 4a592b9f89391c13219455fb3edd5cda5ccdd394 Mon Sep 17 00:00:00 2001 From: jajugoguma Date: Mon, 3 Jan 2022 15:27:00 +0900 Subject: [PATCH 2/8] test: add tests for editors with drop-down layer to follow scrolling --- .../cypress/integration/editor.spec.ts | 64 +++++++++++++++++++ .../toast-ui.grid/src/view/editingLayer.tsx | 24 ++++--- 2 files changed, 75 insertions(+), 13 deletions(-) diff --git a/packages/toast-ui.grid/cypress/integration/editor.spec.ts b/packages/toast-ui.grid/cypress/integration/editor.spec.ts index a369da2b2..15f994cd6 100644 --- a/packages/toast-ui.grid/cypress/integration/editor.spec.ts +++ b/packages/toast-ui.grid/cypress/integration/editor.spec.ts @@ -3,6 +3,7 @@ import { CellValue } from '@t/store/data'; import { createCustomLayerEditor, CustomTextEditor } from '../helper/customEditor'; import { GridEventProps } from '@t/event'; import { clickFilterBtn } from '../helper/util'; +import { cls } from '@/helper/dom'; before(() => { cy.visit('/dist'); @@ -585,3 +586,66 @@ describe('original cell value should be kept', () => { cy.get('input').should('have.value', 'undefined'); }); }); + +describe('Scroll with editor that has drop-down layer', () => { + function scrollTo(position: Cypress.PositionType) { + cy.get(`.${cls('rside-area')} .${cls('body-area')}`) + .wait(0) + .scrollTo(position); + } + + beforeEach(() => { + const data = [ + { id: '1', name: 'Kim', age: '10', school: '1', grade: 'First' }, + { id: '2', name: 'Lee', age: '11', school: '1', grade: 'Second' }, + { id: '3', name: 'Park', age: '12', school: '1', grade: 'First' }, + { id: '4', name: 'Choi', age: '13', school: '1', grade: 'Second' }, + { id: '5', name: 'Cho', age: '14', school: '1', grade: 'Third' }, + { id: '6', name: 'Han', age: '14', school: '1', grade: 'Second' }, + { id: '7', name: 'Lim', age: '14', school: '1', grade: 'First' }, + { id: '8', name: 'Ahn', age: '14', school: '1', grade: 'Second' }, + { id: '9', name: 'Ryu', age: '14', school: '1', grade: 'First' }, + ]; + const columns = [ + { name: 'id', minWidth: 100 }, + { name: 'name', minWidth: 100 }, + { + name: 'school', + formatter: 'listItemText', + editor: { + type: 'select', + options: { + listItems: [ + { text: 'Primary', value: '1' }, + { text: 'Middle', value: '2' }, + { text: 'High', value: '3' }, + ], + }, + }, + minWidth: 100, + }, + { name: 'age', minWidth: 100 }, + { + name: 'grade', + minWidth: 100, + }, + ]; + + cy.createGrid({ data, columns, width: 200, bodyHeight: 150 }); + }); + + [ + { direcion: 'down', position: 'bottom' }, + { direcion: 'up', position: 'top' }, + { direcion: 'right', position: 'right' }, + { direcion: 'left', position: 'left' }, + ].forEach(({ direcion, position }) => { + it(`should hides the drop-down layer when scrolling ${direcion}`, () => { + cy.gridInstance().invoke('startEditing', 4, 'school'); + + scrollTo(position as Cypress.PositionType); + + cy.getByCls('editor-select-box-layer').should('be.not.visible'); + }); + }); +}); diff --git a/packages/toast-ui.grid/src/view/editingLayer.tsx b/packages/toast-ui.grid/src/view/editingLayer.tsx index ab0012fef..5087910d6 100644 --- a/packages/toast-ui.grid/src/view/editingLayer.tsx +++ b/packages/toast-ui.grid/src/view/editingLayer.tsx @@ -112,16 +112,17 @@ export class EditingLayerComp extends Component { } } + private setInitScrollPos() { + const { bodyScrollTop, bodyScrollLeft } = this.props; + + this.initBodyScroll = { + initBodyScrollTop: bodyScrollTop, + initBodyScrollLeft: bodyScrollLeft, + }; + } + private createEditor() { - const { - allColumnMap, - filteredViewData, - editingAddress, - grid, - cellPosRect, - bodyScrollTop, - bodyScrollLeft, - } = this.props; + const { allColumnMap, filteredViewData, editingAddress, grid, cellPosRect } = this.props; const { rowKey, columnName } = editingAddress!; const { right, left } = cellPosRect!; @@ -150,10 +151,7 @@ export class EditingLayerComp extends Component { // To access the actual mounted DOM elements setTimeout(() => { cellEditor.mounted!(); - this.initBodyScroll = { - initBodyScrollTop: bodyScrollTop, - initBodyScrollLeft: bodyScrollLeft, - }; + this.setInitScrollPos(); }); } } From 9526681806e5c3ae3994823399adc52899060240 Mon Sep 17 00:00:00 2001 From: jajugoguma Date: Tue, 4 Jan 2022 19:07:34 +0900 Subject: [PATCH 3/8] chore: apply code reviews --- .../cypress/integration/editor.spec.ts | 9 ++--- packages/toast-ui.grid/src/editor/checkbox.ts | 9 +++-- .../toast-ui.grid/src/editor/datePicker.ts | 14 ++++---- packages/toast-ui.grid/src/editor/dom.ts | 33 ++++++++----------- packages/toast-ui.grid/src/editor/select.ts | 10 ++++-- packages/toast-ui.grid/src/helper/common.ts | 7 +++- 6 files changed, 44 insertions(+), 38 deletions(-) diff --git a/packages/toast-ui.grid/cypress/integration/editor.spec.ts b/packages/toast-ui.grid/cypress/integration/editor.spec.ts index 15f994cd6..30ffdc6a7 100644 --- a/packages/toast-ui.grid/cypress/integration/editor.spec.ts +++ b/packages/toast-ui.grid/cypress/integration/editor.spec.ts @@ -589,9 +589,7 @@ describe('original cell value should be kept', () => { describe('Scroll with editor that has drop-down layer', () => { function scrollTo(position: Cypress.PositionType) { - cy.get(`.${cls('rside-area')} .${cls('body-area')}`) - .wait(0) - .scrollTo(position); + cy.getByCls('rside-area', 'body-area').wait(0).scrollTo(position); } beforeEach(() => { @@ -625,10 +623,7 @@ describe('Scroll with editor that has drop-down layer', () => { minWidth: 100, }, { name: 'age', minWidth: 100 }, - { - name: 'grade', - minWidth: 100, - }, + { name: 'grade', minWidth: 100 }, ]; cy.createGrid({ data, columns, width: 200, bodyHeight: 150 }); diff --git a/packages/toast-ui.grid/src/editor/checkbox.ts b/packages/toast-ui.grid/src/editor/checkbox.ts index 415fcc594..f91a5005f 100644 --- a/packages/toast-ui.grid/src/editor/checkbox.ts +++ b/packages/toast-ui.grid/src/editor/checkbox.ts @@ -9,7 +9,7 @@ import { CellValue, ListItem } from '@t/store/data'; import { getListItems } from '../helper/editor'; import { cls, hasClass } from '../helper/dom'; import { getKeyStrokeString, isArrowKey } from '../helper/keyboard'; -import { findIndex, isNil } from '../helper/common'; +import { findIndex, isNil, pixelToNumber } from '../helper/common'; import { getContainerElement, setLayerPosition, setOpacity, moveLayer } from './dom'; const LAYER_CLASSNAME = cls('editor-checkbox-list-layer'); @@ -203,7 +203,12 @@ export class CheckboxEditor implements CellEditor { // To prevent wrong stacked z-index context, layer append to grid container getContainerElement(this.el).appendChild(this.layer); // @ts-ignore - this.initLayerPos = setLayerPosition(this.el, this.layer); + setLayerPosition(this.el, this.layer); + + this.initLayerPos = { + top: pixelToNumber(this.layer.style.top), + left: pixelToNumber(this.layer.style.left), + }; const checkedInput = this.getCheckedInput(); if (checkedInput) { diff --git a/packages/toast-ui.grid/src/editor/datePicker.ts b/packages/toast-ui.grid/src/editor/datePicker.ts index fefc96b16..4a15610b0 100644 --- a/packages/toast-ui.grid/src/editor/datePicker.ts +++ b/packages/toast-ui.grid/src/editor/datePicker.ts @@ -2,7 +2,7 @@ import TuiDatePicker from 'tui-date-picker'; import { Dictionary } from '@t/options'; import { CellEditor, CellEditorProps, GridRectForDropDownLayerPos, LayerPos } from '@t/editor'; import { cls } from '../helper/dom'; -import { deepMergedCopy, isNumber, isString, isNil } from '../helper/common'; +import { deepMergedCopy, isNumber, isString, isNil, pixelToNumber } from '../helper/common'; import { setLayerPosition, getContainerElement, setOpacity, moveLayer } from './dom'; export class DatePickerEditor implements CellEditor { @@ -126,12 +126,12 @@ export class DatePickerEditor implements CellEditor { this.datePickerEl.open(); // `this.layer.firstElementChild` is real datePicker layer(it is need to get total height) - this.initLayerPos = setLayerPosition( - this.el, - this.layer, - this.layer.firstElementChild as HTMLElement, - true - ); + setLayerPosition(this.el, this.layer, this.layer.firstElementChild as HTMLElement, true); + + this.initLayerPos = { + top: pixelToNumber(this.layer.style.top), + left: pixelToNumber(this.layer.style.left), + }; // To show the layer which has appropriate position setOpacity(this.layer, 1); diff --git a/packages/toast-ui.grid/src/editor/dom.ts b/packages/toast-ui.grid/src/editor/dom.ts index 2f1069142..08c98ddfc 100644 --- a/packages/toast-ui.grid/src/editor/dom.ts +++ b/packages/toast-ui.grid/src/editor/dom.ts @@ -30,16 +30,14 @@ export function moveLayer( initLayerPos: LayerPos, gridRect: GridRectForDropDownLayerPos ) { - if (initLayerPos) { - const { top, left } = initLayerPos; - const { initBodyScrollTop, initBodyScrollLeft, bodyScrollTop, bodyScrollLeft } = gridRect; - const newTop = top + initBodyScrollTop - bodyScrollTop; - const newLeft = left + initBodyScrollLeft - bodyScrollLeft; + const { top, left } = initLayerPos; + const { initBodyScrollTop, initBodyScrollLeft, bodyScrollTop, bodyScrollLeft } = gridRect; + const newTop = top + initBodyScrollTop - bodyScrollTop; + const newLeft = left + initBodyScrollLeft - bodyScrollLeft; - layerEl.style.display = isHidden(newTop, newLeft, gridRect) ? 'none' : ''; - layerEl.style.top = `${newTop}px`; - layerEl.style.left = `${newLeft}px`; - } + layerEl.style.display = isHidden(newTop, newLeft, gridRect) ? 'none' : ''; + layerEl.style.top = `${newTop}px`; + layerEl.style.left = `${newLeft}px`; } export function setLayerPosition( @@ -64,18 +62,15 @@ export function setLayerPosition( const totalHeight = layerHeight + childElHeight; const totalWidth = layerWidth || childElWidth; - const newLayerTop = + layerEl.style.top = `${ (layerTop + totalHeight > innerHeight - SCROLL_BAR_WIDTH ? innerHeight - totalHeight - INDENT - SCROLL_BAR_WIDTH - : layerTop) - containerRect.top; - const newLayerLeft = + : layerTop) - containerRect.top + }px`; + + layerEl.style.left = `${ (left + totalWidth > innerWidth - SCROLL_BAR_HEIGHT ? innerWidth - totalWidth - INDENT - SCROLL_BAR_HEIGHT - : left) - containerRect.left; - - layerEl.style.top = `${newLayerTop}px`; - layerEl.style.left = `${newLayerLeft}px`; - layerEl.style.display = ''; - - return { top: newLayerTop, left: newLayerLeft }; + : left) - containerRect.left + }px`; } diff --git a/packages/toast-ui.grid/src/editor/select.ts b/packages/toast-ui.grid/src/editor/select.ts index 4294bd113..46c9d1207 100644 --- a/packages/toast-ui.grid/src/editor/select.ts +++ b/packages/toast-ui.grid/src/editor/select.ts @@ -12,7 +12,7 @@ import { getListItems } from '../helper/editor'; import { cls } from '../helper/dom'; import { setLayerPosition, getContainerElement, setOpacity, moveLayer } from './dom'; import { getKeyStrokeString } from '../helper/keyboard'; -import { includes, isNil } from '../helper/common'; +import { includes, isNil, pixelToNumber } from '../helper/common'; export class SelectEditor implements CellEditor { public el: HTMLDivElement; @@ -120,7 +120,13 @@ export class SelectEditor implements CellEditor { // To prevent wrong stacked z-index context, layer append to grid container getContainerElement(this.el).appendChild(this.layer); // @ts-ignore - this.initLayerPos = setLayerPosition(this.el, this.layer, this.selectBoxEl.dropdown.el); + setLayerPosition(this.el, this.layer, this.selectBoxEl.dropdown.el); + + this.initLayerPos = { + top: pixelToNumber(this.layer.style.top), + left: pixelToNumber(this.layer.style.left), + }; + this.focusSelectBox(); this.isMounted = true; // To show the layer which has appropriate position diff --git a/packages/toast-ui.grid/src/helper/common.ts b/packages/toast-ui.grid/src/helper/common.ts index 6a9f42aa0..81dbc5c2b 100644 --- a/packages/toast-ui.grid/src/helper/common.ts +++ b/packages/toast-ui.grid/src/helper/common.ts @@ -435,5 +435,10 @@ export function silentSplice(arr: T[], start: number, deleteCount: number, .. } export function isBetween(value: number, start: number, end: number) { - return value >= start && value <= end; + return start <= value && value <= end; +} + +export function pixelToNumber(pixelString: string) { + const regExp = new RegExp(/[0-9]+px/); + return regExp.test(pixelString) ? parseInt(pixelString.replace('px', '')) : 0; } From f609a3fb16226ea98f63f3aac097140bc0859b02 Mon Sep 17 00:00:00 2001 From: jajugoguma Date: Tue, 4 Jan 2022 20:43:51 +0900 Subject: [PATCH 4/8] chore: fix lint error --- packages/toast-ui.grid/cypress/integration/editor.spec.ts | 1 - packages/toast-ui.grid/src/helper/common.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/toast-ui.grid/cypress/integration/editor.spec.ts b/packages/toast-ui.grid/cypress/integration/editor.spec.ts index 30ffdc6a7..64ff01a05 100644 --- a/packages/toast-ui.grid/cypress/integration/editor.spec.ts +++ b/packages/toast-ui.grid/cypress/integration/editor.spec.ts @@ -3,7 +3,6 @@ import { CellValue } from '@t/store/data'; import { createCustomLayerEditor, CustomTextEditor } from '../helper/customEditor'; import { GridEventProps } from '@t/event'; import { clickFilterBtn } from '../helper/util'; -import { cls } from '@/helper/dom'; before(() => { cy.visit('/dist'); diff --git a/packages/toast-ui.grid/src/helper/common.ts b/packages/toast-ui.grid/src/helper/common.ts index 81dbc5c2b..33d6c4c95 100644 --- a/packages/toast-ui.grid/src/helper/common.ts +++ b/packages/toast-ui.grid/src/helper/common.ts @@ -440,5 +440,5 @@ export function isBetween(value: number, start: number, end: number) { export function pixelToNumber(pixelString: string) { const regExp = new RegExp(/[0-9]+px/); - return regExp.test(pixelString) ? parseInt(pixelString.replace('px', '')) : 0; + return regExp.test(pixelString) ? parseInt(pixelString.replace('px', ''), 10) : 0; } From 860475ded189ccab5be58c0e1e2ba50083b58a8f Mon Sep 17 00:00:00 2001 From: jajugoguma Date: Thu, 6 Jan 2022 00:55:14 +0900 Subject: [PATCH 5/8] test: add tests for remove drop-down layer of editor --- .../cypress/integration/editor.spec.ts | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/toast-ui.grid/cypress/integration/editor.spec.ts b/packages/toast-ui.grid/cypress/integration/editor.spec.ts index 64ff01a05..798a2a69d 100644 --- a/packages/toast-ui.grid/cypress/integration/editor.spec.ts +++ b/packages/toast-ui.grid/cypress/integration/editor.spec.ts @@ -345,8 +345,8 @@ it('should do synchronous rendering of the editing cell', () => { }); describe('select, checkbox, radio editor', () => { + const dataForGridWithType = [{ name: '1' }, { name: '2' }, { name: '3' }, { name: '4' }]; function createGridWithType(type: string) { - const data = [{ name: '1' }, { name: '2' }, { name: '3' }, { name: '4' }]; const columns = [ { name: 'name', @@ -364,7 +364,7 @@ describe('select, checkbox, radio editor', () => { }, ]; - cy.createGrid({ data, columns }); + cy.createGrid({ data: dataForGridWithType, columns }); } context('UI', () => { @@ -452,6 +452,26 @@ describe('select, checkbox, radio editor', () => { assertOptionHighlighted(3); }); }); + + describe('remove drop-down layer', () => { + ['radio', 'select', 'checkbox'].forEach((type) => { + it(`should remove drop-down layer when calling cancelEditing() API (${type})`, () => { + createGridWithType(type); + + cy.gridInstance().invoke('cancelEditing'); + + cy.getByCls('editor-select-box-layer').should('be.not.visible'); + }); + + it(`should remove drop-down layer when calling resetData() API (${type})`, () => { + createGridWithType(type); + + cy.gridInstance().invoke('resetData', dataForGridWithType); + + cy.getByCls('editor-select-box-layer').should('be.not.visible'); + }); + }); + }); }); // @TODO: should rewrite test case after modifying casting editing value From e8da849be41fc15af6615f77d4c1c4f31fee967b Mon Sep 17 00:00:00 2001 From: jajugoguma Date: Thu, 6 Jan 2022 00:55:36 +0900 Subject: [PATCH 6/8] fix: remove drop-down layer of editor --- packages/toast-ui.grid/src/editor/checkbox.ts | 4 ++++ packages/toast-ui.grid/src/editor/datePicker.ts | 4 ++++ packages/toast-ui.grid/src/editor/select.ts | 5 +++-- packages/toast-ui.grid/src/view/editingLayer.tsx | 4 ++++ packages/toast-ui.grid/types/editor/index.d.ts | 1 + 5 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/toast-ui.grid/src/editor/checkbox.ts b/packages/toast-ui.grid/src/editor/checkbox.ts index f91a5005f..ae981aec3 100644 --- a/packages/toast-ui.grid/src/editor/checkbox.ts +++ b/packages/toast-ui.grid/src/editor/checkbox.ts @@ -23,6 +23,8 @@ const CHECKED_CHECKBOX_LABEL_CLASSNAME = cls('editor-label-icon-checkbox-checked export class CheckboxEditor implements CellEditor { public el: HTMLElement; + public isMounted = false; + private layer: HTMLUListElement; private readonly inputType: 'checkbox' | 'radio'; @@ -214,6 +216,7 @@ export class CheckboxEditor implements CellEditor { if (checkedInput) { this.highlightItem(`checkbox-${checkedInput.value}`); } + this.isMounted = true; // To show the layer which has appropriate position setOpacity(this.layer, 1); } @@ -224,5 +227,6 @@ export class CheckboxEditor implements CellEditor { this.layer.removeEventListener('keydown', this.onKeydown); getContainerElement(this.el).removeChild(this.layer); this.initLayerPos = null; + this.isMounted = false; } } diff --git a/packages/toast-ui.grid/src/editor/datePicker.ts b/packages/toast-ui.grid/src/editor/datePicker.ts index 4a15610b0..c4f43e4df 100644 --- a/packages/toast-ui.grid/src/editor/datePicker.ts +++ b/packages/toast-ui.grid/src/editor/datePicker.ts @@ -8,6 +8,8 @@ import { setLayerPosition, getContainerElement, setOpacity, moveLayer } from './ export class DatePickerEditor implements CellEditor { public el: HTMLDivElement; + public isMounted = false; + private layer: HTMLDivElement; private inputEl: HTMLInputElement; @@ -132,6 +134,7 @@ export class DatePickerEditor implements CellEditor { top: pixelToNumber(this.layer.style.top), left: pixelToNumber(this.layer.style.left), }; + this.isMounted = true; // To show the layer which has appropriate position setOpacity(this.layer, 1); @@ -144,5 +147,6 @@ export class DatePickerEditor implements CellEditor { this.datePickerEl.destroy(); getContainerElement(this.el).removeChild(this.layer); this.initLayerPos = null; + this.isMounted = false; } } diff --git a/packages/toast-ui.grid/src/editor/select.ts b/packages/toast-ui.grid/src/editor/select.ts index 46c9d1207..829ffb5e3 100644 --- a/packages/toast-ui.grid/src/editor/select.ts +++ b/packages/toast-ui.grid/src/editor/select.ts @@ -17,14 +17,14 @@ import { includes, isNil, pixelToNumber } from '../helper/common'; export class SelectEditor implements CellEditor { public el: HTMLDivElement; + public isMounted = false; + private layer: HTMLDivElement; private selectBoxEl!: SelectBox; private selectFinish = false; - private isMounted = false; - private portalEditingKeydown: PortalEditingKeydown; private initLayerPos: LayerPos | null = null; @@ -138,5 +138,6 @@ export class SelectEditor implements CellEditor { this.layer.removeEventListener('keydown', this.onKeydown); getContainerElement(this.el).removeChild(this.layer); this.initLayerPos = null; + this.isMounted = false; } } diff --git a/packages/toast-ui.grid/src/view/editingLayer.tsx b/packages/toast-ui.grid/src/view/editingLayer.tsx index 5087910d6..97b38a84c 100644 --- a/packages/toast-ui.grid/src/view/editingLayer.tsx +++ b/packages/toast-ui.grid/src/view/editingLayer.tsx @@ -206,6 +206,10 @@ export class EditingLayerComp extends Component { public render({ active, cellPosRect, cellBorderWidth }: Props) { if (!active) { + const { isMounted, beforeDestroy } = this.editor || {}; + if (isMounted && beforeDestroy) { + beforeDestroy(); + } return null; } diff --git a/packages/toast-ui.grid/types/editor/index.d.ts b/packages/toast-ui.grid/types/editor/index.d.ts index c5ba3f797..6aeecf835 100644 --- a/packages/toast-ui.grid/types/editor/index.d.ts +++ b/packages/toast-ui.grid/types/editor/index.d.ts @@ -26,6 +26,7 @@ export interface CellEditor { mounted?(): void; beforeDestroy?(): void; el?: HTMLElement; + isMounted?: boolean; } export interface ListItemOptions { From d67c4cc8c2d725621ac3310462fc65eafd2af61c Mon Sep 17 00:00:00 2001 From: jajugoguma Date: Thu, 6 Jan 2022 12:07:28 +0900 Subject: [PATCH 7/8] chore: remove destructuring --- packages/toast-ui.grid/src/view/editingLayer.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/toast-ui.grid/src/view/editingLayer.tsx b/packages/toast-ui.grid/src/view/editingLayer.tsx index 97b38a84c..e31814ade 100644 --- a/packages/toast-ui.grid/src/view/editingLayer.tsx +++ b/packages/toast-ui.grid/src/view/editingLayer.tsx @@ -206,9 +206,8 @@ export class EditingLayerComp extends Component { public render({ active, cellPosRect, cellBorderWidth }: Props) { if (!active) { - const { isMounted, beforeDestroy } = this.editor || {}; - if (isMounted && beforeDestroy) { - beforeDestroy(); + if (this.editor && this.editor.isMounted && this.editor.beforeDestroy) { + this.editor.beforeDestroy(); } return null; } From 44af5c7c12dfe926f4c7dcaab9ed76b4c09b5cd1 Mon Sep 17 00:00:00 2001 From: jajugoguma Date: Fri, 7 Jan 2022 19:29:07 +0900 Subject: [PATCH 8/8] chore: apply code reviews --- packages/toast-ui.grid/src/editor/checkbox.ts | 4 ---- packages/toast-ui.grid/src/editor/datePicker.ts | 4 ---- packages/toast-ui.grid/src/editor/select.ts | 5 ++--- packages/toast-ui.grid/src/view/editingLayer.tsx | 15 +++++---------- packages/toast-ui.grid/types/editor/index.d.ts | 1 - 5 files changed, 7 insertions(+), 22 deletions(-) diff --git a/packages/toast-ui.grid/src/editor/checkbox.ts b/packages/toast-ui.grid/src/editor/checkbox.ts index ae981aec3..f91a5005f 100644 --- a/packages/toast-ui.grid/src/editor/checkbox.ts +++ b/packages/toast-ui.grid/src/editor/checkbox.ts @@ -23,8 +23,6 @@ const CHECKED_CHECKBOX_LABEL_CLASSNAME = cls('editor-label-icon-checkbox-checked export class CheckboxEditor implements CellEditor { public el: HTMLElement; - public isMounted = false; - private layer: HTMLUListElement; private readonly inputType: 'checkbox' | 'radio'; @@ -216,7 +214,6 @@ export class CheckboxEditor implements CellEditor { if (checkedInput) { this.highlightItem(`checkbox-${checkedInput.value}`); } - this.isMounted = true; // To show the layer which has appropriate position setOpacity(this.layer, 1); } @@ -227,6 +224,5 @@ export class CheckboxEditor implements CellEditor { this.layer.removeEventListener('keydown', this.onKeydown); getContainerElement(this.el).removeChild(this.layer); this.initLayerPos = null; - this.isMounted = false; } } diff --git a/packages/toast-ui.grid/src/editor/datePicker.ts b/packages/toast-ui.grid/src/editor/datePicker.ts index c4f43e4df..4a15610b0 100644 --- a/packages/toast-ui.grid/src/editor/datePicker.ts +++ b/packages/toast-ui.grid/src/editor/datePicker.ts @@ -8,8 +8,6 @@ import { setLayerPosition, getContainerElement, setOpacity, moveLayer } from './ export class DatePickerEditor implements CellEditor { public el: HTMLDivElement; - public isMounted = false; - private layer: HTMLDivElement; private inputEl: HTMLInputElement; @@ -134,7 +132,6 @@ export class DatePickerEditor implements CellEditor { top: pixelToNumber(this.layer.style.top), left: pixelToNumber(this.layer.style.left), }; - this.isMounted = true; // To show the layer which has appropriate position setOpacity(this.layer, 1); @@ -147,6 +144,5 @@ export class DatePickerEditor implements CellEditor { this.datePickerEl.destroy(); getContainerElement(this.el).removeChild(this.layer); this.initLayerPos = null; - this.isMounted = false; } } diff --git a/packages/toast-ui.grid/src/editor/select.ts b/packages/toast-ui.grid/src/editor/select.ts index 829ffb5e3..46c9d1207 100644 --- a/packages/toast-ui.grid/src/editor/select.ts +++ b/packages/toast-ui.grid/src/editor/select.ts @@ -17,14 +17,14 @@ import { includes, isNil, pixelToNumber } from '../helper/common'; export class SelectEditor implements CellEditor { public el: HTMLDivElement; - public isMounted = false; - private layer: HTMLDivElement; private selectBoxEl!: SelectBox; private selectFinish = false; + private isMounted = false; + private portalEditingKeydown: PortalEditingKeydown; private initLayerPos: LayerPos | null = null; @@ -138,6 +138,5 @@ export class SelectEditor implements CellEditor { this.layer.removeEventListener('keydown', this.onKeydown); getContainerElement(this.el).removeChild(this.layer); this.initLayerPos = null; - this.isMounted = false; } } diff --git a/packages/toast-ui.grid/src/view/editingLayer.tsx b/packages/toast-ui.grid/src/view/editingLayer.tsx index e31814ade..e8f386b41 100644 --- a/packages/toast-ui.grid/src/view/editingLayer.tsx +++ b/packages/toast-ui.grid/src/view/editingLayer.tsx @@ -88,9 +88,6 @@ export class EditingLayerComp extends Component { private cancelEditing() { const { rowKey, columnName, value } = this.getEditingCellInfo(); - // eslint-disable-next-line no-unused-expressions - this.editor?.beforeDestroy?.(); - this.props.dispatch('finishEditing', rowKey, columnName, value, { save: false, triggeredByKey: true, @@ -104,10 +101,6 @@ export class EditingLayerComp extends Component { const { rowKey, columnName, value } = this.getEditingCellInfo(); dispatch('setValue', rowKey, columnName, value); - - // eslint-disable-next-line no-unused-expressions - this.editor?.beforeDestroy?.(); - dispatch('finishEditing', rowKey, columnName, value, { save: true, triggeredByKey }); } } @@ -195,6 +188,11 @@ export class EditingLayerComp extends Component { } = this.props; const { focusedColumnName, focusedRowKey, active, forcedDestroyEditing } = nextProps; + if (prevActive && !active) { + // eslint-disable-next-line no-unused-expressions + this.editor?.beforeDestroy?.(); + } + if ( (prevActive && !active && forcedDestroyEditing) || (prevActive && @@ -206,9 +204,6 @@ export class EditingLayerComp extends Component { public render({ active, cellPosRect, cellBorderWidth }: Props) { if (!active) { - if (this.editor && this.editor.isMounted && this.editor.beforeDestroy) { - this.editor.beforeDestroy(); - } return null; } diff --git a/packages/toast-ui.grid/types/editor/index.d.ts b/packages/toast-ui.grid/types/editor/index.d.ts index 6aeecf835..c5ba3f797 100644 --- a/packages/toast-ui.grid/types/editor/index.d.ts +++ b/packages/toast-ui.grid/types/editor/index.d.ts @@ -26,7 +26,6 @@ export interface CellEditor { mounted?(): void; beforeDestroy?(): void; el?: HTMLElement; - isMounted?: boolean; } export interface ListItemOptions {