select.tsx 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. import { Select as Kobalte } from "@kobalte/core/select"
  2. import { createMemo, splitProps, type ComponentProps, type JSX } from "solid-js"
  3. import { pipe, groupBy, entries, map } from "remeda"
  4. import { Button, ButtonProps } from "./button"
  5. import { Icon } from "./icon"
  6. export type SelectProps<T> = Omit<ComponentProps<typeof Kobalte<T>>, "value" | "onSelect" | "children"> & {
  7. placeholder?: string
  8. options: T[]
  9. current?: T
  10. value?: (x: T) => string
  11. label?: (x: T) => string
  12. groupBy?: (x: T) => string
  13. onSelect?: (value: T | undefined) => void
  14. class?: ComponentProps<"div">["class"]
  15. classList?: ComponentProps<"div">["classList"]
  16. children?: (item: T | undefined) => JSX.Element
  17. }
  18. export function Select<T>(props: SelectProps<T> & ButtonProps) {
  19. const [local, others] = splitProps(props, [
  20. "class",
  21. "classList",
  22. "placeholder",
  23. "options",
  24. "current",
  25. "value",
  26. "label",
  27. "groupBy",
  28. "onSelect",
  29. "children",
  30. ])
  31. const grouped = createMemo(() => {
  32. const result = pipe(
  33. local.options,
  34. groupBy((x) => (local.groupBy ? local.groupBy(x) : "")),
  35. // mapValues((x) => x.sort((a, b) => a.title.localeCompare(b.title))),
  36. entries(),
  37. map(([k, v]) => ({ category: k, options: v })),
  38. )
  39. return result
  40. })
  41. return (
  42. // @ts-ignore
  43. <Kobalte<T, { category: string; options: T[] }>
  44. {...others}
  45. data-component="select"
  46. placement="bottom-start"
  47. value={local.current}
  48. options={grouped()}
  49. optionValue={(x) => (local.value ? local.value(x) : (x as string))}
  50. optionTextValue={(x) => (local.label ? local.label(x) : (x as string))}
  51. optionGroupChildren="options"
  52. placeholder={local.placeholder}
  53. sectionComponent={(local) => (
  54. <Kobalte.Section data-slot="select-section">{local.section.rawValue.category}</Kobalte.Section>
  55. )}
  56. itemComponent={(itemProps) => (
  57. <Kobalte.Item
  58. data-slot="select-select-item"
  59. classList={{
  60. ...(local.classList ?? {}),
  61. [local.class ?? ""]: !!local.class,
  62. }}
  63. {...itemProps}
  64. >
  65. <Kobalte.ItemLabel data-slot="select-select-item-label">
  66. {local.children
  67. ? local.children(itemProps.item.rawValue)
  68. : local.label
  69. ? local.label(itemProps.item.rawValue)
  70. : (itemProps.item.rawValue as string)}
  71. </Kobalte.ItemLabel>
  72. <Kobalte.ItemIndicator data-slot="select-select-item-indicator">
  73. <Icon name="check-small" size="small" />
  74. </Kobalte.ItemIndicator>
  75. </Kobalte.Item>
  76. )}
  77. onChange={(v) => {
  78. local.onSelect?.(v ?? undefined)
  79. }}
  80. >
  81. <Kobalte.Trigger
  82. disabled={props.disabled}
  83. data-slot="select-select-trigger"
  84. as={Button}
  85. size={props.size}
  86. variant={props.variant}
  87. classList={{
  88. ...(local.classList ?? {}),
  89. [local.class ?? ""]: !!local.class,
  90. }}
  91. >
  92. <Kobalte.Value<T> data-slot="select-select-trigger-value">
  93. {(state) => {
  94. const selected = state.selectedOption() ?? local.current
  95. if (!selected) return local.placeholder || ""
  96. if (local.label) return local.label(selected)
  97. return selected as string
  98. }}
  99. </Kobalte.Value>
  100. <Kobalte.Icon data-slot="select-select-trigger-icon">
  101. <Icon name="chevron-down" size="small" />
  102. </Kobalte.Icon>
  103. </Kobalte.Trigger>
  104. <Kobalte.Portal>
  105. <Kobalte.Content
  106. classList={{
  107. ...(local.classList ?? {}),
  108. [local.class ?? ""]: !!local.class,
  109. }}
  110. data-component="select-content"
  111. >
  112. <Kobalte.Listbox data-slot="select-select-content-list" />
  113. </Kobalte.Content>
  114. </Kobalte.Portal>
  115. </Kobalte>
  116. )
  117. }