Improve multiselect’s usability with keyboard handling
Had to replace the tags <ul> with a div with an input, so that the browser can focus the keywoard there. For now i do not have a focus-within CSS rule because we do no yet have a style for focus highlight. I have replaced the template for-loop to fill the options with the JavaScript equivalent for two reasons. The first is that GoLand is very stupid and can not handle that templating code inside the JavaScript function and complains of non-existing problemes all the time. The second is that, taking advantage of the input, i now have filtering of options and have to remove accents from the label and convert it to lowercase into a separate property just for that. I could do that with a Go function, but it is something that i also have to do for the input’s value when it changes, therefore i am forced to use JavaScript and, if i am already using it for one string, it makes no sense to have duplicate functionality in Go code. The control still has missing aria attributes, and the list of options is not yet navigable with the keyboard.
This commit is contained in:
parent
f93d557aa9
commit
1c9fe14ab9
|
@ -613,29 +613,6 @@ main > nav {
|
|||
min-width: 20rem;
|
||||
}
|
||||
|
||||
.multiselect ul + .placeholder {
|
||||
position: absolute;
|
||||
font-style: italic;
|
||||
pointer-events: none;
|
||||
font-size: 1em;
|
||||
background-color: initial;
|
||||
top: 1rem;
|
||||
left: 2rem;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.multiselect ul:not(.empty) + .placeholder {
|
||||
background-color: var(--numerus--background-color);
|
||||
top: -.9rem;
|
||||
left: 2rem;
|
||||
font-size: 0.8em;
|
||||
padding: 0 .25rem;
|
||||
}
|
||||
|
||||
.multiselect ul + .placeholder::after {
|
||||
content: " (optional)";
|
||||
}
|
||||
|
||||
.multiselect .tags:after {
|
||||
content: '';
|
||||
border-top: 6px solid black;
|
||||
|
@ -648,24 +625,38 @@ main > nav {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.multiselect .tags li {
|
||||
.multiselect .tag {
|
||||
background-color: var(--numerus--color--hay);
|
||||
}
|
||||
|
||||
.multiselect .tags button {
|
||||
.multiselect .tag button {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
min-width: initial;
|
||||
}
|
||||
|
||||
.multiselect .tags button:hover {
|
||||
.multiselect .tag button:hover {
|
||||
background: rgba(255, 255, 255, .4);
|
||||
}
|
||||
|
||||
.multiselect .tags input {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: none;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.multiselect .options {
|
||||
padding: 0;
|
||||
border-top: 0;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.multiselect .options li {
|
||||
|
|
|
@ -42,15 +42,75 @@
|
|||
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.SelectField*/ -}}
|
||||
<div class="input{{ if .Errors }} has-errors{{ end }}"
|
||||
{{- if .Multiple }}
|
||||
@focusin.window="! $el.contains($event.target) && close() "
|
||||
@click.away="close()"
|
||||
x-data="{
|
||||
options: [
|
||||
{{- range $option := .Options }}
|
||||
{ value: '{{ .Value }}', label: '{{ .Label }}', selected: {{ if $.IsSelected .Value }}true{{ else }}false{{ end }} },
|
||||
{{- end }}
|
||||
],
|
||||
options: [],
|
||||
name: '',
|
||||
filter: '',
|
||||
open: false,
|
||||
init() {
|
||||
$el.querySelector('select').remove();
|
||||
const select = $el.querySelector('select');
|
||||
this.name = select.name;
|
||||
for (const option of select.options) {
|
||||
this.options.push({
|
||||
value: option.value,
|
||||
label: option.innerText,
|
||||
search: this.normalize(option.innerText),
|
||||
selected: option.getAttribute('selected') !== null,
|
||||
});
|
||||
}
|
||||
select.remove();
|
||||
const template = $el.querySelector('template');
|
||||
const input = template.content.querySelector('input');
|
||||
const label = $el.querySelector('label');
|
||||
input.after(label);
|
||||
},
|
||||
selected() {
|
||||
return this.options.filter(option => option.selected);
|
||||
},
|
||||
normalize(s) {
|
||||
return s.normalize('NFD').replace(/\p{Diacritic}/gu, '').toLowerCase();
|
||||
},
|
||||
unselected() {
|
||||
const filter = this.normalize(this.filter);
|
||||
return this.options.filter(option => !option.selected && (filter === '' || option.search.includes(filter)));
|
||||
},
|
||||
empty() {
|
||||
return this.selected().length === 0;
|
||||
},
|
||||
selectOption(option) {
|
||||
option.selected = true;
|
||||
this.filter = '';
|
||||
},
|
||||
selectFirst() {
|
||||
if (!this.open) {
|
||||
return;
|
||||
}
|
||||
const all = this.unselected();
|
||||
if (all.length === 0) {
|
||||
return;
|
||||
}
|
||||
this.selectOption(all[0]);
|
||||
},
|
||||
unselectLast() {
|
||||
if (this.filter !== '') {
|
||||
return;
|
||||
}
|
||||
const all = this.selected();
|
||||
if (all.length === 0) {
|
||||
return;
|
||||
}
|
||||
all[all.length - 1].selected = false;
|
||||
},
|
||||
toggle() {
|
||||
if (this.open) {
|
||||
return this.close();
|
||||
}
|
||||
this.open = true;
|
||||
},
|
||||
close() {
|
||||
this.open = false;
|
||||
},
|
||||
}"
|
||||
{{- end -}}
|
||||
|
@ -82,21 +142,28 @@
|
|||
{{- if .Multiple }}
|
||||
<template x-if="true">
|
||||
<div class="multiselect">
|
||||
<ul class="tags" :class="{'empty': options.filter(option => option.selected).length === 0}"
|
||||
@click="open = !open" @click.away="open = false">
|
||||
<template x-for="(option) in options.filter(option => option.selected)">
|
||||
<li>
|
||||
<input type="hidden" name="{{ .Name }}" :value="option.value">
|
||||
<div class="tags" :class="{'empty': empty()}"
|
||||
@click="toggle()"
|
||||
>
|
||||
<template x-for="option in selected()">
|
||||
<div class="tag">
|
||||
<input type="hidden" :name="name" :value="option.value">
|
||||
<span x-text="option.label"></span>
|
||||
<button type="button" @click.stop="option.selected = false">×</button>
|
||||
</li>
|
||||
</div>
|
||||
</template>
|
||||
</ul>
|
||||
<input id="{{ .Name }}-field" :placeholder="empty() && '{{ .Label }}'" x-model="filter"
|
||||
@input="open = true"
|
||||
@keydown.escape="close()"
|
||||
@keydown.prevent.enter="selectFirst()"
|
||||
@keydown.backspace="unselectLast()"
|
||||
>
|
||||
</div>
|
||||
<ul class="options" x-show.transition="open">
|
||||
<template x-for="(option) in options.filter(option => !option.selected)">
|
||||
<template x-for="option in unselected()">
|
||||
<li
|
||||
x-text="option.label"
|
||||
@click.stop="option.selected = true"
|
||||
@click.stop="selectOption(option)"
|
||||
></li>
|
||||
</template>
|
||||
</ul>
|
||||
|
|
Loading…
Reference in New Issue