Skip to content

Commit 866ee53

Browse files
authored
fix: only open drop when exists after open delay (#277)
* fix: only open drop when exists after open delay
1 parent 2834a2e commit 866ee53

File tree

2 files changed

+135
-122
lines changed

2 files changed

+135
-122
lines changed

packages/multiple-select-vanilla/src/MultipleSelectInstance.ts

Lines changed: 130 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export class MultipleSelectInstance {
3636
protected updateData: any[] = [];
3737
protected data?: Array<OptionRowData | OptGroupRowData> = [];
3838
protected dataTotal?: any;
39-
protected dropElm!: HTMLDivElement;
39+
protected dropElm?: HTMLDivElement;
4040
protected okButtonElm?: HTMLButtonElement;
4141
protected filterParentElm?: HTMLDivElement | null;
4242
protected lastFocusedItemKey = '';
@@ -106,6 +106,7 @@ export class MultipleSelectInstance {
106106

107107
this.virtualScroll?.destroy();
108108
this.dropElm?.remove();
109+
this.dropElm = undefined;
109110
this.parentElm.parentNode?.removeChild(this.parentElm);
110111

111112
if (this.fromHtml) {
@@ -433,14 +434,14 @@ export class MultipleSelectInstance {
433434

434435
saLabelElm.appendChild(createDomElement('span', { textContent: this.formatSelectAll() }));
435436
this.selectAllParentElm.appendChild(saLabelElm);
436-
this.dropElm.appendChild(this.selectAllParentElm);
437+
this.dropElm?.appendChild(this.selectAllParentElm);
437438
}
438439

439440
this.ulElm = document.createElement('ul');
440441
this.ulElm.role = 'combobox';
441442
this.ulElm.ariaExpanded = 'false';
442443
this.ulElm.ariaMultiSelectable = String(!this.options.single);
443-
this.dropElm.appendChild(this.ulElm);
444+
this.dropElm?.appendChild(this.ulElm);
444445

445446
if (this.options.showOkButton && !this.options.single) {
446447
this.okButtonElm = createDomElement(
@@ -461,8 +462,8 @@ export class MultipleSelectInstance {
461462
}
462463

463464
if (rows.length > Constants.BLOCK_ROWS * Constants.CLUSTER_BLOCKS) {
464-
const dropVisible = this.dropElm.style.display !== 'none';
465-
if (!dropVisible) {
465+
const dropVisible = this.dropElm && this.dropElm?.style.display !== 'none';
466+
if (!dropVisible && this.dropElm) {
466467
this.dropElm.style.left = '-10000';
467468
this.dropElm.style.display = 'block';
468469
this.dropElm.ariaExpanded = 'true';
@@ -511,7 +512,7 @@ export class MultipleSelectInstance {
511512
}
512513
updateDataOffset();
513514

514-
if (!dropVisible) {
515+
if (!dropVisible && this.dropElm) {
515516
this.dropElm.style.left = '0';
516517
this.dropElm.style.display = 'none';
517518
this.dropElm.ariaExpanded = 'false';
@@ -788,13 +789,13 @@ export class MultipleSelectInstance {
788789
]);
789790

790791
this.clearSearchIconElm = this.filterParentElm?.querySelector('.ms-icon-close');
791-
this.searchInputElm = this.dropElm.querySelector<HTMLInputElement>('.ms-search input');
792-
this.selectAllElm = this.dropElm.querySelector<HTMLInputElement>(`input[data-name="${this.selectAllName}"]`);
793-
this.selectGroupElms = this.dropElm.querySelectorAll<HTMLInputElement>(
792+
this.searchInputElm = this.dropElm?.querySelector<HTMLInputElement>('.ms-search input');
793+
this.selectAllElm = this.dropElm?.querySelector<HTMLInputElement>(`input[data-name="${this.selectAllName}"]`);
794+
this.selectGroupElms = this.dropElm?.querySelectorAll<HTMLInputElement>(
794795
`input[data-name="${this.selectGroupName}"],span[data-name="${this.selectGroupName}"]`,
795796
);
796-
this.selectItemElms = this.dropElm.querySelectorAll<HTMLInputElement>(`input[data-name="${this.selectItemName}"]:enabled`);
797-
this.noResultsElm = this.dropElm.querySelector<HTMLDivElement>('.ms-no-results');
797+
this.selectItemElms = this.dropElm?.querySelectorAll<HTMLInputElement>(`input[data-name="${this.selectItemName}"]:enabled`);
798+
this.noResultsElm = this.dropElm?.querySelector<HTMLDivElement>('.ms-no-results');
798799

799800
const toggleOpen = (e: MouseEvent & { target: HTMLElement }) => {
800801
e.preventDefault();
@@ -925,87 +926,91 @@ export class MultipleSelectInstance {
925926
);
926927
}
927928

928-
this._bindEventService.bind(
929-
this.selectGroupElms,
930-
'click',
931-
((e: MouseEvent & { currentTarget: HTMLInputElement }) => {
932-
const selectElm = e.currentTarget;
933-
const checked = selectElm.checked;
934-
const group = findByParam(this.data, '_key', selectElm.dataset.key);
935-
936-
this._checkGroup(group, checked);
937-
this.options.onOptgroupClick(
938-
removeUndefined({
939-
label: group.label,
940-
selected: group.selected,
941-
data: group._data,
942-
children: group.children.map((child: any) => {
943-
if (child) {
944-
return removeUndefined({
945-
text: child.text,
946-
value: child.value,
947-
selected: child.selected,
948-
disabled: child.disabled,
949-
data: child._data,
950-
});
951-
}
929+
if (this.selectGroupElms) {
930+
this._bindEventService.bind(
931+
this.selectGroupElms,
932+
'click',
933+
((e: MouseEvent & { currentTarget: HTMLInputElement }) => {
934+
const selectElm = e.currentTarget;
935+
const checked = selectElm.checked;
936+
const group = findByParam(this.data, '_key', selectElm.dataset.key);
937+
938+
this._checkGroup(group, checked);
939+
this.options.onOptgroupClick(
940+
removeUndefined({
941+
label: group.label,
942+
selected: group.selected,
943+
data: group._data,
944+
children: group.children.map((child: any) => {
945+
if (child) {
946+
return removeUndefined({
947+
text: child.text,
948+
value: child.value,
949+
selected: child.selected,
950+
disabled: child.disabled,
951+
data: child._data,
952+
});
953+
}
954+
}),
952955
}),
953-
}),
954-
);
955-
}) as EventListener,
956-
undefined,
957-
'group-checkbox-list',
958-
);
956+
);
957+
}) as EventListener,
958+
undefined,
959+
'group-checkbox-list',
960+
);
961+
}
959962

960-
this._bindEventService.bind(
961-
this.selectItemElms,
962-
'click',
963-
((e: MouseEvent & { currentTarget: HTMLInputElement }) => {
964-
const selectElm = e.currentTarget;
965-
const checked = selectElm.checked;
966-
const option = findByParam(this.data, '_key', selectElm.dataset.key);
967-
const close = () => {
968-
if (this.options.single && this.options.isOpen && !this.options.keepOpen) {
969-
this.close('selection');
963+
if (this.selectItemElms) {
964+
this._bindEventService.bind(
965+
this.selectItemElms,
966+
'click',
967+
((e: MouseEvent & { currentTarget: HTMLInputElement }) => {
968+
const selectElm = e.currentTarget;
969+
const checked = selectElm.checked;
970+
const option = findByParam(this.data, '_key', selectElm.dataset.key);
971+
const close = () => {
972+
if (this.options.single && this.options.isOpen && !this.options.keepOpen) {
973+
this.close('selection');
974+
}
975+
};
976+
977+
if (this.options.onBeforeClick(option) === false) {
978+
close();
979+
return;
970980
}
971-
};
972981

973-
if (this.options.onBeforeClick(option) === false) {
974-
close();
975-
return;
976-
}
982+
this._check(option, checked);
983+
this.options.onClick(
984+
removeUndefined({
985+
text: option.text,
986+
value: option.value,
987+
selected: option.selected,
988+
data: option._data,
989+
}),
990+
);
977991

978-
this._check(option, checked);
979-
this.options.onClick(
980-
removeUndefined({
981-
text: option.text,
982-
value: option.value,
983-
selected: option.selected,
984-
data: option._data,
985-
}),
986-
);
987-
988-
close();
989-
}) as EventListener,
990-
undefined,
991-
'input-checkbox-list',
992-
);
992+
close();
993+
}) as EventListener,
994+
undefined,
995+
'input-checkbox-list',
996+
);
997+
}
993998

994-
if (this.lastFocusedItemKey) {
999+
if (this.lastFocusedItemKey && this.dropElm) {
9951000
// if we previously had an item focused and the VirtualScroll recreates the list, we need to refocus on last item by its input data-key
9961001
const input = this.dropElm.querySelector<HTMLInputElement>(`li[data-key=${this.lastFocusedItemKey}]`);
9971002
input?.focus();
9981003
}
9991004

1000-
if (this.options.navigationHighlight) {
1005+
if (this.options.navigationHighlight && this.dropElm) {
10011006
// when hovering an select option, we will also change the highlight to that option
10021007
this._bindEventService.bind(
10031008
this.dropElm,
10041009
'mouseover',
10051010
((e: MouseEvent & { target: HTMLDivElement | HTMLLIElement }) => {
10061011
const liElm = (e.target.closest('.ms-select-all') || e.target.closest('li')) as HTMLLIElement;
10071012

1008-
if (this.dropElm.contains(liElm) && this.lastMouseOverPosition !== `${e.clientX}:${e.clientY}`) {
1013+
if (this.dropElm?.contains(liElm) && this.lastMouseOverPosition !== `${e.clientX}:${e.clientY}`) {
10091014
const optionElms = this.dropElm?.querySelectorAll<HTMLLIElement>(OPTIONS_LIST_SELECTOR) || [];
10101015
const newIdx = Array.from(optionElms).findIndex(el => el.dataset.key === liElm.dataset.key);
10111016
if (this._currentHighlightIndex !== newIdx && !liElm.classList.contains('disabled')) {
@@ -1145,7 +1150,7 @@ export class MultipleSelectInstance {
11451150
}
11461151

11471152
protected openDrop() {
1148-
if (this.choiceElm?.classList.contains('disabled')) {
1153+
if (!this.dropElm || this.choiceElm?.classList.contains('disabled')) {
11491154
return;
11501155
}
11511156
this.options.isOpen = true;
@@ -1343,13 +1348,15 @@ export class MultipleSelectInstance {
13431348
this.options.isOpen = false;
13441349
this.parentElm.classList.remove('ms-parent-open');
13451350
this.choiceElm?.querySelector('div.ms-icon-caret')?.classList.remove('open');
1346-
this.dropElm.style.display = 'none';
1347-
this.dropElm.ariaExpanded = 'false';
1348-
1349-
if (this.options.container) {
1350-
this.parentElm.appendChild(this.dropElm);
1351-
this.dropElm.style.top = 'auto';
1352-
this.dropElm.style.left = 'auto';
1351+
if (this.dropElm) {
1352+
this.dropElm.style.display = 'none';
1353+
this.dropElm.ariaExpanded = 'false';
1354+
1355+
if (this.options.container) {
1356+
this.parentElm.appendChild(this.dropElm);
1357+
this.dropElm.style.top = 'auto';
1358+
this.dropElm.style.left = 'auto';
1359+
}
13531360
}
13541361
this.options.onClose(reason);
13551362
}
@@ -1444,7 +1451,7 @@ export class MultipleSelectInstance {
14441451
protected updateSelected(rows?: HtmlStruct[]) {
14451452
for (let i = this.updateDataStart!; i < this.updateDataEnd!; i++) {
14461453
const row = this.updateData[i];
1447-
const inputElm = this.dropElm.querySelector<HTMLInputElement>(`input[data-key=${row._key}]`);
1454+
const inputElm = this.dropElm?.querySelector<HTMLInputElement>(`input[data-key=${row._key}]`);
14481455
if (inputElm) {
14491456
inputElm.checked = row.selected;
14501457
const closestLiElm = inputElm.closest('li');
@@ -1471,7 +1478,7 @@ export class MultipleSelectInstance {
14711478

14721479
if (this.selectAllElm) {
14731480
this.selectAllElm.ariaChecked = String(this.isAllSelected);
1474-
const checkboxIconElm = this.dropElm.querySelector('.ms-select-all .icon-checkbox-container div');
1481+
const checkboxIconElm = this.dropElm?.querySelector('.ms-select-all .icon-checkbox-container div');
14751482
if (checkboxIconElm) {
14761483
let iconClass = '';
14771484
if (this.isAllSelected) {
@@ -1857,53 +1864,55 @@ export class MultipleSelectInstance {
18571864
}
18581865

18591866
protected adjustDropWidthByText() {
1860-
const parentWidth = this.parentElm.scrollWidth;
1867+
if (this.dropElm) {
1868+
const parentWidth = this.parentElm.scrollWidth;
18611869

1862-
// keep the dropWidth/width as reference, if our new calculated width is below then we will re-adjust (else do nothing)
1863-
let currentDefinedWidth: number | string = parentWidth;
1864-
if (this.options.dropWidth || this.options.width) {
1865-
currentDefinedWidth = this.options.dropWidth || this.options.width || 0;
1866-
}
1870+
// keep the dropWidth/width as reference, if our new calculated width is below then we will re-adjust (else do nothing)
1871+
let currentDefinedWidth: number | string = parentWidth;
1872+
if (this.options.dropWidth || this.options.width) {
1873+
currentDefinedWidth = this.options.dropWidth || this.options.width || 0;
1874+
}
18671875

1868-
// calculate the "Select All" element width, this text is configurable which is why we recalculate every time
1869-
const selectAllSpanElm = this.dropElm.querySelector<HTMLSpanElement>('.ms-select-all span');
1870-
const dropUlElm = this.dropElm.querySelector('ul') as HTMLUListElement;
1876+
// calculate the "Select All" element width, this text is configurable which is why we recalculate every time
1877+
const selectAllSpanElm = this.dropElm.querySelector<HTMLSpanElement>('.ms-select-all span');
1878+
const dropUlElm = this.dropElm.querySelector('ul') as HTMLUListElement;
18711879

1872-
const liPadding = 26; // there are multiple padding involved, let's fix it at 26px
1880+
const liPadding = 26; // there are multiple padding involved, let's fix it at 26px
18731881

1874-
const selectAllElmWidth = selectAllSpanElm?.clientWidth ?? 0 + liPadding;
1875-
const hasScrollbar = dropUlElm.scrollHeight > dropUlElm.clientHeight;
1876-
const scrollbarWidth = hasScrollbar ? this.getScrollbarWidth() : 0;
1877-
let contentWidth = 0;
1882+
const selectAllElmWidth = selectAllSpanElm?.clientWidth ?? 0 + liPadding;
1883+
const hasScrollbar = dropUlElm.scrollHeight > dropUlElm.clientHeight;
1884+
const scrollbarWidth = hasScrollbar ? this.getScrollbarWidth() : 0;
1885+
let contentWidth = 0;
18781886

1879-
this.dropElm.querySelectorAll('li label').forEach(elm => {
1880-
if (elm.scrollWidth > contentWidth) {
1881-
contentWidth = elm.scrollWidth;
1882-
}
1883-
});
1887+
this.dropElm.querySelectorAll('li label').forEach(elm => {
1888+
if (elm.scrollWidth > contentWidth) {
1889+
contentWidth = elm.scrollWidth;
1890+
}
1891+
});
18841892

1885-
// add a padding & include the browser scrollbar width
1886-
contentWidth += liPadding + scrollbarWidth;
1893+
// add a padding & include the browser scrollbar width
1894+
contentWidth += liPadding + scrollbarWidth;
18871895

1888-
// make sure the new calculated width is wide enough to include the "Select All" text (this text is configurable)
1889-
if (contentWidth < selectAllElmWidth) {
1890-
contentWidth = selectAllElmWidth;
1891-
}
1896+
// make sure the new calculated width is wide enough to include the "Select All" text (this text is configurable)
1897+
if (contentWidth < selectAllElmWidth) {
1898+
contentWidth = selectAllElmWidth;
1899+
}
18921900

1893-
// if a maxWidth is defined, make sure our new calculate width is not over the maxWidth
1894-
if (this.options.maxWidth && contentWidth > this.options.maxWidth) {
1895-
contentWidth = this.options.maxWidth;
1896-
}
1901+
// if a maxWidth is defined, make sure our new calculate width is not over the maxWidth
1902+
if (this.options.maxWidth && contentWidth > this.options.maxWidth) {
1903+
contentWidth = this.options.maxWidth;
1904+
}
18971905

1898-
// if a minWidth is defined, make sure our new calculate width is not below the minWidth
1899-
if (this.options.minWidth && contentWidth < this.options.minWidth) {
1900-
contentWidth = this.options.minWidth;
1901-
}
1906+
// if a minWidth is defined, make sure our new calculate width is not below the minWidth
1907+
if (this.options.minWidth && contentWidth < this.options.minWidth) {
1908+
contentWidth = this.options.minWidth;
1909+
}
19021910

1903-
// finally re-adjust the drop to the new calculated width when necessary
1904-
if (currentDefinedWidth === '100%' || +currentDefinedWidth < contentWidth) {
1905-
this.dropElm.style.width = `${contentWidth}px`;
1906-
this.dropElm.style.maxWidth = `${contentWidth}px`;
1911+
// finally re-adjust the drop to the new calculated width when necessary
1912+
if (currentDefinedWidth === '100%' || +currentDefinedWidth < contentWidth) {
1913+
this.dropElm.style.width = `${contentWidth}px`;
1914+
this.dropElm.style.maxWidth = `${contentWidth}px`;
1915+
}
19071916
}
19081917
}
19091918

packages/multiple-select-vanilla/src/utils/domUtils.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,11 @@ export function getElementOffset(element?: HTMLElement): HtmlElementPosition | u
157157
return { top, left, bottom, right };
158158
}
159159

160-
export function getElementSize(elm: HTMLElement, mode: 'inner' | 'outer' | 'scroll', type: 'height' | 'width') {
160+
export function getElementSize(elm: HTMLElement | undefined, mode: 'inner' | 'outer' | 'scroll', type: 'height' | 'width') {
161+
if (!elm) {
162+
return 0;
163+
}
164+
161165
// first try defined style width or offsetWidth (which include scroll & padding)
162166
let size = Number.parseFloat(elm.style[type]);
163167
if (!size || Number.isNaN(size)) {

0 commit comments

Comments
 (0)