fix(dashboard,ui): Fixes to Combobox and CategoryCombobox (#9537)
**What** - Fixes the Combobox to keep the width of the content constant. - Brings CategoryCombobox inline with the other Combobox component - Adds keyboard navigation to the CategoryCombobox: You can now navigate options using ArrowUp and ArrowDown, and if an option has children you can use ArrowRight to see the children options. - Add "outline-none" to the Drawer component to stop it from flashing whenever focus is dropped. - Removes a dependency that was added to the UI package by mistake Resolves CC-155
This commit is contained in:
committed by
GitHub
parent
93b38bf47b
commit
1f682daf5c
@@ -18,6 +18,7 @@ import { clx, Text } from "@medusajs/ui"
|
||||
import { matchSorter } from "match-sorter"
|
||||
import {
|
||||
ComponentPropsWithoutRef,
|
||||
CSSProperties,
|
||||
ForwardedRef,
|
||||
Fragment,
|
||||
ReactNode,
|
||||
@@ -41,6 +42,9 @@ type ComboboxOption = {
|
||||
|
||||
type Value = string[] | string
|
||||
|
||||
const TABLUAR_NUM_WIDTH = 8
|
||||
const TAG_BASE_WIDTH = 28
|
||||
|
||||
interface ComboboxProps<T extends Value = Value>
|
||||
extends Omit<ComponentPropsWithoutRef<"input">, "onChange" | "value"> {
|
||||
value?: T
|
||||
@@ -195,6 +199,17 @@ const ComboboxImpl = <T extends Value = string>(
|
||||
|
||||
const hidePlaceholder = showSelected || open
|
||||
|
||||
const tagWidth = useMemo(() => {
|
||||
if (!Array.isArray(selectedValues)) {
|
||||
return TAG_BASE_WIDTH + TABLUAR_NUM_WIDTH // There can only be a single digit
|
||||
}
|
||||
|
||||
const count = selectedValues.length
|
||||
const digits = count.toString().length
|
||||
|
||||
return TAG_BASE_WIDTH + digits * TABLUAR_NUM_WIDTH
|
||||
}, [selectedValues])
|
||||
|
||||
const results = useMemo(() => {
|
||||
return isSearchControlled ? options : matches
|
||||
}, [matches, options, isSearchControlled])
|
||||
@@ -213,41 +228,42 @@ const ComboboxImpl = <T extends Value = string>(
|
||||
<div
|
||||
className={clx(
|
||||
"relative flex cursor-pointer items-center gap-x-2 overflow-hidden",
|
||||
"h-8 w-full rounded-md px-2 py-0.5",
|
||||
"h-8 w-full rounded-md",
|
||||
"bg-ui-bg-field transition-fg shadow-borders-base",
|
||||
"hover:bg-ui-bg-field-hover",
|
||||
"has-[input:focus]:shadow-borders-interactive-with-active",
|
||||
"has-[:invalid]:shadow-borders-error has-[[aria-invalid=true]]:shadow-borders-error",
|
||||
"has-[:disabled]:bg-ui-bg-disabled has-[:disabled]:text-ui-fg-disabled has-[:disabled]:cursor-not-allowed",
|
||||
{
|
||||
"pl-0.5": hasValue && isArrayValue,
|
||||
},
|
||||
className
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--tag-width": `${tagWidth}px`,
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
{showTag && (
|
||||
<div className="bg-ui-bg-base txt-compact-small-plus text-ui-fg-subtle focus-within:border-ui-fg-interactive relative flex h-[28px] items-center rounded-[4px] border py-[3px] pl-1.5 pr-1">
|
||||
<span>{selectedValues.length}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="size-fit outline-none"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
handleValueChange(undefined)
|
||||
}}
|
||||
>
|
||||
<XMarkMini className="text-ui-fg-muted" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
handleValueChange(undefined)
|
||||
}}
|
||||
className="bg-ui-bg-base hover:bg-ui-bg-base-hover txt-compact-small-plus text-ui-fg-subtle focus-within:border-ui-fg-interactive transition-fg absolute left-0.5 top-0.5 z-[1] flex h-[28px] items-center rounded-[4px] border py-[3px] pl-1.5 pr-1 outline-none"
|
||||
>
|
||||
<span className="tabular-nums">{selectedValues.length}</span>
|
||||
<XMarkMini className="text-ui-fg-muted" />
|
||||
</button>
|
||||
)}
|
||||
<div className="relative flex size-full items-center">
|
||||
{showSelected && (
|
||||
<Text size="small" leading="compact">
|
||||
{t("general.selected")}
|
||||
</Text>
|
||||
<div className="pointer-events-none absolute inset-y-0 left-[calc(var(--tag-width)+8px)] flex size-full items-center">
|
||||
<Text size="small" leading="compact">
|
||||
{t("general.selected")}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
{hideInput && (
|
||||
<div className="absolute inset-y-0 left-0 flex size-full items-center overflow-hidden">
|
||||
<div className="pointer-events-none absolute inset-y-0 left-[calc(var(--tag-width)+8px)] flex size-full items-center overflow-hidden">
|
||||
<Text size="small" leading="compact" className="truncate">
|
||||
{selectedLabel}
|
||||
</Text>
|
||||
@@ -256,10 +272,14 @@ const ComboboxImpl = <T extends Value = string>(
|
||||
<PrimitiveCombobox
|
||||
autoSelect
|
||||
ref={comboboxRef}
|
||||
onFocus={() => setOpen(true)}
|
||||
className={clx(
|
||||
"txt-compact-small text-ui-fg-base placeholder:text-ui-fg-subtle size-full cursor-pointer bg-transparent pr-7 outline-none focus:cursor-text",
|
||||
"txt-compact-small text-ui-fg-base placeholder:text-ui-fg-subtle transition-fg size-full cursor-pointer bg-transparent pl-2 pr-8 outline-none focus:cursor-text",
|
||||
"hover:bg-ui-bg-field-hover",
|
||||
{
|
||||
"opacity-0": hideInput,
|
||||
"pl-2": !showTag,
|
||||
"pl-[calc(var(--tag-width)+8px)]": showTag,
|
||||
}
|
||||
)}
|
||||
placeholder={hidePlaceholder ? undefined : placeholder}
|
||||
@@ -267,11 +287,12 @@ const ComboboxImpl = <T extends Value = string>(
|
||||
/>
|
||||
</div>
|
||||
<PrimitiveComboboxDisclosure
|
||||
render={() => {
|
||||
render={(props) => {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
type="button"
|
||||
className="text-ui-fg-muted pointer-events-none absolute right-2 size-fit outline-none"
|
||||
className="text-ui-fg-muted transition-fg hover:bg-ui-bg-field-hover absolute right-0 flex size-8 items-center justify-center rounded-r outline-none"
|
||||
>
|
||||
<TrianglesMini />
|
||||
</button>
|
||||
@@ -281,10 +302,11 @@ const ComboboxImpl = <T extends Value = string>(
|
||||
</div>
|
||||
<PrimitiveComboboxPopover
|
||||
gutter={4}
|
||||
sameWidth
|
||||
ref={listboxRef}
|
||||
role="listbox"
|
||||
className={clx(
|
||||
"shadow-elevation-flyout bg-ui-bg-base -left-2 z-50 w-[calc(var(--popover-anchor-width)+16px)] rounded-[8px] p-1",
|
||||
"shadow-elevation-flyout bg-ui-bg-base z-50 rounded-[8px] p-1",
|
||||
"max-h-[200px] overflow-y-auto",
|
||||
"data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
||||
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
||||
@@ -303,7 +325,7 @@ const ComboboxImpl = <T extends Value = string>(
|
||||
setValueOnClick={false}
|
||||
disabled={disabled}
|
||||
className={clx(
|
||||
"transition-fg bg-ui-bg-base data-[active-item=true]:bg-ui-bg-base-hover group flex cursor-pointer items-center gap-x-2 rounded-[4px] px-2 py-1.5",
|
||||
"transition-fg bg-ui-bg-base data-[active-item=true]:bg-ui-bg-base-hover group flex cursor-pointer items-center gap-x-2 rounded-[4px] px-2 py-1",
|
||||
{
|
||||
"text-ui-fg-disabled": disabled,
|
||||
"bg-ui-bg-component": disabled,
|
||||
|
||||
@@ -76,13 +76,13 @@ export const useComboboxData = <
|
||||
*/
|
||||
const disabled = !rest.isPending && !options.length && !searchValue
|
||||
|
||||
// // make sure that the default value is included in the option, if its not in options already
|
||||
if (
|
||||
defaultValue &&
|
||||
defaultOptions.length &&
|
||||
!options.find((o) => o.value === defaultValue)
|
||||
) {
|
||||
options.unshift(defaultOptions[0])
|
||||
// make sure that the default value is included in the options
|
||||
if (defaultValue && defaultOptions.length && !searchValue) {
|
||||
defaultOptions.forEach((option) => {
|
||||
if (!options.find((o) => o.value === option.value)) {
|
||||
options.unshift(option)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
ArrowUturnLeft,
|
||||
CheckMini,
|
||||
EllipseMiniSolid,
|
||||
TriangleRightMini,
|
||||
TrianglesMini,
|
||||
XMarkMini,
|
||||
@@ -9,12 +9,15 @@ import { AdminProductCategoryResponse } from "@medusajs/types"
|
||||
import { Text, clx } from "@medusajs/ui"
|
||||
import * as Popover from "@radix-ui/react-popover"
|
||||
import {
|
||||
CSSProperties,
|
||||
ComponentPropsWithoutRef,
|
||||
Fragment,
|
||||
MouseEvent,
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react"
|
||||
@@ -38,6 +41,9 @@ type Level = {
|
||||
label: string
|
||||
}
|
||||
|
||||
const TABLUAR_NUM_WIDTH = 8
|
||||
const TAG_BASE_WIDTH = 28
|
||||
|
||||
export const CategoryCombobox = forwardRef<
|
||||
HTMLInputElement,
|
||||
CategoryComboboxProps
|
||||
@@ -118,20 +124,23 @@ export const CategoryCombobox = forwardRef<
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelect(option: ProductCategoryOption) {
|
||||
return (e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const handleSelect = useCallback(
|
||||
(option: ProductCategoryOption) => {
|
||||
return (e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
if (isSelected(value, option.value)) {
|
||||
onChange(value.filter((v) => v !== option.value))
|
||||
} else {
|
||||
onChange([...value, option.value])
|
||||
if (isSelected(value, option.value)) {
|
||||
onChange(value.filter((v) => v !== option.value))
|
||||
} else {
|
||||
onChange([...value, option.value])
|
||||
}
|
||||
|
||||
innerRef.current?.focus()
|
||||
}
|
||||
|
||||
innerRef.current?.focus()
|
||||
}
|
||||
}
|
||||
},
|
||||
[value, onChange]
|
||||
)
|
||||
|
||||
function handleOpenChange(open: boolean) {
|
||||
if (!open) {
|
||||
@@ -150,21 +159,101 @@ export const CategoryCombobox = forwardRef<
|
||||
|
||||
const options = getOptions(product_categories || [])
|
||||
|
||||
const showTag = value.length > 0 && !open
|
||||
const showTag = value.length > 0
|
||||
const showSelected = !open && value.length > 0
|
||||
|
||||
const tagWidth = useMemo(() => {
|
||||
const count = value.length
|
||||
const digits = count.toString().length
|
||||
|
||||
return TAG_BASE_WIDTH + digits * TABLUAR_NUM_WIDTH
|
||||
}, [value])
|
||||
|
||||
const showLevelUp = !searchValue && level.length > 0
|
||||
|
||||
const [focusedIndex, setFocusedIndex] = useState<number>(-1)
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (!open) {
|
||||
return
|
||||
}
|
||||
|
||||
const optionsLength = showLevelUp ? options.length + 1 : options.length
|
||||
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault()
|
||||
setFocusedIndex((prev) => {
|
||||
const nextIndex = prev < optionsLength - 1 ? prev + 1 : prev
|
||||
return nextIndex
|
||||
})
|
||||
} else if (e.key === "ArrowUp") {
|
||||
e.preventDefault()
|
||||
setFocusedIndex((prev) => {
|
||||
return prev > 0 ? prev - 1 : prev
|
||||
})
|
||||
} else if (e.key === "ArrowRight") {
|
||||
const index = showLevelUp ? focusedIndex - 1 : focusedIndex
|
||||
const hasChildren = options[index]?.has_children
|
||||
|
||||
if (!hasChildren || !!searchValue) {
|
||||
return
|
||||
}
|
||||
|
||||
e.preventDefault()
|
||||
setLevel([
|
||||
...level,
|
||||
{
|
||||
id: options[index].value,
|
||||
label: options[index].label,
|
||||
},
|
||||
])
|
||||
setFocusedIndex(0)
|
||||
} else if (e.key === "Enter" && focusedIndex !== -1) {
|
||||
e.preventDefault()
|
||||
|
||||
if (showLevelUp && focusedIndex === 0) {
|
||||
setLevel(level.slice(0, level.length - 1))
|
||||
setFocusedIndex(0)
|
||||
return
|
||||
}
|
||||
|
||||
const index = showLevelUp ? focusedIndex - 1 : focusedIndex
|
||||
|
||||
handleSelect(options[index])(e as any)
|
||||
}
|
||||
},
|
||||
[open, focusedIndex, options, level, handleSelect, searchValue, showLevelUp]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("keydown", handleKeyDown)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown)
|
||||
}
|
||||
}, [handleKeyDown])
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover.Root modal open={open} onOpenChange={handleOpenChange}>
|
||||
<Popover.Trigger asChild>
|
||||
<Popover.Root open={open} onOpenChange={handleOpenChange}>
|
||||
<Popover.Anchor
|
||||
asChild
|
||||
onClick={() => {
|
||||
if (!open) {
|
||||
handleOpenChange(true)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
data-anchor
|
||||
className={clx(
|
||||
"relative flex cursor-pointer items-center gap-x-2 overflow-hidden",
|
||||
"h-8 w-full rounded-md px-2 py-0.5",
|
||||
"h-8 w-full rounded-md",
|
||||
"bg-ui-bg-field transition-fg shadow-borders-base",
|
||||
"hover:bg-ui-bg-field-hover",
|
||||
"has-[input:focus]:shadow-borders-interactive-with-active",
|
||||
"has-[:invalid]:shadow-borders-error has-[[aria-invalid=true]]:shadow-borders-error",
|
||||
"has-[:disabled]:bg-ui-bg-disabled has-[:disabled]:text-ui-fg-disabled has-[:disabled]:cursor-not-allowed",
|
||||
@@ -173,169 +262,199 @@ export const CategoryCombobox = forwardRef<
|
||||
// this prevents the styling from flickering when navigating
|
||||
// between levels.
|
||||
"shadow-borders-interactive-with-active": open,
|
||||
"pl-0.5": showTag,
|
||||
},
|
||||
className
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--tag-width": `${tagWidth}px`,
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
{open ? (
|
||||
<input
|
||||
ref={innerRef}
|
||||
value={searchValue}
|
||||
onChange={(e) => onSearchValueChange(e.target.value)}
|
||||
className={clx(
|
||||
"txt-compact-small w-full appearance-none bg-transparent outline-none",
|
||||
"placeholder:text-ui-fg-muted"
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
) : showTag ? (
|
||||
<div className="flex w-full items-center gap-x-2">
|
||||
<div className="flex w-fit items-center gap-x-1">
|
||||
<div className="bg-ui-bg-base txt-compact-small-plus text-ui-fg-subtle focus-within:border-ui-fg-interactive relative flex h-[28px] items-center rounded-[4px] border py-[3px] pl-1.5 pr-1">
|
||||
<span>{value.length}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="size-fit outline-none"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
onChange([])
|
||||
}}
|
||||
>
|
||||
<XMarkMini className="text-ui-fg-muted" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{showTag && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
onChange([])
|
||||
}}
|
||||
className="bg-ui-bg-base hover:bg-ui-bg-base-hover txt-compact-small-plus text-ui-fg-subtle focus-within:border-ui-fg-interactive transition-fg absolute left-0.5 top-0.5 flex h-[28px] items-center rounded-[4px] border py-[3px] pl-1.5 pr-1 outline-none"
|
||||
>
|
||||
<span className="tabular-nums">{value.length}</span>
|
||||
<XMarkMini className="text-ui-fg-muted" />
|
||||
</button>
|
||||
)}
|
||||
{showSelected && (
|
||||
<div className="pointer-events-none absolute inset-y-0 left-[calc(var(--tag-width)+8px)] flex size-full items-center">
|
||||
<Text size="small" leading="compact">
|
||||
{t("general.selected")}
|
||||
</Text>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full"></div>
|
||||
)}
|
||||
<div className="flex size-5 items-center justify-center">
|
||||
<input
|
||||
ref={innerRef}
|
||||
value={searchValue}
|
||||
onChange={(e) => {
|
||||
onSearchValueChange(e.target.value)
|
||||
}}
|
||||
className={clx(
|
||||
"txt-compact-small size-full cursor-pointer appearance-none bg-transparent pr-8 outline-none",
|
||||
"hover:bg-ui-bg-field-hover",
|
||||
"focus:cursor-text",
|
||||
"placeholder:text-ui-fg-muted",
|
||||
{
|
||||
"pl-2": !showTag,
|
||||
"pl-[calc(var(--tag-width)+8px)]": showTag,
|
||||
}
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleOpenChange(true)}
|
||||
className="text-ui-fg-muted transition-fg hover:bg-ui-bg-field-hover absolute right-0 flex size-8 items-center justify-center rounded-r outline-none"
|
||||
>
|
||||
<TrianglesMini className="text-ui-fg-muted" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</Popover.Trigger>
|
||||
<Popover.Portal>
|
||||
<Popover.Content
|
||||
sideOffset={8}
|
||||
role="listbox"
|
||||
className={clx(
|
||||
"shadow-elevation-flyout bg-ui-bg-base -left-2 z-50 w-[var(--radix-popper-anchor-width)] rounded-[8px]",
|
||||
"max-h-[200px] overflow-y-auto",
|
||||
"data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
||||
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
||||
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
|
||||
)}
|
||||
onOpenAutoFocus={(e) => {
|
||||
e.preventDefault()
|
||||
}}
|
||||
>
|
||||
{!searchValue && level.length > 0 && (
|
||||
<Fragment>
|
||||
<div className="p-1">
|
||||
</Popover.Anchor>
|
||||
<Popover.Content
|
||||
sideOffset={4}
|
||||
role="listbox"
|
||||
className={clx(
|
||||
"shadow-elevation-flyout bg-ui-bg-base -left-2 z-50 w-[var(--radix-popper-anchor-width)] rounded-[8px]",
|
||||
"max-h-[200px] overflow-y-auto",
|
||||
"data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
||||
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
||||
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
|
||||
)}
|
||||
onInteractOutside={(e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const target = e.target as HTMLElement
|
||||
|
||||
if (target.closest("[data-anchor]")) {
|
||||
return
|
||||
}
|
||||
|
||||
handleOpenChange(false)
|
||||
}}
|
||||
>
|
||||
{showLevelUp && (
|
||||
<Fragment>
|
||||
<div className="p-1">
|
||||
<button
|
||||
data-active={focusedIndex === 0}
|
||||
role="button"
|
||||
className={clx(
|
||||
"transition-fg grid w-full appearance-none grid-cols-[20px_1fr] items-center justify-center gap-2 rounded-md px-2 py-1.5 text-left outline-none",
|
||||
"data-[active=true]:bg-ui-bg-field-hover"
|
||||
)}
|
||||
type="button"
|
||||
onClick={handleLevelUp}
|
||||
onMouseEnter={() => setFocusedIndex(0)}
|
||||
onMouseLeave={() => setFocusedIndex(-1)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<ArrowUturnLeft className="text-ui-fg-muted" />
|
||||
<Text size="small" leading="compact">
|
||||
{getParentLabel(level)}
|
||||
</Text>
|
||||
</button>
|
||||
</div>
|
||||
<Divider />
|
||||
</Fragment>
|
||||
)}
|
||||
<div className="p-1">
|
||||
{options.length > 0 &&
|
||||
!showLoading &&
|
||||
options.map((option, index) => (
|
||||
<div
|
||||
key={option.value}
|
||||
className={clx(
|
||||
"transition-fg bg-ui-bg-base grid cursor-pointer grid-cols-1 items-center gap-2 overflow-hidden",
|
||||
{
|
||||
"grid-cols-[1fr_32px]": option.has_children && !searchValue,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<button
|
||||
className={clx(
|
||||
"transition-fg grid w-full appearance-none grid-cols-[20px_1fr] items-center justify-center gap-2 rounded-md px-2 py-1.5 text-left outline-none",
|
||||
"hover:bg-ui-bg-base-hover active:bg-ui-bg-base-pressed"
|
||||
)}
|
||||
data-active={
|
||||
showLevelUp
|
||||
? focusedIndex === index + 1
|
||||
: focusedIndex === index
|
||||
}
|
||||
type="button"
|
||||
onClick={handleLevelUp}
|
||||
role="option"
|
||||
className={clx(
|
||||
"grid h-full w-full appearance-none grid-cols-[20px_1fr] items-center gap-2 overflow-hidden rounded-md px-2 py-1.5 text-left outline-none",
|
||||
"data-[active=true]:bg-ui-bg-field-hover"
|
||||
)}
|
||||
onClick={handleSelect(option)}
|
||||
onMouseEnter={() =>
|
||||
setFocusedIndex(showLevelUp ? index + 1 : index)
|
||||
}
|
||||
onMouseLeave={() => setFocusedIndex(-1)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<ArrowUturnLeft className="text-ui-fg-muted" />
|
||||
<Text size="small" leading="compact">
|
||||
{getParentLabel(level)}
|
||||
<div className="flex size-5 items-center justify-center">
|
||||
{isSelected(value, option.value) && <EllipseMiniSolid />}
|
||||
</div>
|
||||
<Text
|
||||
as="span"
|
||||
size="small"
|
||||
leading="compact"
|
||||
className="w-full truncate"
|
||||
>
|
||||
{option.label}
|
||||
</Text>
|
||||
</button>
|
||||
</div>
|
||||
<Divider />
|
||||
</Fragment>
|
||||
)}
|
||||
<div className="p-1">
|
||||
{options.length > 0 &&
|
||||
!showLoading &&
|
||||
options.map((option) => (
|
||||
<div
|
||||
key={option.value}
|
||||
className={clx(
|
||||
"transition-fg bg-ui-bg-base grid cursor-pointer grid-cols-1 items-center gap-2 overflow-hidden",
|
||||
{
|
||||
"grid-cols-[1fr_32px]":
|
||||
option.has_children && !searchValue,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{option.has_children && !searchValue && (
|
||||
<button
|
||||
type="button"
|
||||
role="option"
|
||||
className={clx(
|
||||
"grid h-full w-full appearance-none grid-cols-[20px_1fr] items-center gap-2 overflow-hidden rounded-md px-2 py-1.5 text-left",
|
||||
"hover:bg-ui-bg-base-hover"
|
||||
"text-ui-fg-muted flex size-8 appearance-none items-center justify-center rounded-md outline-none",
|
||||
"hover:bg-ui-bg-base-hover active:bg-ui-bg-base-pressed"
|
||||
)}
|
||||
onClick={handleSelect(option)}
|
||||
type="button"
|
||||
onClick={handleLevelDown(option)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<div className="flex size-5 items-center justify-center">
|
||||
{isSelected(value, option.value) && <CheckMini />}
|
||||
</div>
|
||||
<Text
|
||||
as="span"
|
||||
size="small"
|
||||
leading="compact"
|
||||
className="w-full truncate"
|
||||
>
|
||||
{option.label}
|
||||
</Text>
|
||||
<TriangleRightMini />
|
||||
</button>
|
||||
{option.has_children && !searchValue && (
|
||||
<button
|
||||
className={clx(
|
||||
"text-ui-fg-muted flex size-8 appearance-none items-center justify-center rounded-md outline-none",
|
||||
"hover:bg-ui-bg-base-hover active:bg-ui-bg-base-pressed"
|
||||
)}
|
||||
type="button"
|
||||
onClick={handleLevelDown(option)}
|
||||
>
|
||||
<TriangleRightMini />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{showLoading &&
|
||||
Array.from({ length: 5 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="grid grid-cols-[20px_1fr_20px] gap-2 px-2 py-1.5"
|
||||
>
|
||||
<div />
|
||||
<TextSkeleton size="small" leading="compact" />
|
||||
<div />
|
||||
</div>
|
||||
))}
|
||||
{options.length === 0 && !showLoading && (
|
||||
<div className="px-2 py-1.5">
|
||||
<Text size="small" leading="compact">
|
||||
{query ? (
|
||||
<Trans
|
||||
i18n={i18n}
|
||||
i18nKey={"general.noResultsTitle"}
|
||||
tOptions={{
|
||||
query: query,
|
||||
}}
|
||||
components={[
|
||||
<span className="font-medium" key="query" />,
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
t("general.noResultsTitle")
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
))}
|
||||
{showLoading &&
|
||||
Array.from({ length: 5 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="grid grid-cols-[20px_1fr_20px] gap-2 px-2 py-1.5"
|
||||
>
|
||||
<div />
|
||||
<TextSkeleton size="small" leading="compact" />
|
||||
<div />
|
||||
</div>
|
||||
))}
|
||||
{options.length === 0 && !showLoading && (
|
||||
<div className="px-2 py-1.5">
|
||||
<Text size="small" leading="compact">
|
||||
{query ? (
|
||||
<Trans
|
||||
i18n={i18n}
|
||||
i18nKey={"general.noResultsTitle"}
|
||||
tOptions={{
|
||||
query: query,
|
||||
}}
|
||||
components={[<span className="font-medium" key="query" />]}
|
||||
/>
|
||||
) : (
|
||||
t("general.noResultsTitle")
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -21,8 +21,8 @@ type ProductOrganizationFormProps = {
|
||||
}
|
||||
|
||||
const ProductOrganizationSchema = zod.object({
|
||||
type_id: zod.string().optional(),
|
||||
collection_id: zod.string().optional(),
|
||||
type_id: zod.string().nullable(),
|
||||
collection_id: zod.string().nullable(),
|
||||
category_ids: zod.array(zod.string()),
|
||||
tag_ids: zod.array(zod.string()),
|
||||
})
|
||||
@@ -69,8 +69,8 @@ export const ProductOrganizationForm = ({
|
||||
|
||||
const form = useExtendableForm({
|
||||
defaultValues: {
|
||||
type_id: product.type_id || "",
|
||||
collection_id: product.collection_id || "",
|
||||
type_id: product.type_id ?? "",
|
||||
collection_id: product.collection_id ?? "",
|
||||
category_ids: product.categories?.map((c) => c.id) || [],
|
||||
tag_ids: product.tags?.map((t) => t.id) || [],
|
||||
},
|
||||
@@ -84,13 +84,10 @@ export const ProductOrganizationForm = ({
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
await mutateAsync(
|
||||
{
|
||||
type_id: data.type_id || undefined,
|
||||
collection_id: data.collection_id || undefined,
|
||||
categories: data.category_ids.map((id) => ({ id })) || undefined,
|
||||
tags:
|
||||
data.tag_ids?.map((t) => ({
|
||||
id: t,
|
||||
})) || undefined,
|
||||
type_id: data.type_id || null,
|
||||
collection_id: data.collection_id || null,
|
||||
categories: data.category_ids.map((c) => ({ id: c })),
|
||||
tags: data.tag_ids?.map((t) => ({ id: t })),
|
||||
},
|
||||
{
|
||||
onSuccess: ({ product }) => {
|
||||
@@ -148,10 +145,10 @@ export const ProductOrganizationForm = ({
|
||||
<Form.Control>
|
||||
<Combobox
|
||||
{...field}
|
||||
multiple={false}
|
||||
options={collections.options}
|
||||
searchValue={collections.searchValue}
|
||||
onSearchValueChange={collections.onSearchValueChange}
|
||||
fetchNextPage={collections.fetchNextPage}
|
||||
searchValue={collections.searchValue}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
@@ -190,9 +187,8 @@ export const ProductOrganizationForm = ({
|
||||
{...field}
|
||||
multiple
|
||||
options={tags.options}
|
||||
searchValue={tags.searchValue}
|
||||
onSearchValueChange={tags.onSearchValueChange}
|
||||
fetchNextPage={tags.fetchNextPage}
|
||||
searchValue={tags.searchValue}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
|
||||
@@ -43,14 +43,14 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@medusajs/ui-preset": "^1.1.3",
|
||||
"@storybook/addon-essentials": "^7.0.23",
|
||||
"@storybook/addon-interactions": "^7.0.23",
|
||||
"@storybook/addon-links": "^7.0.23",
|
||||
"@storybook/addon-styling": "^1.3.6",
|
||||
"@storybook/blocks": "^7.0.23",
|
||||
"@storybook/react": "^7.0.23",
|
||||
"@storybook/react-vite": "^7.0.23",
|
||||
"@storybook/testing-library": "^0.0.14-next.2",
|
||||
"@storybook/addon-essentials": "^8.3.5",
|
||||
"@storybook/addon-interactions": "^8.3.5",
|
||||
"@storybook/addon-links": "^8.3.5",
|
||||
"@storybook/addon-styling": "^1.3.7",
|
||||
"@storybook/blocks": "^8.3.5",
|
||||
"@storybook/react": "^8.3.5",
|
||||
"@storybook/react-vite": "^8.3.5",
|
||||
"@storybook/testing-library": "^0.2.2",
|
||||
"@testing-library/dom": "^9.3.1",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
@@ -71,7 +71,7 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"rimraf": "^5.0.1",
|
||||
"storybook": "^7.0.23",
|
||||
"storybook": "^8.3.5",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"tsc-alias": "^1.8.7",
|
||||
"typescript": "^5.1.6",
|
||||
@@ -105,8 +105,7 @@
|
||||
"react-currency-input-field": "^3.6.11",
|
||||
"react-stately": "^3.31.1",
|
||||
"sonner": "^1.5.0",
|
||||
"tailwind-merge": "^2.2.1",
|
||||
"upgrade": "^1.1.0"
|
||||
"tailwind-merge": "^2.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
|
||||
@@ -75,7 +75,7 @@ const DrawerContent = React.forwardRef<
|
||||
<DrawerPrimitives.Content
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"bg-ui-bg-base shadow-elevation-modal border-ui-border-base fixed inset-y-2 flex w-full flex-1 flex-col rounded-lg border focus:outline-none max-sm:inset-x-2 max-sm:w-[calc(100%-16px)] sm:right-2 sm:max-w-[560px]",
|
||||
"bg-ui-bg-base shadow-elevation-modal border-ui-border-base fixed inset-y-2 flex w-full flex-1 flex-col rounded-lg border outline-none max-sm:inset-x-2 max-sm:w-[calc(100%-16px)] sm:right-2 sm:max-w-[560px]",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:slide-out-to-right-1/2 data-[state=open]:slide-in-from-right-1/2 duration-200",
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -86,7 +86,7 @@ const textVariants = cva({
|
||||
],
|
||||
})
|
||||
|
||||
interface TextProps
|
||||
export interface TextProps
|
||||
extends React.ComponentPropsWithoutRef<"p">,
|
||||
VariantProps<typeof textVariants> {
|
||||
asChild?: boolean
|
||||
|
||||
Reference in New Issue
Block a user