Skip to content

Commit

Permalink
Add an input dialog for multiple selection (#13621)
Browse files Browse the repository at this point in the history
* Add an input dialog for multiple selection

* Update packages/ui-components/style/styling.css

Co-authored-by: Frédéric Collonval <fcollonval@users.noreply.github.com>

* Update packages/apputils/test/inputdialog.spec.ts

Co-authored-by: Frédéric Collonval <fcollonval@users.noreply.github.com>

* Remove the hidden entry in select

Co-authored-by: Frédéric Collonval <fcollonval@users.noreply.github.com>
  • Loading branch information
brichet and fcollonval committed Jan 3, 2023
1 parent 3d7445f commit 60ddcbf
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 15 deletions.
90 changes: 90 additions & 0 deletions packages/apputils/src/inputdialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,40 @@ export namespace InputDialog {
});
}

/**
* Constructor options for item selection input dialogs
*/
export interface IMultipleItemsOptions extends IOptions {
/**
* List of choices
*/
items: Array<string>;
/**
* Default choices
*/
defaults?: string[];
}

/**
* Create and show a input dialog for a choice.
*
* @param options - The dialog setup options.
*
* @returns A promise that resolves with whether the dialog was accepted
*/
export function getMultipleItems(
options: IMultipleItemsOptions
): Promise<Dialog.IResult<string[]>> {
return showDialog({
...options,
body: new InputMultipleItemsDialog(options),
buttons: [
Dialog.cancelButton({ label: options.cancelLabel }),
Dialog.okButton({ label: options.okLabel })
]
});
}

/**
* Constructor options for text input dialogs
*/
Expand Down Expand Up @@ -459,3 +493,59 @@ class InputItemsDialog extends InputDialogBase<string> {
private _list: HTMLSelectElement;
private _editable: boolean;
}

/**
* Widget body for input list dialog
*/
class InputMultipleItemsDialog extends InputDialogBase<string> {
/**
* InputMultipleItemsDialog constructor
*
* @param options Constructor options
*/
constructor(options: InputDialog.IMultipleItemsOptions) {
super(options.label);

let defaults = options.defaults || [];

this._list = document.createElement('select');
this._list.setAttribute('multiple', '');

options.items.forEach(item => {
const option = document.createElement('option');
option.value = item;
option.textContent = item;
this._list.appendChild(option);
});

// use the select
this._input.remove();
this.node.appendChild(this._list);

// select the current ones
const htmlOptions = this._list.options;
for (let i: number = 0; i < htmlOptions.length; i++) {
const option = htmlOptions[i];
if (defaults.includes(option.value)) {
option.selected = true;
} else {
option.selected = false;
}
}
}

/**
* Get the user choices
*/
getValue(): string[] {
let result = [];
for (let opt of this._list.options) {
if (opt.selected && !opt.classList.contains('hidden')) {
result.push(opt.value || opt.text);
}
}
return result;
}

private _list: HTMLSelectElement;
}
42 changes: 42 additions & 0 deletions packages/apputils/test/inputdialog.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,48 @@ describe('@jupyterlab/apputils', () => {
});
});

describe('getMultipleItems()', () => {
it('should accept at least two arguments', async () => {
const dialog = InputDialog.getMultipleItems({
title: 'list',
items: ['item1']
});

await dismissDialog();
expect((await dialog).button.accept).toBe(false);
});

it('should return empty list if none is selected', async () => {
const dialog = InputDialog.getMultipleItems({
items: ['item1', 'item2'],
title: 'Pick a choice'
});

await acceptDialog();

const result = await dialog;

expect(result.button.accept).toBe(true);
expect(result.value).toStrictEqual([]);
});

it('should accept option "defaults"', async () => {
const dialog = InputDialog.getMultipleItems({
label: 'list',
items: ['item1', 'item2', 'item3'],
defaults: ['item1', 'item3'],
title: 'Pick a choice'
});

await acceptDialog();

const result = await dialog;

expect(result.button.accept).toBe(true);
expect(result.value).toStrictEqual(['item1', 'item3']);
});
});

describe('getText()', () => {
it('should accept at least one argument', async () => {
const dialog = InputDialog.getText({
Expand Down
35 changes: 22 additions & 13 deletions packages/ui-components/src/components/styling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ export namespace Styling {
node.classList.add('jp-mod-styled');
}
if (node.localName === 'select') {
wrapSelect(node as HTMLSelectElement);
const multiple = node.hasAttribute('multiple');
wrapSelect(node as HTMLSelectElement, multiple);
}
const nodes = node.getElementsByTagName(tagName);
for (let i = 0; i < nodes.length; i++) {
Expand All @@ -49,15 +50,19 @@ export namespace Styling {
child.classList.add(className);
}
if (tagName === 'select') {
wrapSelect(child as HTMLSelectElement);
const multiple = child.hasAttribute('multiple');
wrapSelect(child as HTMLSelectElement, multiple);
}
}
}

/**
* Wrap a select node.
*/
export function wrapSelect(node: HTMLSelectElement): HTMLElement {
export function wrapSelect(
node: HTMLSelectElement,
multiple?: boolean
): HTMLElement {
const wrapper = document.createElement('div');
wrapper.classList.add('jp-select-wrapper');
node.addEventListener('focus', Private.onFocus);
Expand All @@ -68,16 +73,20 @@ export namespace Styling {
}
wrapper.appendChild(node);

// add the icon node
wrapper.appendChild(
caretDownEmptyIcon.element({
tag: 'span',
stylesheet: 'select',
right: '8px',
top: '5px',
width: '18px'
})
);
if (multiple) {
wrapper.classList.add('multiple');
} else {
// add the icon node
wrapper.appendChild(
caretDownEmptyIcon.element({
tag: 'span',
stylesheet: 'select',
right: '8px',
top: '5px',
width: '18px'
})
);
}

return wrapper;
}
Expand Down
15 changes: 13 additions & 2 deletions packages/ui-components/style/styling.css
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,14 @@ input.jp-mod-styled:focus {
flex-direction: column;
padding: 1px;
background-color: var(--jp-layout-color1);
height: 28px;
box-sizing: border-box;
margin-bottom: 12px;
}

.jp-select-wrapper:not(.multiple) {
height: 28px;
}

.jp-select-wrapper.jp-mod-focused select.jp-mod-styled {
border: var(--jp-border-width) solid var(--jp-input-active-border-color);
box-shadow: var(--jp-input-box-shadow);
Expand All @@ -73,7 +76,6 @@ select.jp-mod-styled:hover {

select.jp-mod-styled {
flex: 1 1 auto;
height: 32px;
width: 100%;
font-size: var(--jp-ui-font-size2);
background: var(--jp-input-background);
Expand All @@ -86,3 +88,12 @@ select.jp-mod-styled {
-webkit-appearance: none;
-moz-appearance: none;
}

select.jp-mod-styled:not([multiple]) {
height: 32px;
}

select.jp-mod-styled[multiple] {
max-height: 200px;
overflow-y: auto;
}

0 comments on commit 60ddcbf

Please sign in to comment.