feat(combobox): implement ComboboxController
This commit is contained in:
parent
1a531d018f
commit
bea8cba6ce
1 changed files with 91 additions and 0 deletions
91
app/javascript/controllers/combobox_controller.ts
Normal file
91
app/javascript/controllers/combobox_controller.ts
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
import invariant from 'tiny-invariant';
|
||||||
|
import { isInputElement, isElement } from '@coldwired/utils';
|
||||||
|
|
||||||
|
import { Hint } from '../shared/combobox';
|
||||||
|
import { ComboboxUI } from '../shared/combobox-ui';
|
||||||
|
import { ApplicationController } from './application_controller';
|
||||||
|
|
||||||
|
export class ComboboxController extends ApplicationController {
|
||||||
|
#combobox?: ComboboxUI;
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
const { input, valueInput, list, item, hint } = this.getElements();
|
||||||
|
const hints = JSON.parse(list.dataset.hints ?? '{}') as Record<
|
||||||
|
string,
|
||||||
|
string
|
||||||
|
>;
|
||||||
|
this.#combobox = new ComboboxUI({
|
||||||
|
input,
|
||||||
|
valueInput,
|
||||||
|
list,
|
||||||
|
item,
|
||||||
|
hint,
|
||||||
|
allowsCustomValue: this.element.hasAttribute('data-allows-custom-value'),
|
||||||
|
getHintText: (hint) => getHintText(hints, hint)
|
||||||
|
});
|
||||||
|
this.#combobox.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.#combobox?.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getElements() {
|
||||||
|
const input =
|
||||||
|
this.element.querySelector<HTMLInputElement>('input[type="text"]');
|
||||||
|
const valueInput = this.element.querySelector<HTMLInputElement>(
|
||||||
|
'input[type="hidden"]'
|
||||||
|
);
|
||||||
|
const list = this.element.querySelector<HTMLUListElement>('[role=listbox]');
|
||||||
|
const item = this.element.querySelector<HTMLTemplateElement>('template');
|
||||||
|
const hint =
|
||||||
|
this.element.querySelector<HTMLElement>('[aria-live]') ?? undefined;
|
||||||
|
|
||||||
|
invariant(
|
||||||
|
isInputElement(input),
|
||||||
|
'ComboboxController requires a input element'
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
isInputElement(valueInput),
|
||||||
|
'ComboboxController requires a hidden input element'
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
isElement(list),
|
||||||
|
'ComboboxController requires a [role=listbox] element'
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
isElement(item),
|
||||||
|
'ComboboxController requires a template element'
|
||||||
|
);
|
||||||
|
|
||||||
|
return { input, valueInput, list, item, hint };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHintText(hints: Record<string, string>, hint: Hint): string {
|
||||||
|
const slot = hints[getSlotName(hint)];
|
||||||
|
switch (hint.type) {
|
||||||
|
case 'empty':
|
||||||
|
return slot;
|
||||||
|
case 'selected':
|
||||||
|
return slot.replace('{label}', hint.label ?? '');
|
||||||
|
default:
|
||||||
|
return slot
|
||||||
|
.replace('{count}', String(hint.count))
|
||||||
|
.replace('{label}', hint.label ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSlotName(hint: Hint): string {
|
||||||
|
switch (hint.type) {
|
||||||
|
case 'empty':
|
||||||
|
return 'empty';
|
||||||
|
case 'selected':
|
||||||
|
return 'selected';
|
||||||
|
default:
|
||||||
|
if (hint.count == 1) {
|
||||||
|
return hint.label ? 'oneWithLabel' : 'one';
|
||||||
|
}
|
||||||
|
return hint.label ? 'manyWithLabel' : 'many';
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue