Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions dev/vscode-single-select/combobox-mode/custom-controller.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>
VSCode Elements — Single Select — Combobox — Custom Controller
</title>
<link
rel="stylesheet"
href="/node_modules/@vscode/codicons/dist/codicon.css"
id="vscode-codicon-stylesheet"
>
<script
type="module"
src="/node_modules/@vscode-elements/webview-playground/dist/index.js"
></script>
<script type="module" src="/dist/main.js"></script>
</head>
<body>
<main>
<vscode-demo>
<vscode-single-select combobox label="Default controller (baseline)">
<vscode-option>Lorem</vscode-option>
<vscode-option>Ipsum</vscode-option>
<vscode-option>Dolor</vscode-option>
<vscode-option disabled>Sit (disabled)</vscode-option>
<vscode-option>Amet</vscode-option>
</vscode-single-select>
</vscode-demo>

<vscode-demo>
<vscode-single-select-custom
combobox
label="Custom controller (wrap navigation + default filter)"
>
<vscode-option>Lorem</vscode-option>
<vscode-option>Ipsum</vscode-option>
<vscode-option>Dolor</vscode-option>
<vscode-option disabled>Sit (disabled)</vscode-option>
<vscode-option>Amet</vscode-option>
</vscode-single-select-custom>
</vscode-demo>

<script type="module">
import {VscodeSingleSelect} from '/dist/vscode-single-select/vscode-single-select.js';
import {OptionListController} from '/dist/includes/vscode-select/OptionListController.js';

class MyOptionListController extends OptionListController {
// Prefer startsWithPerTerm as a default filter when in combobox mode
get filterMethod() {
return super.filterMethod ?? 'startsWithPerTerm';
}
set filterMethod(val) {
super.filterMethod = val ?? 'startsWithPerTerm';
}
next(fromIndex) {
const res = super.next(fromIndex);
if (res) return res;
const first = this.next(-1);
return first ?? null;
}
prev(fromIndex) {
const res = super.prev(fromIndex);
if (res) return res;
const len = this.numOptions;
for (let i = len; i >= 0; i--) {
const p = super.prev(i + 1);
if (p) return p;
}
return null;
}
}

class VscodeSingleSelectCustom extends VscodeSingleSelect {
createOptionListController() {
return new MyOptionListController(this);
}
}

customElements.define(
'vscode-single-select-custom',
VscodeSingleSelectCustom
);
</script>
</main>
</body>
</html>
216 changes: 216 additions & 0 deletions dev/vscode-single-select/combobox-mode/npm-search-controller.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>
VSCode Elements — Single Select — Combobox — npm Search (Custom
Controller)
</title>
<link
rel="stylesheet"
href="/node_modules/@vscode/codicons/dist/codicon.css"
id="vscode-codicon-stylesheet"
>
<script
type="module"
src="/node_modules/@vscode-elements/webview-playground/dist/index.js"
></script>
<script type="module" src="/dist/main.js"></script>
</head>
<body>
<main>
<vscode-demo>
<vscode-single-select-npm combobox label="npm packages (live search)">
<vscode-option disabled>Type at least 2 characters…</vscode-option>
</vscode-single-select-npm>
</vscode-demo>

<script type="module">
import {VscodeSingleSelect} from '/dist/vscode-single-select/vscode-single-select.js';
import {OptionListController} from '/dist/includes/vscode-select/OptionListController.js';

// Simple debounce helper
function debounce(fn, delay = 300) {
let t;
return (...args) => {
clearTimeout(t);
t = setTimeout(() => fn(...args), delay);
};
}

