summaryrefslogtreecommitdiffhomepage
path: root/cloud/web/src/ui/dialog-select.tsx
blob: 087b94411f0f4fb81766ea15e28d0997227125ae (plain)
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
import style from "./dialog-select.module.css"
import { z } from "zod"
import { createMemo, createSignal, For, JSX, onMount } from "solid-js"
import { createList } from "solid-list"
import { createDialog } from "./context-dialog"

export const DialogSelect = createDialog({
  size: "md",
  schema: z.object({
    title: z.string(),
    placeholder: z.string(),
    onSelect: z
      .function(z.tuple([z.any()]))
      .returns(z.void())
      .optional(),
    options: z.array(
      z.object({
        display: z.string(),
        value: z.any().optional(),
        onSelect: z.function().returns(z.void()).optional(),
        prefix: z.custom<JSX.Element>().optional(),
      }),
    ),
  }),
  render: (ctx) => {
    let input: HTMLInputElement
    onMount(() => {
      input.focus()
      input.value = ""
    })

    const [filter, setFilter] = createSignal("")
    const filtered = createMemo(() =>
      ctx.input.options?.filter((i) =>
        i.display.toLowerCase().includes(filter().toLowerCase()),
      ),
    )
    const list = createList({
      loop: true,
      initialActive: 0,
      items: () => filtered().map((_, i) => i),
      handleTab: false,
    })

    const handleSelection = (index: number) => {
      const option = ctx.input.options[index]

      // If the option has its own onSelect handler, use it
      if (option.onSelect) {
        option.onSelect()
      }
      // Otherwise, if there's a global onSelect handler, call it with the option's value
      else if (ctx.input.onSelect) {
        ctx.input.onSelect(
          option.value !== undefined ? option.value : option.display,
        )
      }
    }

    return (
      <>
        <div data-slot="header">
          <label
            data-size="md"
            data-slot="title"
            data-component="label"
            for={`dialog-select-${ctx.input.title}`}
          >
            {ctx.input.title}
          </label>
        </div>
        <div data-slot="main">
          <input
            data-size="lg"
            data-component="input"
            value={filter()}
            onInput={(e) => {
              setFilter(e.target.value)
              list.setActive(0)
            }}
            onKeyDown={(e) => {
              if (e.key === "Enter") {
                const selected = list.active()
                if (selected === null) return
                handleSelection(selected)
                return
              }
              if (e.key === "Escape") {
                setFilter("")
                return
              }
              list.onKeyDown(e)
            }}
            id={`dialog-select-${ctx.input.title}`}
            ref={(r) => (input = r)}
            data-slot="input"
            placeholder={ctx.input.placeholder}
          />
        </div>
        <div data-slot="options" class={style.options}>
          <For
            each={filtered()}
            fallback={
              <div data-slot="option" data-empty>
                No results
              </div>
            }
          >
            {(option, index) => (
              <div
                onClick={() => handleSelection(index())}
                data-slot="option"
                data-active={list.active() === index() ? true : undefined}
              >
                {option.prefix && <div data-slot="prefix">{option.prefix}</div>}
                <div data-slot="title">{option.display}</div>
              </div>
            )}
          </For>
        </div>
      </>
    )
  },
})