Use faster, editable dropdown for Collations in database dialogs (#23974)

* Also fixed an issue where a manually edited text field doesn't get updated when selecting the same dropdown value from before the manual edit.
---------

Co-authored-by: Charles Gagnon <chgagnon@microsoft.com>
This commit is contained in:
Cory Rivera
2023-07-26 10:04:29 -07:00
committed by GitHub
parent 5f1801d6d4
commit cc778ad69f
7 changed files with 67 additions and 32 deletions

View File

@@ -1826,14 +1826,21 @@ declare module 'azdata' {
/**
* Corresponds to the aria-live accessibility attribute for this component
*/
ariaLive?: AriaLiveValue
ariaLive?: AriaLiveValue;
}
export interface ContainerProperties extends ComponentProperties {
/**
* Corresponds to the aria-live accessibility attribute for this component
*/
ariaLive?: AriaLiveValue
ariaLive?: AriaLiveValue;
}
export interface DropDownProperties {
/**
* Whether or not an option in the list must be selected or a "new" option can be set. Only applicable when 'editable' is true. Default false.
*/
strictSelection?: boolean;
}
export interface NodeInfo {

View File

@@ -112,9 +112,11 @@ export class Dropdown extends Disposable implements IListVirtualDelegate<string>
this._inputContainer = DOM.append(this._el, DOM.$('.dropdown-input.select-container'));
this._inputContainer.style.width = '100%';
this._inputContainer.style.height = '100%';
this._selectListContainer = DOM.$('div');
this._selectListContainer.style.backgroundColor = opt.contextBackground;
this._selectListContainer.style.outline = `1px solid ${opt.contextBorder}`;
this._input = new InputBox(this._inputContainer, contextViewService, {
validationOptions: {
// @SQLTODO
@@ -141,11 +143,11 @@ export class Dropdown extends Disposable implements IListVirtualDelegate<string>
}));
const inputTracker = this._register(DOM.trackFocus(this._input.inputElement));
inputTracker.onDidBlur(() => {
this._register(inputTracker.onDidBlur(() => {
if (!this._selectList.isDOMFocused()) {
this._onBlur.fire();
}
});
}));
/*
This event listener is intended to close the expanded drop down when the ADS shell window is resized
@@ -167,14 +169,12 @@ export class Dropdown extends Disposable implements IListVirtualDelegate<string>
break;
case KeyCode.Escape:
if (this._isDropDownVisible) {
this._input.validate();
this._onBlur.fire();
this._hideList();
e.stopPropagation();
}
break;
case KeyCode.Tab:
this._input.validate();
this._onBlur.fire();
this._hideList();
break;
@@ -244,7 +244,7 @@ export class Dropdown extends Disposable implements IListVirtualDelegate<string>
}
}));
this._input.onDidChange(e => {
this._register(this._input.onDidChange(e => {
if (this._dataSource.values.length > 0) {
this._dataSource.filter = e;
if (this._isDropDownVisible) {
@@ -254,12 +254,11 @@ export class Dropdown extends Disposable implements IListVirtualDelegate<string>
if (this.fireOnTextChange) {
this.value = e;
}
});
}));
this.onBlur(() => {
this._register(this.onBlur(() => {
this._hideList();
this._input.validate();
});
}));
this._register(this._selectList);
this._register(this._input);
@@ -316,6 +315,8 @@ export class Dropdown extends Disposable implements IListVirtualDelegate<string>
private _hideList(): void {
this.contextViewService.hideContextView();
this._inputContainer.setAttribute('aria-expanded', 'false');
// Show error for input box in case the user closed the dropdown without selecting anything, like by hitting Escape
this.input.validate();
}
private _updateDropDownList(): void {
@@ -323,17 +324,9 @@ export class Dropdown extends Disposable implements IListVirtualDelegate<string>
const selectedIndex = this._dataSource.filteredValues.indexOf(this.value);
this._selectList.setSelection(selectedIndex !== -1 ? [selectedIndex] : []);
let width = this._inputContainer.clientWidth;
// Find the longest option in the list and set our width to that (max 500px)
const longestOption = this._dataSource.filteredValues.reduce((previous, current) => {
return previous.length > current.length ? previous : current;
}, '');
this._widthControlElement.innerText = longestOption;
const inputContainerWidth = DOM.getContentWidth(this._inputContainer);
const longestOptionWidth = DOM.getTotalWidth(this._widthControlElement);
width = clamp(longestOptionWidth, inputContainerWidth, 500);
let width = clamp(longestOptionWidth, inputContainerWidth, 500);
const height = Math.min(this._dataSource.filteredValues.length * this.getHeight(), this._options.maxHeight ?? 500);
this._selectListContainer.style.width = `${width}px`;
@@ -345,6 +338,13 @@ export class Dropdown extends Disposable implements IListVirtualDelegate<string>
if (vals) {
this._dataSource.filter = undefined;
this._dataSource.values = vals;
// Find the longest option in the list to set the width of the dropdown
let longestOption = this._dataSource.values.reduce((previous, current) => {
return previous.length > current.length ? previous : current;
}, '');
this._widthControlElement.innerText = longestOption;
if (this._isDropDownVisible) {
this._updateDropDownList();
}
@@ -357,9 +357,12 @@ export class Dropdown extends Disposable implements IListVirtualDelegate<string>
}
public set value(val: string) {
this._input.value = val;
if (this._previousValue !== val) {
// A value can be changed either by selecting an option from the dropdown list or editing the text field directly.
// If you try to select the same dropdown value again after changing the text field directly, that change should
// still be applied, which is why we check both _previousValue and _input.value.
if (this._previousValue !== val || this._input.value !== val) {
this._previousValue = val;
this._input.value = val;
this._onValueChange.fire(val);
}
}
@@ -378,7 +381,7 @@ export class Dropdown extends Disposable implements IListVirtualDelegate<string>
}
private _inputValidator(value: string): IMessage | null {
if (!this._input.hasFocus() && this._input.isEnabled() && !this._selectList.isDOMFocused() && !this._dataSource.values.some(i => i === value)) {
if (this._input.isEnabled() && !this._selectList.isDOMFocused() && !this._isDropDownVisible && !this._dataSource.values.some(i => i === value)) {
if (this._options.strictSelection && this._options.errorMessage) {
return {
content: this._options.errorMessage,
@@ -418,4 +421,8 @@ export class Dropdown extends Disposable implements IListVirtualDelegate<string>
public get options(): IDropdownOptions {
return this._options;
}
public set strictSelection(val: boolean | undefined) {
this._options.strictSelection = val;
}
}

View File

@@ -68,4 +68,12 @@ suite('Editable dropdown tests', () => {
dropdown.input.value = options.values[0];
assert.strictEqual(count, 3, 'onValueChange event was fired with input box value change even after setting the fireOnTextChange to false');
});
test('selecting same dropdown value again after changing text field should update text field', () => {
const dropdown = new Dropdown(container, undefined, options);
dropdown.value = options.values[0];
dropdown.input.value = 'NotARealValue';
dropdown.value = options.values[0];
assert.strictEqual(dropdown.input.value, options.values[0]);
});
});

View File

@@ -81,7 +81,7 @@ export default class DropDownComponent extends ComponentBase<azdata.DropDownProp
if (this._editableDropDownContainer) {
let dropdownOptions: IDropdownOptions = {
values: [],
strictSelection: false,
strictSelection: this.strictSelection ?? false,
placeholder: this.placeholder,
maxHeight: 125,
ariaLabel: '',
@@ -178,7 +178,7 @@ export default class DropDownComponent extends ComponentBase<azdata.DropDownProp
}
this._editableDropdown.enabled = this.enabled;
this._editableDropdown.fireOnTextChange = this.fireOnTextChange;
this._editableDropdown.strictSelection = this.strictSelection;
if (this.placeholder) {
this._editableDropdown.input.setPlaceHolder(this.placeholder);
}
@@ -338,7 +338,11 @@ export default class DropDownComponent extends ComponentBase<azdata.DropDownProp
}
public get placeholder(): string | undefined {
return this.getPropertyOrDefault<string>((props) => props.placeholder, undefined);
return this.getPropertyOrDefault<string | undefined>((props) => props.placeholder, undefined);
}
public get strictSelection(): boolean | undefined {
return this.getPropertyOrDefault<boolean | undefined>((props) => props.strictSelection, undefined);
}
public get validationErrorMessages(): string[] | undefined {