class NpmOptionListController extends OptionListController {
constructor(host) {
super(host);
this._abort = null;
this._hostEl = host;
this._busyCount = 0;
this._spinnerEl = null;
this._debouncedFetch = debounce(
(q) => this._fetchAndPopulate(q),
300
);
}

// Prefer a friendlier default in combobox mode
set filterMethod(val) {
super.filterMethod = val ?? 'startsWithPerTerm';
}

// Always present a string to the host; prevents accidental 'undefined' propagation
get filterPattern() {
return super.filterPattern ?? '';
}
set filterPattern(val) {
const pattern = val ?? '';
super.filterPattern = pattern;
const q = pattern.trim();

if (q.length >= 2) {
this._debouncedFetch(q);
} else {
this.populate([
{
label: 'Type at least 2 characters…',
value: '',
description: '',
disabled: true,
selected: false,
},
]);
this.activeIndex = -1;
}
}

async _fetchAndPopulate(query) {
// Cancel any in-flight request
if (this._abort) this._abort.abort();
this._abort = new AbortController();

// mark busy and show spinner
this._busyCount++;
this._updateSpinner();

try {
const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(query)}&size=10`;
const res = await fetch(url, {signal: this._abort.signal});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();

const objects = Array.isArray(data?.objects) ? data.objects : [];

if (objects.length === 0) {
this.populate([
{
label: 'No results',
value: '',
description: '',
disabled: true,
selected: false,
},
]);
this.activeIndex = -1;
return;
}

const options = objects.map((o) => {
const pkg = o.package ?? {};
const name = pkg.name ?? '';
const version = pkg.version ? `@${pkg.version}` : '';
const desc = (pkg.description ?? '').toString().trim();
return {
label: name,
value: name,
description: [version, desc].filter(Boolean).join(' '),
disabled: false,
selected: false,
};
});

this.populate(options);
this.activeIndex = options.length > 0 ? 0 : -1;
} catch (err) {
if (err?.name === 'AbortError') return; // ignore canceled request
// Fallback UI state on errors
this.populate([
{
label: 'Error loading results',
value: '',
description: '',
disabled: true,
selected: false,
},
]);
this.activeIndex = -1;
} finally {
// clear busy and hide spinner if no more requests
this._busyCount = Math.max(0, this._busyCount - 1);
this._updateSpinner();
}
}

_updateSpinner() {
const host = this._hostEl;
if (!host) return;
if (this._busyCount > 0) {
if (!this._spinnerEl) {
const sp = document.createElement('span');
sp.slot = 'iconAfter';
sp.className = 'codicon codicon-loading codicon-modifier-spin';
sp.setAttribute('aria-label', 'Loading');
this._spinnerEl = sp;
}
if (!host.contains(this._spinnerEl)) {
host.appendChild(this._spinnerEl);
}
} else {
if (this._spinnerEl && this._spinnerEl.parentElement === host) {
host.removeChild(this._spinnerEl);
}
}
}
}

class VscodeSingleSelectNpm extends VscodeSingleSelect {
constructor() {
super();
// Ensure our custom controller is used even if the base factory hasn't been picked up in the built dist yet.
try {
this.removeController(this._opts);
} catch {}
this._opts = new NpmOptionListController(this);
// Sync combobox mode reliably at construction time (attribute is reliable before property upgrade)
const initialCombobox =
this.hasAttribute('combobox') || this.combobox;
this._opts.comboboxMode = initialCombobox;
// Ensure initial state doesn't propagate undefined values to the input
this._isBeingFiltered = false;
this._opts.filterPattern = '';
}

// Defensive: ensure the input never shows the literal 'undefined'
_onComboboxInputFocus(ev) {
super._onComboboxInputFocus?.(ev);
const input = this.renderRoot?.querySelector('.combobox-input');
if (
input &&
(input.value === undefined || input.value === 'undefined')
) {
input.value = '';
}
}

// Keep factory override for when the base (dist) includes the factory hook
createOptionListController() {
return new NpmOptionListController(this);
}
}

customElements.define(
'vscode-single-select-npm',
VscodeSingleSelectNpm
);
</script>
</main>
</body>
</html>
84 changes: 84 additions & 0 deletions dev/vscode-single-select/select-mode/custom-controller.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VSCode Elements — Single Select — Custom Controller</title>
<link
rel="stylesheet"
href="/node_modules/@vscode/codicons/dist/codicon.css"
id="vscode-codicon-stylesheet"
>
<script
type="module"
src="/node_modules/@vscode-elements/webview-playground/dist/index.js"
></script>
<script type="module" src="/dist/main.js"></script>
</head>
<body>
<main>
<vscode-demo>
<vscode-single-select label="Default controller (baseline)">
<vscode-option>Lorem</vscode-option>
<vscode-option>Ipsum</vscode-option>
<vscode-option>Dolor</vscode-option>
<vscode-option disabled>Sit (disabled)</vscode-option>
<vscode-option>Amet</vscode-option>
</vscode-single-select>
</vscode-demo>

<vscode-demo>
<vscode-single-select-custom
label="Custom controller (wrap navigation)"
>
<vscode-option>Lorem</vscode-option>
<vscode-option>Ipsum</vscode-option>
<vscode-option>Dolor</vscode-option>
<vscode-option disabled>Sit (disabled)</vscode-option>
<vscode-option>Amet</vscode-option>
</vscode-single-select-custom>
</vscode-demo>

<script type="module">
// Import the base single-select and option list controller from the built output.
import {VscodeSingleSelect} from '/dist/vscode-single-select/vscode-single-select.js';
import {OptionListController} from '/dist/includes/vscode-select/OptionListController.js';

// A simple custom controller that wraps around on ArrowDown/ArrowUp
class MyOptionListController extends OptionListController {
next(fromIndex) {
const res = super.next(fromIndex);
if (res) return res;
// wrap to first visible option if any
const first = this.next(-1);
return first ?? null;
}
prev(fromIndex) {
const res = super.prev(fromIndex);
if (res) return res;
// wrap to last visible option if any
// Find the last visible, enabled option by scanning backwards from the end
const len = this.numOptions;
for (let i = len; i >= 0; i--) {
const p = super.prev(i + 1);
if (p) return p;
}
return null;
}
}

// Subclass the shipped element and override the controller factory
class VscodeSingleSelectCustom extends VscodeSingleSelect {
createOptionListController() {
return new MyOptionListController(this);
}
}

customElements.define(
'vscode-single-select-custom',
VscodeSingleSelectCustom
);
</script>
</main>
</body>
</html>
Loading