-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Listbox and ListboxItem components (#53)
* Add Listbox and ListboxItem components Implemented Listbox and ListboxItem components, along with their styling. These components provide interactive lists for user selection. Also added corresponding storybook stories and tests. Dependency packages for solid-virtual were installed to support virtualization within the listbox. * Add new features to Listbox component This commit introduces new functionalities to the Listbox component. Properties like 'size', 'theme', and 'bordered' have been added providing more customization options. Additionally, a 'toPx' function has been implemented to simplify CSS value conversions. This function is used for more precise size values for the Listbox items. Storybook stories have also been updated to accommodate these changes. * Improve and extend Listbox components This commit brings enhancements to the Listbox component by introducing the 'shouldFocusOnHover' prop and refining the typing of VirtualizedListboxProps. The modifications promote better customization of the list boxes and lay groundwork for more sophisticated property handling mechanisms. Furthermore, the diffs include the addition of the 'splitProps' function to separate 'options' and 'itemLabel', thereby refining local component settings. * Remove ts-expect-error comments from Storybook stories This commit removes ts-expect-error comments in multiple Storybook stories, including TextArea, TextField, Select, and Listbox. The removal suggests that previously existing typescript errors have been resolved. Also, it includes type correction in Listbox story where options are casted to string array. * Update Storybook scripts and resolve TypeScript errors This commit updates the scripts in the Storybook package.json to use simpler calls and removes ts-expect-error comments in multiple Storybook stories, thereby acknowledging the resolution of TypeScript errors. The build command in Netlify is also adjusted. Further, it includes dependency library updates in the lock file. * Downgrade TypeScript version in Storybook package This commit downgrades the TypeScript version in the Storybook package from ^5.0.2 to ^4.9.5. The change also updates the TypeScript * Revert to previous package versions in pnpm-lock.yaml This commit downgrades several packages within the pnpm-lock.yaml file to their preceding versions. This was necessary to resolve * Enhanced package dependencies and adjusted TypeScript version Applied a patch to the @storybook/manager-api package for issue resolution, and upgraded TypeScript from 4.9.5 to 5.0.4 across various sections of the code. This update ensures compatibility and leverages the latest TypeScript enhancements. * Adjust Listbox stories * Adjust Listbox stories * Create metal-doors-mate.md
- Loading branch information
1 parent
7883a91
commit fea16ba
Showing
23 changed files
with
666 additions
and
86 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
"@codeui/kit": patch | ||
"@codeui/storybook-playground": patch | ||
--- | ||
|
||
Add Listbox and ListboxItem components |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
import { createTheme, style } from "@vanilla-extract/css"; | ||
import { tokens } from "../../foundation/contract.css"; | ||
import { themeTokens, themeVars } from "../../foundation"; | ||
import { componentStateStyles } from "@kobalte/vanilla-extract"; | ||
import { LISTBOX_ITEM_SIZE } from "./sizes"; | ||
import { toPx } from "../../utils/css"; | ||
|
||
export const [listTheme, listThemeVars] = createTheme({ | ||
contentBackground: tokens.dropdownBackground, | ||
contentRadius: themeTokens.radii.lg, | ||
contentBoxShadow: tokens.dropdownBoxShadow, | ||
contentPadding: themeTokens.spacing["2"], | ||
contentBorderColor: tokens.dropdownBorder, | ||
contentMaxHeight: "400px", | ||
contentMaxHeightXs: "270px", | ||
separator: tokens.dropdownBorder, | ||
itemMinHeight: "2.60rem", | ||
itemTextColor: tokens.dropdownItemTextColor, | ||
itemHoverBackground: tokens.dropdownItemHoverBackground, | ||
itemHoverTextColor: tokens.dropdownItemHoverTextColor, | ||
itemDisabledOpacity: ".4", | ||
indicatorSize: "20px", | ||
}); | ||
|
||
const ButtonSizes = { | ||
xs: "xs", | ||
sm: "sm", | ||
md: "md", | ||
lg: "lg", | ||
xl: "xl", | ||
} as const; | ||
|
||
export const list = style([ | ||
listTheme, | ||
{ | ||
borderRadius: themeTokens.radii.sm, | ||
selectors: { | ||
"&[data-bordered]": { | ||
padding: themeTokens.spacing["2"], | ||
border: `1px solid ${themeVars.separator}`, | ||
}, | ||
"&[data-theme=primary]": { | ||
vars: { | ||
[listThemeVars.itemTextColor]: tokens.dropdownItemTextColor, | ||
[listThemeVars.itemHoverBackground]: tokens.brandAccentHover, | ||
[listThemeVars.itemHoverTextColor]: tokens.dropdownItemHoverTextColor, | ||
}, | ||
}, | ||
"&[data-theme=neutral]": { | ||
vars: { | ||
[listThemeVars.itemTextColor]: tokens.dropdownItemTextColor, | ||
[listThemeVars.itemHoverBackground]: tokens.dropdownItemHoverBackground, | ||
[listThemeVars.itemHoverTextColor]: tokens.dropdownItemHoverTextColor, | ||
}, | ||
}, | ||
}, | ||
}, | ||
]); | ||
|
||
/** | ||
* TODO: same as select! | ||
*/ | ||
export const item = style([ | ||
{ | ||
textAlign: "left", | ||
justifyContent: "space-between", | ||
border: 0, | ||
padding: `${themeTokens.spacing["2"]} ${themeTokens.spacing["3"]}`, | ||
borderRadius: themeTokens.radii.sm, | ||
background: "transparent", | ||
color: listThemeVars.itemTextColor, | ||
userSelect: "none", | ||
display: "flex", | ||
alignItems: "center", | ||
outline: "none", | ||
fontWeight: themeTokens.fontWeight.normal, | ||
transition: "opacity .2s, background-color .2s, transform .2s", | ||
gap: themeTokens.spacing["2"], | ||
margin: `${themeTokens.spacing["1"]} 0`, | ||
minHeight: listThemeVars.itemMinHeight, | ||
selectors: { | ||
"&:first-child,&:last-child": { | ||
margin: 0, | ||
}, | ||
}, | ||
}, | ||
{ | ||
selectors: { | ||
[`&[data-size=${ButtonSizes.xs}]`]: { | ||
height: toPx(LISTBOX_ITEM_SIZE.xs), | ||
fontSize: themeTokens.fontSize.sm, | ||
borderRadius: themeTokens.radii.xs, | ||
minHeight: 0, | ||
}, | ||
[`&[data-size=${ButtonSizes.sm}]`]: { | ||
height: toPx(LISTBOX_ITEM_SIZE.sm), | ||
fontSize: themeTokens.fontSize.md, | ||
minHeight: 0, | ||
}, | ||
[`&[data-size=${ButtonSizes.md}]`]: { | ||
height: toPx(LISTBOX_ITEM_SIZE.md), | ||
fontSize: themeTokens.fontSize.md, | ||
}, | ||
}, | ||
}, | ||
{ | ||
":disabled": { | ||
opacity: listThemeVars.itemDisabledOpacity, | ||
}, | ||
":focus": { | ||
boxShadow: "none", | ||
outline: "none", | ||
backgroundColor: listThemeVars.itemHoverBackground, | ||
color: listThemeVars.itemHoverTextColor, | ||
}, | ||
":focus-visible": { | ||
backgroundColor: listThemeVars.itemHoverBackground, | ||
color: listThemeVars.itemHoverTextColor, | ||
}, | ||
}, | ||
componentStateStyles({ | ||
highlighted: { | ||
boxShadow: "none", | ||
outline: "none", | ||
backgroundColor: listThemeVars.itemHoverBackground, | ||
color: listThemeVars.itemHoverTextColor, | ||
}, | ||
disabled: { | ||
opacity: listThemeVars.itemDisabledOpacity, | ||
not: { | ||
":hover": {}, | ||
}, | ||
}, | ||
}), | ||
]); | ||
|
||
export const itemIndicator = style({ | ||
marginLeft: "auto", | ||
height: listThemeVars.indicatorSize, | ||
width: listThemeVars.indicatorSize, | ||
strokeDashoffset: 32, | ||
selectors: { | ||
[`${item}[data-selected] &`]: { | ||
strokeDashoffset: 0, | ||
}, | ||
[`&[data-size=${ButtonSizes.xs}]`]: { | ||
vars: { | ||
[listThemeVars.indicatorSize]: "14px", | ||
}, | ||
}, | ||
[`&[data-size=${ButtonSizes.sm}]`]: { | ||
vars: { | ||
[listThemeVars.indicatorSize]: "18px", | ||
}, | ||
}, | ||
[`&[data-size=${ButtonSizes.md}]`]: { | ||
vars: { | ||
[listThemeVars.indicatorSize]: "20px", | ||
}, | ||
}, | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { JSXElement, splitProps } from "solid-js"; | ||
import { Listbox as KListbox } from "@kobalte/core"; | ||
import { CheckIcon } from "../../icons"; | ||
import { mergeClasses } from "../../utils/css"; | ||
import * as styles from "./Listbox.css"; | ||
|
||
export type ListboxProps<Option, OptGroup> = Omit< | ||
KListbox.ListboxRootProps<Option, OptGroup>, | ||
"renderItem" | ||
> & { | ||
size?: "xs" | "sm" | "md"; | ||
theme?: "primary" | "neutral"; | ||
itemLabel?: (item: Option) => JSXElement; | ||
bordered?: boolean; | ||
}; | ||
|
||
export function Listbox<Option, OptGroup = never>(props: ListboxProps<Option, OptGroup>) { | ||
const [local, others] = splitProps(props, [ | ||
"class", | ||
"size", | ||
"itemLabel", | ||
"bordered", | ||
"theme", | ||
]); | ||
|
||
return ( | ||
<KListbox.Root | ||
data-bordered={local.bordered ? "" : undefined} | ||
data-theme={local.theme ?? "neutral"} | ||
class={mergeClasses(styles.list)} | ||
shouldFocusOnHover | ||
renderItem={node => ( | ||
<ListboxItem itemLabel={local.itemLabel} size={local.size} item={node} /> | ||
)} | ||
{...others} | ||
/> | ||
); | ||
} | ||
|
||
export function ListboxItem<T>( | ||
props: KListbox.ListboxItemProps & { | ||
size?: "xs" | "sm" | "md"; | ||
itemLabel?: (item: T) => JSXElement; | ||
}, | ||
) { | ||
const [local, others] = splitProps(props, ["size", "itemLabel"]); | ||
return ( | ||
<KListbox.Item data-size={props.size ?? undefined} class={styles.item} {...others}> | ||
<KListbox.ItemLabel> | ||
{local.itemLabel ? local.itemLabel(others.item.rawValue) : others.item.rawValue} | ||
</KListbox.ItemLabel> | ||
<KListbox.ItemIndicator forceMount> | ||
<CheckIcon data-size={props.size ?? undefined} class={styles.itemIndicator} /> | ||
</KListbox.ItemIndicator> | ||
</KListbox.Item> | ||
); | ||
} |
111 changes: 111 additions & 0 deletions
111
packages/kit/src/components/Listbox/VirtualizedListbox.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import { createVirtualizer } from "@tanstack/solid-virtual"; | ||
import { Listbox, ListboxItem, ListboxProps } from "./Listbox"; | ||
import { For, splitProps } from "solid-js"; | ||
import { LISTBOX_ITEM_SIZE } from "./sizes"; | ||
|
||
type VirtualizedListboxProps<OptGroup> = Omit< | ||
ListboxProps<Item, OptGroup>, | ||
| "virtualized" | ||
| "children" | ||
| "options" | ||
| "optionValue" | ||
| "optionDisabled" | ||
| "optionLabel" | ||
| "optionTextValue" | ||
| "scrollToItem" | ||
> & { | ||
options: Item[]; | ||
virtualizerOptions?: { | ||
estimateSize?: (index: number) => number; | ||
enableSmoothScroll?: false; | ||
overscan?: number; | ||
}; | ||
}; | ||
|
||
interface Item { | ||
value: string; | ||
label: string; | ||
disabled?: boolean; | ||
} | ||
|
||
export function VirtualizedListbox<OptGroup = never>( | ||
props: VirtualizedListboxProps<OptGroup>, | ||
) { | ||
let listboxRef: HTMLUListElement | undefined; | ||
|
||
const virtualizer = createVirtualizer<HTMLUListElement | undefined, Item>({ | ||
get count() { | ||
return props.options.length; | ||
}, | ||
get enableSmoothScroll() { | ||
return props.virtualizerOptions?.enableSmoothScroll ?? false; | ||
}, | ||
get overscan() { | ||
return props.virtualizerOptions?.overscan ?? 5; | ||
}, | ||
getScrollElement: () => listboxRef, | ||
estimateSize: (index: number) => | ||
// TODO: fix that size | ||
props.virtualizerOptions?.estimateSize?.(index) ?? | ||
LISTBOX_ITEM_SIZE[props.size ?? "md"], | ||
// TODO: why error? | ||
// @ts-ignore | ||
getItemKey: (index: number) => { | ||
return props.options[index].value; | ||
}, | ||
}); | ||
|
||
const [local, others] = splitProps(props, ["options", "itemLabel"]); | ||
|
||
return ( | ||
// TODO fix type | ||
<Listbox<any> | ||
options={local.options} | ||
optionValue="value" | ||
optionTextValue="label" | ||
optionDisabled="disabled" | ||
ref={listboxRef} | ||
scrollToItem={key => | ||
virtualizer.scrollToIndex(props.options.findIndex(option => option.value === key)) | ||
} | ||
virtualized | ||
{...others} | ||
> | ||
{items => ( | ||
<div | ||
style={{ | ||
height: `${virtualizer.getTotalSize()}px`, | ||
width: "100%", | ||
position: "relative", | ||
}} | ||
> | ||
<For each={virtualizer.getVirtualItems()}> | ||
{virtualRow => { | ||
// TODO what if key is not string? | ||
const item = items().getItem(virtualRow.key as string); | ||
if (item) { | ||
return ( | ||
<ListboxItem<Item> | ||
size={props.size} | ||
item={item} | ||
itemLabel={item => { | ||
return local.itemLabel ? local.itemLabel(item) : item.label; | ||
}} | ||
style={{ | ||
position: "absolute", | ||
top: 0, | ||
left: 0, | ||
width: "100%", | ||
height: `${virtualRow.size}px`, | ||
transform: `translateY(${virtualRow.start}px)`, | ||
}} | ||
/> | ||
); | ||
} | ||
}} | ||
</For> | ||
</div> | ||
)} | ||
</Listbox> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export const LISTBOX_ITEM_SIZE: Record<ListboxItemSizeKey, number> = { | ||
xs: 30, | ||
sm: 36, | ||
md: 40, | ||
}; | ||
|
||
export type ListboxItemSizeKey = "xs" | "sm" | "md"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.