x
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<div contents data-controller="dialog"> <button type="button" class="btn" data-action="dialog#showModal">Show command</button> <dialog class="dialog" style="--dialog-size: 24rem;" data-dialog-target="content" data-action="click->dialog#closeOnClickOutside"> <div class="command border-0" data-controller="command" data-action="combobox-commit->command#commit"> <label class="command__input input input--actor flex items-center gap"> <span class="icon icon--search" aria-hidden="true"></span> <input type="text" name="search" id="search" data-command-target="input" data-action="command#filter" autofocus="autofocus" autocomplete="off" placeholder="Filter..." /> </label> <div class="command__list" data-command-target="list" role="listbox" aria-label="Commands"> <div class="command__group" role="group" aria-labelledby="suggestions"> <span id="suggestions" class="command__group-header">Suggestions</span> <div id="ddc01dc3b9ffcbfbdb4907cd3da58d68" class="btn command__item" data-command-target="item" data-value="calendar" data-href="#" role="option"> <span class="icon icon--calendar" aria-hidden="true"></span> <span>Calendar</span> </div> <div id="3b27f3fb1e4d672ea795e3217408f674" class="btn command__item" data-command-target="item" data-value="search emoji" data-href="#" role="option"> <span class="icon icon--smile" aria-hidden="true"></span> <span>Seach Emoji</span> </div> <div id="c22db06fba3706dc466962befbeef9cb" class="btn command__item" data-command-target="item" data-value="calculator" data-href="#" aria-disabled="true" role="option"> <span class="icon icon--calculator" aria-hidden="true"></span> <span>Calculator</span> </div> </div> <div class="command__group" role="group" aria-labelledby="settings"> <span id="settings" class="command__group-header">Settings</span> <div id="8a53f5d3f6f8fc32eebfbeede35d2c4d" class="btn command__item" data-command-target="item" data-value="profile" data-href="#" role="option"> <span class="icon icon--user" aria-hidden="true"></span> <span>Profile</span> <span class="command__item-key">⌘P</span> </div> <div id="5e98ca38b0c47299cb99cd4eb0575d42" class="btn command__item" data-command-target="item" data-value="billing" data-href="#" role="option"> <span class="icon icon--credit-card" aria-hidden="true"></span> <span>Billing</span> <span class="command__item-key">⌘B</span> </div> <div id="6fbfdc72f5525ac3ebbe6c5312bf0609" class="btn command__item" data-command-target="item" data-value="settings" data-href="#" role="option"> <span class="icon icon--settings" aria-hidden="true"></span> <span>Settings</span> <span class="command__item-key">⌘P</span> </div> </div> </div> </div> </dialog></div>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div contents data-controller="dialog"> <button type="button" class="btn" data-action="dialog#showModal">Show command</button> <dialog class="dialog" style="--dialog-size: 24rem;" data-dialog-target="content" data-action="click->dialog#closeOnClickOutside"> <div class="command border-0" data-controller="command" data-action="combobox-commit->command#commit"> <label class="command__input input input--actor flex items-center gap"> <span class="icon icon--search" aria-hidden="true"></span> <%= text_field_tag "search", nil, data: { command_target: "input", action: "command#filter" }, autofocus: true, autocomplete: "off", placeholder: "Filter..." %> </label> <div class="command__list" data-command-target="list" role="listbox" aria-label="Commands"> <%= render "command_options" %> </div> </div> </dialog></div>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
.command { background-color: var(--color-surface); border-radius: var(--rounded-xl); border-width: var(--border); box-shadow: var(--shadow-xs); display: flex; padding: var(--size-2); flex-direction: column;}.command__input { --input-background: var(--color-border-light); --input-box-shadow: none; --input-outline-size: 0;}.command__list { max-block-size: 300px; overflow-y: auto; &:not(:has(.command__item:not([hidden]))) { display: none; }}.command__group:not(:first-child) { border-block-start-width: var(--border);}.command__group { display: flex; flex-direction: column; padding-block: var(--size-1); row-gap: 1px; &:not(:has(.command__item:not([hidden]))) { display: none; }}.command__group-header { color: var(--color-text-subtle); font-size: var(--text-xs); font-weight: var(--font-medium); padding: var(--size-1) var(--size-1_5);}.command__item { --btn-border-color: transparent; --btn-box-shadow: none; --btn-font-weight: var(--font-normal); --btn-justify-content: start; --btn-outline-size: 0; --btn-padding: var(--size-1) var(--size-1_5); --btn-radius: var(--rounded-sm); &[aria-selected="true"] { --btn-background: var(--color-border-light); }}.command__item-key { color: var(--color-text-subtle); font-size: var(--text-xs); margin-inline-start: auto;}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import { Controller } from "@hotwired/stimulus"import Combobox from "https://esm.sh/@github/combobox-nav@3.0.1?standalone"import debounce from "https://esm.sh/just-debounce-it@3.2.0?standalone"export default class extends Controller { static targets = [ "input", "list", "item" ] initialize() { this.filter = debounce(this.filter.bind(this), 100) } connect() { this.combobox = new Combobox(this.inputTarget, this.listTarget) this.combobox.start() } disconnect() { this.combobox.destroy() } filter({ target }) { this.itemTargets.forEach(it => this.#matches(it.dataset.value, target.value) ? it.removeAttribute("hidden") : it.toggleAttribute("hidden", true)) } commit({ target }) { Turbo.visit(target.dataset.href, { action: "advance" }) } #matches(text, potentialMatch) { return text.toLowerCase().includes(potentialMatch.toLowerCase()) }}