Skip to content

Commit faadee8

Browse files
authored
SelectBox: Prevent layout/hide on no room/offscreen Fixes: microsoft#55750 (microsoft#56173)
1 parent 5399233 commit faadee8

2 files changed

Lines changed: 65 additions & 22 deletions

File tree

src/vs/base/browser/ui/contextview/contextview.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,11 @@ export class ContextView {
173173
}
174174

175175
private doLayout(): void {
176+
// Check that we still have a delegate - this.delegate.layout may have hidden
177+
if (!this.isVisible()) {
178+
return;
179+
}
180+
176181
// Get anchor
177182
let anchor = this.delegate.getAnchor();
178183

src/vs/base/browser/ui/selectBox/selectBoxCustom.ts

Lines changed: 60 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ class SelectListRenderer implements IRenderer<ISelectOptionItem, ISelectListTemp
8282
export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISelectOptionItem> {
8383

8484
private static readonly DEFAULT_DROPDOWN_MINIMUM_BOTTOM_MARGIN = 32;
85-
private static readonly DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN = 42;
85+
private static readonly DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN = 2;
8686
private static readonly DEFAULT_MINIMUM_VISIBLE_OPTIONS = 3;
8787

8888
private _isVisible: boolean;
@@ -393,9 +393,11 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
393393
}
394394

395395
// Set drop-down position above/below from required height and margins
396-
this.layoutSelectDropDown(true);
396+
// If pre-layout cannot fit at least one option do not show drop-down
397+
if (!this.layoutSelectDropDown(true)) {
398+
return;
399+
}
397400

398-
this._isVisible = true;
399401
this.cloneElementFont(this.selectElement, this.selectDropDownContainer);
400402

401403
this.contextViewProvider.showContextView({
@@ -411,6 +413,7 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
411413

412414
// Track initial selection the case user escape, blur
413415
this._currentSelection = this.selected;
416+
this._isVisible = true;
414417
}
415418

416419
private hideSelectDropDown(focusSelect: boolean) {
@@ -423,6 +426,7 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
423426
if (focusSelect) {
424427
this.selectElement.focus();
425428
}
429+
426430
this.contextViewProvider.hideContextView();
427431
}
428432

@@ -443,7 +447,7 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
443447
};
444448
}
445449

