feat(app): polish select-v2 component (#30446)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
This commit is contained in:
@@ -1,87 +0,0 @@
|
||||
[data-component="select"][data-trigger-style="settings-v2"] [data-slot="select-select-trigger"] {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: fit-content;
|
||||
height: 24px;
|
||||
padding: 4px 4px 4px 8px;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--v2-text-text-base);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
[data-component="select"][data-trigger-style="settings-v2"] [data-slot="select-select-trigger-value"] {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
height: 13px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-style: normal;
|
||||
font-weight: 530;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.04px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-variation-settings: "slnt" 0;
|
||||
}
|
||||
|
||||
[data-component="select"][data-trigger-style="settings-v2"] [data-slot="select-select-trigger"]:focus-visible {
|
||||
outline: 2px solid var(--v2-border-border-focus);
|
||||
outline-offset: 2.5px;
|
||||
}
|
||||
|
||||
[data-component="select"][data-trigger-style="settings-v2"]
|
||||
[data-slot="select-select-trigger"]:is(:hover, [data-state="hover"]):not(:disabled) {
|
||||
background-color: var(--v2-overlay-simple-overlay-hover);
|
||||
}
|
||||
|
||||
[data-component="select"][data-trigger-style="settings-v2"]
|
||||
[data-slot="select-select-trigger"]:is(:active, [data-state="pressed"]):not(:disabled) {
|
||||
background-color: var(--v2-overlay-simple-overlay-pressed);
|
||||
}
|
||||
|
||||
[data-component="select"][data-trigger-style="settings-v2"] [data-slot="select-select-trigger"]:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
[data-component="select"][data-trigger-style="settings-v2"] [data-slot="select-select-trigger-icon"] {
|
||||
display: flex;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
[data-component="select"][data-trigger-style="settings-v2"]
|
||||
[data-slot="select-select-trigger-icon"]
|
||||
[data-slot="icon-svg"] {
|
||||
margin-inline: -5px;
|
||||
color: #3a3a3a;
|
||||
}
|
||||
|
||||
[data-component="select-content"][data-trigger-style="settings-v2"] {
|
||||
min-width: 160px;
|
||||
border-radius: 8px;
|
||||
padding: 0;
|
||||
|
||||
[data-slot="select-select-content-list"] {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
[data-slot="select-select-item"] {
|
||||
font-size: var(--font-size-base);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-large);
|
||||
letter-spacing: var(--letter-spacing-normal);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import * as mod from "./select-v2"
|
||||
import { create } from "../storybook/scaffold"
|
||||
|
||||
const story = create({
|
||||
title: "UI/SelectV2",
|
||||
mod,
|
||||
args: {
|
||||
options: ["One", "Two", "Three"],
|
||||
current: "One",
|
||||
placeholder: "Choose...",
|
||||
},
|
||||
})
|
||||
|
||||
export default {
|
||||
title: "UI/SelectV2",
|
||||
id: "components-select-v2",
|
||||
component: story.meta.component,
|
||||
tags: ["autodocs"],
|
||||
}
|
||||
|
||||
export const Basic = story.Basic
|
||||
@@ -1,171 +0,0 @@
|
||||
import { Select as Kobalte } from "@kobalte/core/select"
|
||||
import { createMemo, onCleanup, splitProps, type ComponentProps, type JSX } from "solid-js"
|
||||
import { pipe, groupBy, entries, map } from "remeda"
|
||||
import { Icon as IconV2 } from "../v2/components/icon"
|
||||
import { Icon } from "./icon"
|
||||
import "./select-v2.css"
|
||||
|
||||
export type SelectV2Props<T> = Omit<ComponentProps<typeof Kobalte<T>>, "value" | "onSelect" | "children"> & {
|
||||
placeholder?: string
|
||||
options: T[]
|
||||
current?: T
|
||||
value?: (x: T) => string
|
||||
label?: (x: T) => string
|
||||
groupBy?: (x: T) => string
|
||||
valueClass?: ComponentProps<"div">["class"]
|
||||
onSelect?: (value: T | undefined) => void
|
||||
onHighlight?: (value: T | undefined) => (() => void) | void
|
||||
class?: ComponentProps<"div">["class"]
|
||||
classList?: ComponentProps<"div">["classList"]
|
||||
children?: (item: T | undefined) => JSX.Element
|
||||
triggerStyle?: JSX.CSSProperties
|
||||
triggerProps?: Record<string, string | number | boolean | undefined>
|
||||
}
|
||||
|
||||
export function SelectV2<T>(props: SelectV2Props<T> & { disabled?: boolean }) {
|
||||
const [local, others] = splitProps(props, [
|
||||
"class",
|
||||
"classList",
|
||||
"placeholder",
|
||||
"options",
|
||||
"current",
|
||||
"value",
|
||||
"label",
|
||||
"groupBy",
|
||||
"valueClass",
|
||||
"onSelect",
|
||||
"onHighlight",
|
||||
"onOpenChange",
|
||||
"children",
|
||||
"triggerStyle",
|
||||
"triggerProps",
|
||||
])
|
||||
|
||||
const state = {
|
||||
key: undefined as string | undefined,
|
||||
cleanup: undefined as (() => void) | void,
|
||||
}
|
||||
|
||||
const stop = () => {
|
||||
state.cleanup?.()
|
||||
state.cleanup = undefined
|
||||
state.key = undefined
|
||||
}
|
||||
|
||||
const keyFor = (item: T) => (local.value ? local.value(item) : (item as string))
|
||||
|
||||
const move = (item: T | undefined) => {
|
||||
if (!local.onHighlight) return
|
||||
if (!item) {
|
||||
stop()
|
||||
return
|
||||
}
|
||||
|
||||
const key = keyFor(item)
|
||||
if (state.key === key) return
|
||||
state.cleanup?.()
|
||||
state.cleanup = local.onHighlight(item)
|
||||
state.key = key
|
||||
}
|
||||
|
||||
onCleanup(stop)
|
||||
|
||||
const grouped = createMemo(() => {
|
||||
const result = pipe(
|
||||
local.options,
|
||||
groupBy((x) => (local.groupBy ? local.groupBy(x) : "")),
|
||||
entries(),
|
||||
map(([k, v]) => ({ category: k, options: v })),
|
||||
)
|
||||
return result
|
||||
})
|
||||
|
||||
return (
|
||||
// @ts-ignore
|
||||
<Kobalte<T, { category: string; options: T[] }>
|
||||
{...others}
|
||||
data-component="select"
|
||||
data-trigger-style="settings-v2"
|
||||
placement="bottom-end"
|
||||
gutter={4}
|
||||
value={local.current}
|
||||
options={grouped()}
|
||||
optionValue={(x) => (local.value ? local.value(x) : (x as string))}
|
||||
optionTextValue={(x) => (local.label ? local.label(x) : (x as string))}
|
||||
optionGroupChildren="options"
|
||||
placeholder={local.placeholder}
|
||||
sectionComponent={(local) => (
|
||||
<Kobalte.Section data-slot="select-section">{local.section.rawValue.category}</Kobalte.Section>
|
||||
)}
|
||||
itemComponent={(itemProps) => (
|
||||
<Kobalte.Item
|
||||
{...itemProps}
|
||||
data-slot="select-select-item"
|
||||
classList={{
|
||||
...local.classList,
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
onPointerEnter={() => move(itemProps.item.rawValue)}
|
||||
onPointerMove={() => move(itemProps.item.rawValue)}
|
||||
onFocus={() => move(itemProps.item.rawValue)}
|
||||
>
|
||||
<Kobalte.ItemLabel data-slot="select-select-item-label">
|
||||
{local.children
|
||||
? local.children(itemProps.item.rawValue)
|
||||
: local.label
|
||||
? local.label(itemProps.item.rawValue)
|
||||
: (itemProps.item.rawValue as string)}
|
||||
</Kobalte.ItemLabel>
|
||||
<Kobalte.ItemIndicator data-slot="select-select-item-indicator">
|
||||
<Icon name="check-small" size="small" />
|
||||
</Kobalte.ItemIndicator>
|
||||
</Kobalte.Item>
|
||||
)}
|
||||
onChange={(v) => {
|
||||
local.onSelect?.(v ?? undefined)
|
||||
stop()
|
||||
}}
|
||||
onOpenChange={(open) => {
|
||||
local.onOpenChange?.(open)
|
||||
if (!open) stop()
|
||||
}}
|
||||
>
|
||||
<Kobalte.Trigger
|
||||
{...local.triggerProps}
|
||||
type="button"
|
||||
disabled={props.disabled}
|
||||
data-slot="select-select-trigger"
|
||||
as="button"
|
||||
style={local.triggerStyle}
|
||||
classList={{
|
||||
...local.classList,
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
>
|
||||
<Kobalte.Value<T> data-slot="select-select-trigger-value" class={local.valueClass}>
|
||||
{(state) => {
|
||||
const selected = state.selectedOption() ?? local.current
|
||||
if (!selected) return local.placeholder || ""
|
||||
if (local.label) return local.label(selected)
|
||||
return selected as string
|
||||
}}
|
||||
</Kobalte.Value>
|
||||
<Kobalte.Icon data-slot="select-select-trigger-icon">
|
||||
<IconV2 name="chevron-down" class="-mx-[5px]" />
|
||||
</Kobalte.Icon>
|
||||
</Kobalte.Trigger>
|
||||
<Kobalte.Portal>
|
||||
<Kobalte.Content
|
||||
classList={{
|
||||
...local.classList,
|
||||
[local.class ?? ""]: !!local.class,
|
||||
}}
|
||||
data-component="select-content"
|
||||
data-trigger-style="settings-v2"
|
||||
>
|
||||
<Kobalte.Listbox data-slot="select-select-content-list" />
|
||||
</Kobalte.Content>
|
||||
</Kobalte.Portal>
|
||||
</Kobalte>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user