446-
private layoutSelectDropDown(preLayoutPosition?: boolean) {
450+
private layoutSelectDropDown(preLayoutPosition?: boolean): boolean {
447451

448452
// Layout ContextView drop down select list and container
449453
// Have to manage our vertical overflow, sizing, position below or above
@@ -454,30 +458,54 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
454458
const selectPosition = dom.getDomNodePagePosition(this.selectElement);
455459
const styles = getComputedStyle(this.selectElement);
456460
const verticalPadding = parseFloat(styles.getPropertyValue('--dropdown-padding-top')) + parseFloat(styles.getPropertyValue('--dropdown-padding-bottom'));
457-
let maxSelectDropDownHeight = 0;
458-
maxSelectDropDownHeight = (window.innerHeight - selectPosition.top - selectPosition.height - this.selectBoxOptions.minBottomMargin);
461+
const maxSelectDropDownHeightBelow = (window.innerHeight - selectPosition.top - selectPosition.height - this.selectBoxOptions.minBottomMargin);
462+
const maxSelectDropDownHeightAbove = (selectPosition.top - SelectBoxList.DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN);
459463

464+
// Get initial list height and determine space above and below
460465
this.selectList.layout();
461466
let listHeight = this.selectList.contentHeight;
467+
const minRequiredDropDownHeight = listHeight + verticalPadding;
468+
const maxVisibleOptionsBelow = ((Math.floor((maxSelectDropDownHeightBelow - verticalPadding) / this.getHeight())));
469+
const maxVisibleOptionsAbove = ((Math.floor((maxSelectDropDownHeightAbove - verticalPadding) / this.getHeight())));
462470

463471
// If we are only doing pre-layout check/adjust position only
464472
// Calculate vertical space available, flip up if insufficient
465-
// Use reflected padding on parent select, ContextView style properties not available before DOM attachment
473+
// Use reflected padding on parent select, ContextView style
474+
// properties not available before DOM attachment
475+
466476
if (preLayoutPosition) {
477+
// Check if select moved out of viewport , do not open
478+
// If at least one option cannot be shown, don't open the drop-down or hide/remove if open
479+
480+
if ((selectPosition.top + selectPosition.height) > (window.innerHeight - 22)
481+
|| selectPosition.top < SelectBoxList.DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN
482+
|| ((maxVisibleOptionsBelow < 1) && (maxVisibleOptionsAbove < 1))) {
483+
// Indicate we cannot open
484+
return false;
485+
}
467486

487+
// Determine if we have to flip up
468488
// Always show complete list items - never more than Max available vertical height
469-
if (listHeight + verticalPadding > maxSelectDropDownHeight) {
470-
const maxVisibleOptions = ((Math.floor((maxSelectDropDownHeight - verticalPadding) / this.getHeight())));
471-
472-
// Check if we can at least show min items otherwise flip above
473-
if (maxVisibleOptions < SelectBoxList.DEFAULT_MINIMUM_VISIBLE_OPTIONS) {
474-
this._dropDownPosition = AnchorPosition.ABOVE;
475-
} else {
476-
this._dropDownPosition = AnchorPosition.BELOW;
477-
}
489+
if (maxVisibleOptionsBelow < SelectBoxList.DEFAULT_MINIMUM_VISIBLE_OPTIONS
490+
&& maxVisibleOptionsAbove > maxVisibleOptionsBelow
491+
&& this.options.length > maxVisibleOptionsBelow
492+
) {
493+
this._dropDownPosition = AnchorPosition.ABOVE;
494+
} else {
495+
this._dropDownPosition = AnchorPosition.BELOW;
478496
}
479497
// Do full layout on showSelectDropDown only
480-
return;
498+
return true;
499+
}
500+
501+
// Check if select out of viewport or cutting into status bar
502+
if ((selectPosition.top + selectPosition.height) > (window.innerHeight - 22)
503+
|| selectPosition.top < SelectBoxList.DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN
504+
|| (this._dropDownPosition === AnchorPosition.BELOW && maxVisibleOptionsBelow < 1)
505+
|| (this._dropDownPosition === AnchorPosition.ABOVE && maxVisibleOptionsAbove < 1)) {
506+
// Cannot properly layout, close and hide
507+
this.hideSelectDropDown(true);
508+
return false;
481509
}
482510

483511
// Make visible to enable measurements
@@ -486,15 +514,22 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
486514
// SetUp list dimensions and layout - account for container padding
487515
// Use position to check above or below available space
488516
if (this._dropDownPosition === AnchorPosition.BELOW) {
517+
if (this._isVisible && maxVisibleOptionsBelow + maxVisibleOptionsAbove < 1) {
518+
// If drop-down is visible, must be doing a DOM re-layout, hide since we don't fit
519+
// Hide drop-down, hide contextview, focus on parent select
520+
this.hideSelectDropDown(true);
521+
return false;
522+
}
523+
489524
// Set container height to max from select bottom to margin (default/minBottomMargin)
490-
if (listHeight + verticalPadding > maxSelectDropDownHeight) {
491-
listHeight = ((Math.floor((maxSelectDropDownHeight - verticalPadding) / this.getHeight())) * this.getHeight());
525+
if (minRequiredDropDownHeight > maxSelectDropDownHeightBelow) {
526+
listHeight = (maxVisibleOptionsBelow * this.getHeight() + verticalPadding);
492527
}
493528
} else {
494529
// Set container height to max from select top to margin (default/minTopMargin)
495-
maxSelectDropDownHeight = (selectPosition.top - SelectBoxList.DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN);
496-
if (listHeight + verticalPadding > maxSelectDropDownHeight) {
497-
listHeight = ((Math.floor((maxSelectDropDownHeight - SelectBoxList.DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN) / this.getHeight())) * this.getHeight());
530+
if (minRequiredDropDownHeight > maxSelectDropDownHeightAbove) {
531+
// listHeight = ((Math.floor((maxSelectDropDownHeightBelow - verticalPadding) / this.getHeight())) * this.getHeight());
532+
listHeight = (maxVisibleOptionsAbove * this.getHeight() + verticalPadding);
498533
}
499534
}
500535

@@ -522,6 +557,9 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
522557
this.selectDropDownListContainer.setAttribute('tabindex', '0');
523558
dom.toggleClass(this.selectElement, 'synthetic-focus', true);
524559
dom.toggleClass(this.selectDropDownContainer, 'synthetic-focus', true);
560+
return true;
561+
} else {
562+
return false;
525563
}
526564
}
527565

0 commit comments

Comments
 (0)