feat(admin,admin-ui,medusa): Add Medusa Admin plugin (#3334)
This commit is contained in:
committed by
GitHub
parent
d6b1ad1ccd
commit
40de54b010
235
packages/admin-ui/ui/src/components/molecules/select/index.tsx
Normal file
235
packages/admin-ui/ui/src/components/molecules/select/index.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
import clsx from "clsx"
|
||||
import React, {
|
||||
useContext,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react"
|
||||
import Primitive from "react-select"
|
||||
import AsyncPrimitive from "react-select/async"
|
||||
import AsyncCreatablePrimitive from "react-select/async-creatable"
|
||||
import CreatablePrimitive from "react-select/creatable"
|
||||
import InputHeader, { InputHeaderProps } from "../../fundamentals/input-header"
|
||||
import { ModalContext } from "../modal"
|
||||
import { SelectComponents } from "./select-components"
|
||||
|
||||
export type SelectOption<T> = {
|
||||
value: T
|
||||
label: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
type MultiSelectProps = InputHeaderProps & {
|
||||
// component props
|
||||
label: string
|
||||
required?: boolean
|
||||
name?: string
|
||||
className?: string
|
||||
fullWidth?: boolean
|
||||
// Multiselect props
|
||||
placeholder?: string
|
||||
isMultiSelect?: boolean
|
||||
labelledBy?: string
|
||||
options: { label: string; value: string | null; disabled?: boolean }[]
|
||||
value:
|
||||
| { label: string; value: string }[]
|
||||
| { label: string; value: string }
|
||||
| null
|
||||
filterOptions?: (q: string) => any[]
|
||||
hasSelectAll?: boolean
|
||||
isLoading?: boolean
|
||||
shouldToggleOnHover?: boolean
|
||||
onChange: (values: any[] | any) => void
|
||||
disabled?: boolean
|
||||
enableSearch?: boolean
|
||||
isCreatable?: boolean
|
||||
clearSelected?: boolean
|
||||
onCreateOption?: (value: string) => { value: string; label: string }
|
||||
}
|
||||
|
||||
const SSelect = React.forwardRef(
|
||||
(
|
||||
{
|
||||
label,
|
||||
name,
|
||||
fullWidth = false,
|
||||
required,
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
isMultiSelect,
|
||||
hasSelectAll,
|
||||
tooltipContent,
|
||||
tooltip,
|
||||
enableSearch = true,
|
||||
clearSelected = false,
|
||||
isCreatable,
|
||||
filterOptions,
|
||||
placeholder = "Search...",
|
||||
options,
|
||||
onCreateOption,
|
||||
}: MultiSelectProps,
|
||||
ref
|
||||
) => {
|
||||
const { portalRef } = useContext(ModalContext)
|
||||
|
||||
const [isFocussed, setIsFocussed] = useState(false)
|
||||
const [scrollBlocked, setScrollBlocked] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("resize", () => {
|
||||
setIsFocussed(false)
|
||||
selectRef?.current?.blur()
|
||||
})
|
||||
}, [])
|
||||
|
||||
const selectRef = useRef(null)
|
||||
|
||||
useImperativeHandle(ref, () => selectRef.current)
|
||||
|
||||
const containerRef = useRef(null)
|
||||
|
||||
const onClickOption = (val, ...args) => {
|
||||
if (
|
||||
val?.length &&
|
||||
val?.find((option) => option.value === "all") &&
|
||||
hasSelectAll &&
|
||||
isMultiSelect
|
||||
) {
|
||||
onChange(options)
|
||||
} else {
|
||||
onChange(val)
|
||||
if (!isMultiSelect) {
|
||||
selectRef?.current?.blur()
|
||||
setIsFocussed(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleOnCreateOption = (val) => {
|
||||
if (onCreateOption) {
|
||||
onCreateOption(val)
|
||||
setIsFocussed(false)
|
||||
selectRef?.current?.blur()
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const delayDebounceFn = setTimeout(() => {
|
||||
if (isFocussed) {
|
||||
setScrollBlocked(false)
|
||||
}
|
||||
}, 50)
|
||||
|
||||
return () => clearTimeout(delayDebounceFn)
|
||||
}, [isFocussed])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={clsx({
|
||||
"w-full": fullWidth,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
key={name}
|
||||
className={clsx(className, {
|
||||
"bg-white rounded-t-rounded": isFocussed,
|
||||
})}
|
||||
>
|
||||
<div className="w-full flex text-grey-50 pr-0.5 justify-between pointer-events-none cursor-pointer mb-2">
|
||||
<InputHeader {...{ label, required, tooltip, tooltipContent }} />
|
||||
</div>
|
||||
|
||||
{
|
||||
<GetSelect
|
||||
isCreatable={isCreatable}
|
||||
searchBackend={filterOptions}
|
||||
options={
|
||||
hasSelectAll && isMultiSelect
|
||||
? [{ value: "all", label: "Select All" }, ...options]
|
||||
: options
|
||||
}
|
||||
ref={selectRef}
|
||||
value={value}
|
||||
isMulti={isMultiSelect}
|
||||
openMenuOnFocus={isMultiSelect}
|
||||
isSearchable={enableSearch}
|
||||
isClearable={clearSelected}
|
||||
onChange={onClickOption}
|
||||
onMenuOpen={() => {
|
||||
setIsFocussed(true)
|
||||
}}
|
||||
onMenuClose={() => {
|
||||
setScrollBlocked(true)
|
||||
setIsFocussed(false)
|
||||
}}
|
||||
closeMenuOnScroll={(e) => {
|
||||
if (
|
||||
!scrollBlocked &&
|
||||
e.target?.contains(containerRef.current) &&
|
||||
e.target !== document
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}}
|
||||
closeMenuOnSelect={!isMultiSelect}
|
||||
blurInputOnSelect={!isMultiSelect}
|
||||
styles={{ menuPortal: (base) => ({ ...base, zIndex: 60 }) }}
|
||||
hideSelectedOptions={false}
|
||||
menuPortalTarget={portalRef?.current?.lastChild || document.body}
|
||||
menuPlacement="auto"
|
||||
backspaceRemovesValue={false}
|
||||
classNamePrefix="react-select"
|
||||
placeholder={placeholder}
|
||||
className="react-select-container"
|
||||
onCreateOption={handleOnCreateOption}
|
||||
components={SelectComponents}
|
||||
/>
|
||||
}
|
||||
{isFocussed && enableSearch && <div className="w-full h-5" />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
const GetSelect = React.forwardRef(
|
||||
(
|
||||
{ isCreatable, searchBackend, onCreateOption, handleClose, ...props },
|
||||
ref
|
||||
) => {
|
||||
if (isCreatable) {
|
||||
return searchBackend ? (
|
||||
<AsyncCreatablePrimitive
|
||||
ref={ref}
|
||||
defaultOptions={true}
|
||||
onCreateOption={onCreateOption}
|
||||
isSearchable
|
||||
loadOptions={searchBackend}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<CreatablePrimitive
|
||||
{...props}
|
||||
isSearchable
|
||||
ref={ref}
|
||||
onCreateOption={onCreateOption}
|
||||
/>
|
||||
)
|
||||
} else if (searchBackend) {
|
||||
return (
|
||||
<AsyncPrimitive
|
||||
ref={ref}
|
||||
defaultOptions={true}
|
||||
loadOptions={searchBackend}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return <Primitive ref={ref} {...props} />
|
||||
}
|
||||
)
|
||||
|
||||
export default SSelect
|
||||
@@ -0,0 +1,190 @@
|
||||
import clsx from "clsx"
|
||||
import React, { ComponentPropsWithRef, forwardRef } from "react"
|
||||
import {
|
||||
ContainerProps,
|
||||
GroupBase,
|
||||
IndicatorsContainerProps,
|
||||
ValueContainerProps,
|
||||
} from "react-select"
|
||||
import InputError from "../../../../atoms/input-error"
|
||||
|
||||
type AdjacentContainerProps = {
|
||||
label?: string
|
||||
htmlFor?: string
|
||||
helperText?: string
|
||||
required?: boolean
|
||||
name?: string
|
||||
errors?: Record<string, unknown>
|
||||
children?: React.ReactNode
|
||||
} & ComponentPropsWithRef<"div">
|
||||
|
||||
export const AdjacentContainer = forwardRef<
|
||||
HTMLDivElement,
|
||||
AdjacentContainerProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
label,
|
||||
helperText,
|
||||
required,
|
||||
errors,
|
||||
name,
|
||||
children,
|
||||
}: AdjacentContainerProps,
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-xsmall w-full" ref={ref}>
|
||||
{label && (
|
||||
<label
|
||||
className="inter-small-semibold text-grey-50"
|
||||
id={`${name}_label`}
|
||||
>
|
||||
{label}
|
||||
{required && <span className="text-rose-50">*</span>}
|
||||
</label>
|
||||
)}
|
||||
{children}
|
||||
{name && errors ? (
|
||||
<InputError errors={errors} name={name} className="-mt-0.5" />
|
||||
) : helperText ? (
|
||||
<p className="inter-small-regular text-grey-50">{helperText}</p>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export const SelectContainer = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
innerProps,
|
||||
selectProps: { isDisabled, isRtl },
|
||||
hasValue,
|
||||
cx,
|
||||
className,
|
||||
children,
|
||||
}: ContainerProps<Option, IsMulti, Group>) => {
|
||||
return (
|
||||
<div
|
||||
{...innerProps}
|
||||
className={cx(
|
||||
{
|
||||
"--is-disabled": isDisabled,
|
||||
"--is-rtl": isRtl,
|
||||
"--has-value": hasValue,
|
||||
},
|
||||
clsx(
|
||||
"relative pointer-events-auto",
|
||||
{ "text-grey-40": isDisabled },
|
||||
className
|
||||
)
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ValueContainer = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>(
|
||||
props: ValueContainerProps<Option, IsMulti, Group>
|
||||
) => {
|
||||
const {
|
||||
className,
|
||||
children,
|
||||
cx,
|
||||
innerProps,
|
||||
isMulti,
|
||||
hasValue,
|
||||
selectProps: { value, inputValue, label, selectedPlaceholder },
|
||||
} = props
|
||||
|
||||
if (isMulti && Array.isArray(value)) {
|
||||
return (
|
||||
<div
|
||||
{...innerProps}
|
||||
className={cx(
|
||||
{
|
||||
"value-container": true,
|
||||
"value-container--is-multi": isMulti,
|
||||
"value-container--has-value": hasValue,
|
||||
},
|
||||
clsx(
|
||||
"group flex items-center flex-wrap relative scrolling-touch overflow-hidden flex-1",
|
||||
{
|
||||
"gap-2xsmall": isMulti,
|
||||
},
|
||||
className
|
||||
)
|
||||
)}
|
||||
>
|
||||
{value?.length > 0 && (
|
||||
<div className="h-7 bg-grey-20 text-grey-50 px-small inter-small-semibold flex items-center rounded-rounded gap-x-2xsmall cursor-default">
|
||||
<span>{value.length}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="relative grow">
|
||||
{children}
|
||||
{value?.length > 0 && inputValue === "" && (
|
||||
<span className="absolute top-1/2 -translate-y-1/2 inter-base-regular text-grey-50">
|
||||
{selectedPlaceholder || label || "Selected"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
{...innerProps}
|
||||
className={cx(
|
||||
{
|
||||
"value-container": true,
|
||||
"value-container--is-multi": isMulti,
|
||||
"value-container--has-value": hasValue,
|
||||
},
|
||||
clsx(
|
||||
"flex items-center flex-wrap relative scrolling-touch overflow-hidden flex-1",
|
||||
{
|
||||
"gap-2xsmall": isMulti,
|
||||
},
|
||||
className
|
||||
)
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const IndicatorsContainer = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
className,
|
||||
cx,
|
||||
innerProps,
|
||||
children,
|
||||
}: IndicatorsContainerProps<Option, IsMulti, Group>) => {
|
||||
return (
|
||||
<div
|
||||
{...innerProps}
|
||||
className={cx(
|
||||
{
|
||||
"indicators-container": true,
|
||||
},
|
||||
clsx("text-grey-50 flex items-center gap-x-small px-small", className)
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import clsx from "clsx"
|
||||
import {
|
||||
ClearIndicatorProps,
|
||||
ControlProps,
|
||||
DropdownIndicatorProps,
|
||||
GroupBase,
|
||||
LoadingIndicatorProps,
|
||||
} from "react-select"
|
||||
import Spinner from "../../../../atoms/spinner"
|
||||
import ChevronDownIcon from "../../../../fundamentals/icons/chevron-down"
|
||||
import XCircleIcon from "../../../../fundamentals/icons/x-circle-icon"
|
||||
|
||||
const Control = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
className,
|
||||
cx,
|
||||
children,
|
||||
innerRef,
|
||||
innerProps,
|
||||
isDisabled,
|
||||
isFocused,
|
||||
menuIsOpen,
|
||||
selectProps: { size, customStyles, name },
|
||||
}: ControlProps<Option, IsMulti, Group>) => {
|
||||
return (
|
||||
<div
|
||||
ref={innerRef}
|
||||
{...innerProps}
|
||||
id={name}
|
||||
className={cx(
|
||||
{
|
||||
control: true,
|
||||
"control--is-disabled": isDisabled,
|
||||
"control--is-focused": isFocused,
|
||||
"control--menu-is-open": menuIsOpen,
|
||||
},
|
||||
clsx(
|
||||
"flex p-0 overflow-hidden rounded-rounded border border-gray-20 bg-grey-5 focus-within:shadow-cta transition-colors focus-within:border-violet-60 box-border pl-small",
|
||||
{
|
||||
"h-xlarge": size === "sm",
|
||||
"h-10": size === "md" || !size,
|
||||
},
|
||||
className,
|
||||
customStyles?.control
|
||||
)
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
"flex items-center flex-1",
|
||||
customStyles?.inner_control
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Control
|
||||
|
||||
export const DropdownIndicator = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
innerProps,
|
||||
cx,
|
||||
children,
|
||||
className,
|
||||
selectProps: { menuIsOpen },
|
||||
}: DropdownIndicatorProps<Option, IsMulti, Group>) => {
|
||||
return (
|
||||
<div
|
||||
{...innerProps}
|
||||
className={cx(
|
||||
{
|
||||
indicator: true,
|
||||
"dropdown-indicator": true,
|
||||
},
|
||||
clsx(
|
||||
"transition-all",
|
||||
{
|
||||
"rotate-180": menuIsOpen,
|
||||
},
|
||||
className
|
||||
)
|
||||
)}
|
||||
>
|
||||
{children || <ChevronDownIcon size={16} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const LoadingIndicator = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
innerProps,
|
||||
className,
|
||||
cx,
|
||||
}: LoadingIndicatorProps<Option, IsMulti, Group>) => {
|
||||
return (
|
||||
<div
|
||||
{...innerProps}
|
||||
className={cx(
|
||||
{
|
||||
indicator: true,
|
||||
"loading-indicator": true,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Spinner size="small" variant="secondary" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ClearIndicator = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
innerProps,
|
||||
className,
|
||||
cx,
|
||||
children,
|
||||
}: ClearIndicatorProps<Option, IsMulti, Group>) => {
|
||||
return (
|
||||
<div
|
||||
{...innerProps}
|
||||
role="button"
|
||||
aria-label="Clear selected options"
|
||||
className={cx(
|
||||
{
|
||||
indicator: true,
|
||||
"clear-indicator": true,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children || <XCircleIcon size={16} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
AdjacentContainer,
|
||||
IndicatorsContainer,
|
||||
SelectContainer,
|
||||
ValueContainer,
|
||||
} from "./containers"
|
||||
import Control, {
|
||||
ClearIndicator,
|
||||
DropdownIndicator,
|
||||
LoadingIndicator,
|
||||
} from "./control"
|
||||
import Input from "./input"
|
||||
import Menu, { LoadingMessage, MenuList, Option } from "./menu"
|
||||
import Placeholder from "./placeholder"
|
||||
import SingleValue from "./single-value"
|
||||
|
||||
const Components = {
|
||||
ClearIndicator,
|
||||
DropdownIndicator,
|
||||
LoadingIndicator,
|
||||
SelectContainer,
|
||||
Control,
|
||||
Input,
|
||||
Placeholder,
|
||||
Menu,
|
||||
MenuList,
|
||||
Option,
|
||||
SingleValue,
|
||||
MultiValue: () => null,
|
||||
MultiValueContainer: () => null,
|
||||
MultiValueRemove: () => null,
|
||||
ValueContainer,
|
||||
IndicatorsContainer,
|
||||
LoadingMessage,
|
||||
IndicatorSeparator: null,
|
||||
}
|
||||
|
||||
export { AdjacentContainer }
|
||||
export default Components
|
||||
@@ -0,0 +1,36 @@
|
||||
import clsx from "clsx"
|
||||
import { GroupBase, InputProps } from "react-select"
|
||||
import SelectPrimitives from "./select-primitives"
|
||||
|
||||
const Input = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>(
|
||||
props: InputProps<Option, IsMulti, Group>
|
||||
) => {
|
||||
const { className, cx, value, inputClassName } = props
|
||||
|
||||
return (
|
||||
<div
|
||||
data-value={value || ""}
|
||||
className={cx({ "input-container": true }, className)}
|
||||
>
|
||||
<SelectPrimitives.Input
|
||||
{...props}
|
||||
className={cx(
|
||||
{
|
||||
input: true,
|
||||
"input--is-disabled": props.isDisabled ? true : false,
|
||||
},
|
||||
clsx(
|
||||
"inter-base-regular text-grey-90 caret-violet-60",
|
||||
inputClassName
|
||||
)
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Input
|
||||
@@ -0,0 +1,343 @@
|
||||
import clsx from "clsx"
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import {
|
||||
ActionMeta,
|
||||
CX,
|
||||
GroupBase,
|
||||
MenuListProps,
|
||||
MenuProps,
|
||||
NoticeProps,
|
||||
OnChangeValue,
|
||||
OptionProps,
|
||||
OptionsOrGroups,
|
||||
PropsValue,
|
||||
} from "react-select"
|
||||
import Button from "../../../../fundamentals/button"
|
||||
import CheckIcon from "../../../../fundamentals/icons/check-icon"
|
||||
import ListArrowIcon from "../../../../fundamentals/icons/list-arrow-icon"
|
||||
import { hasPrefix, hasSuffix, optionIsDisabled } from "../utils"
|
||||
import SelectPrimitives from "./select-primitives"
|
||||
|
||||
const Menu = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
className,
|
||||
cx,
|
||||
children,
|
||||
innerProps,
|
||||
innerRef,
|
||||
placement,
|
||||
selectProps: { onMenuClose, menuIsOpen, customStyles, styles },
|
||||
}: MenuProps<Option, IsMulti, Group>) => {
|
||||
useEffect(() => {
|
||||
const closeOnResize = () => {
|
||||
if (menuIsOpen) {
|
||||
onMenuClose()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("resize", closeOnResize)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", closeOnResize)
|
||||
}
|
||||
}, [menuIsOpen, onMenuClose])
|
||||
|
||||
return (
|
||||
<div
|
||||
{...innerProps}
|
||||
ref={innerRef}
|
||||
style={{ ...styles?.menu }}
|
||||
className={cx(
|
||||
{ menu: true },
|
||||
clsx(
|
||||
"absolute w-full overflow-hidden border-border z-[60] bg-grey-0 rounded-rounded border border-grey-20 shadow-dropdown mb-base",
|
||||
{
|
||||
"top-[calc(100%+8px)]": placement === "bottom",
|
||||
"bottom-full": placement === "top",
|
||||
},
|
||||
className,
|
||||
customStyles?.menu
|
||||
)
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Menu
|
||||
|
||||
type SelectAllOptionProps<
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
> = {
|
||||
cx: CX
|
||||
onChange: (
|
||||
newValue: OnChangeValue<Option, IsMulti>,
|
||||
actionMeta: ActionMeta<Option>
|
||||
) => void
|
||||
options: OptionsOrGroups<Option, Group>
|
||||
value: PropsValue<Option>
|
||||
}
|
||||
|
||||
const SelectAllOption = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
cx,
|
||||
onChange,
|
||||
options,
|
||||
value,
|
||||
}: SelectAllOptionProps<Option, IsMulti, Group>) => {
|
||||
const [isFocused, setIsFocused] = useState(false)
|
||||
const ref = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const isAllSelected = useMemo(() => {
|
||||
if (Array.isArray(value)) {
|
||||
const selectableOptions = options.filter((o) => !optionIsDisabled(o))
|
||||
return value.length === selectableOptions.length
|
||||
}
|
||||
|
||||
return false
|
||||
}, [options, value])
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
if (isAllSelected) {
|
||||
onChange([] as unknown as OnChangeValue<Option, IsMulti>, {
|
||||
action: "deselect-option",
|
||||
option: [] as unknown as Option,
|
||||
})
|
||||
} else {
|
||||
const selectableOptions = options.filter((o) => !optionIsDisabled(o))
|
||||
|
||||
onChange(selectableOptions as unknown as OnChangeValue<Option, IsMulti>, {
|
||||
action: "select-option",
|
||||
option: selectableOptions as unknown as Option,
|
||||
})
|
||||
}
|
||||
}, [isAllSelected, onChange, options])
|
||||
|
||||
useEffect(() => {
|
||||
setIsFocused(
|
||||
document.activeElement !== null && document.activeElement === ref.current
|
||||
)
|
||||
|
||||
return () => {
|
||||
setIsFocused(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
className={cx(
|
||||
{
|
||||
option: true,
|
||||
"option--is-focused": isFocused,
|
||||
},
|
||||
clsx("mx-base mb-2xsmall h-xlarge")
|
||||
)}
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
>
|
||||
<ListArrowIcon size={16} />
|
||||
<span className="inter-small-semibold">
|
||||
{!isAllSelected ? "Select All" : "Deselect All"}
|
||||
</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export const MenuList = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>(
|
||||
props: MenuListProps<Option, IsMulti, Group>
|
||||
) => {
|
||||
const {
|
||||
className,
|
||||
children,
|
||||
cx,
|
||||
isMulti,
|
||||
selectProps: { selectAll, value, onChange },
|
||||
options,
|
||||
} = props
|
||||
return (
|
||||
<SelectPrimitives.MenuList
|
||||
{...props}
|
||||
className={cx(
|
||||
{
|
||||
"menu-list": true,
|
||||
"menu-list--is-multi": isMulti,
|
||||
},
|
||||
clsx("overflow-y-auto flex flex-col py-xsmall no-scrollbar", className)
|
||||
)}
|
||||
>
|
||||
{isMulti && selectAll && (
|
||||
<SelectAllOption
|
||||
cx={cx}
|
||||
onChange={onChange}
|
||||
options={options}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</SelectPrimitives.MenuList>
|
||||
)
|
||||
}
|
||||
|
||||
export const LoadingMessage = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
innerProps,
|
||||
cx,
|
||||
className,
|
||||
selectProps: { size },
|
||||
}: NoticeProps<Option, IsMulti, Group>) => {
|
||||
const Skeleton = () => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"w-full flex items-center px-base transition-colors hover:bg-grey-5",
|
||||
{
|
||||
"h-xlarge": size === "sm",
|
||||
"h-10": size === "md" || !size,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="bg-grey-10 animate-pulse w-1/4 h-xsmall rounded-rounded" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
{...innerProps}
|
||||
className={cx(
|
||||
{
|
||||
"menu-notice": true,
|
||||
"menu-notice--loading": true,
|
||||
},
|
||||
clsx("flex flex-col", className)
|
||||
)}
|
||||
>
|
||||
<Skeleton />
|
||||
<Skeleton />
|
||||
<Skeleton />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Option = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>(
|
||||
props: OptionProps<Option, IsMulti, Group>
|
||||
) => {
|
||||
const {
|
||||
isSelected,
|
||||
isDisabled,
|
||||
isFocused,
|
||||
children,
|
||||
cx,
|
||||
className,
|
||||
innerProps,
|
||||
innerRef,
|
||||
selectProps: { hideSelectedOptions, isMulti, size, truncateOption },
|
||||
} = props
|
||||
|
||||
const prefix = hasPrefix(props.data) ? props.data.prefix : null
|
||||
const suffix = hasSuffix(props.data) ? props.data.suffix : null
|
||||
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
className={cx(
|
||||
{
|
||||
option: true,
|
||||
"option--is-selected": isSelected,
|
||||
"option--is-disabled": isDisabled,
|
||||
"option--is-focused": isFocused,
|
||||
},
|
||||
clsx(
|
||||
"flex items-center justify-between py-xsmall px-base transition-colors hover:bg-grey-5",
|
||||
{
|
||||
"text-grey-30 select-none cursor-not-allowed": isDisabled,
|
||||
"bg-grey-10": isFocused && !isDisabled,
|
||||
hidden: hideSelectedOptions && isSelected,
|
||||
},
|
||||
{
|
||||
"h-xlarge": size === "sm",
|
||||
"h-10": size === "md" || !size,
|
||||
},
|
||||
className
|
||||
)
|
||||
)}
|
||||
ref={innerRef}
|
||||
data-diabled={isDisabled ? true : undefined}
|
||||
aria-disabled={isDisabled ? true : undefined}
|
||||
tabIndex={isDisabled ? -1 : 0}
|
||||
{...innerProps}
|
||||
>
|
||||
<div className="flex items-center gap-x-small flex-1">
|
||||
{isMulti && (
|
||||
<CheckboxAdornment isSelected={isSelected} isDisabled={isDisabled} />
|
||||
)}
|
||||
<div
|
||||
className={clsx(
|
||||
"flex items-center justify-between gap-x-xsmall inter-base-regular flex-1",
|
||||
{
|
||||
truncate: !!truncateOption,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{prefix && <span className="inter-base-semibold">{prefix}</span>}
|
||||
<span className="w-full">{children}</span>
|
||||
|
||||
{suffix && (
|
||||
<span className="inter-base-regular justify-self-end text-grey-50">
|
||||
{suffix}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!isMulti && (
|
||||
<div className="w-5 ml-xsmall">
|
||||
{isSelected && <CheckIcon size={16} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const CheckboxAdornment = ({
|
||||
isSelected,
|
||||
isDisabled,
|
||||
}: Pick<OptionProps, "isSelected" | "isDisabled">) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
`w-base h-base flex justify-center text-grey-0 border-grey-30 border rounded-base transition-colors`,
|
||||
{
|
||||
"bg-violet-60 border-violet-60": isSelected,
|
||||
"bg-grey-5": isDisabled,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span className="self-center">
|
||||
{isSelected && <CheckIcon size={10} />}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
import {
|
||||
GroupBase,
|
||||
MultiValueGenericProps,
|
||||
MultiValueProps,
|
||||
MultiValueRemoveProps
|
||||
} from "react-select";
|
||||
import CrossIcon from "../../../../fundamentals/icons/cross-icon";
|
||||
import { optionIsFixed } from "../utils";
|
||||
|
||||
const MultiValue = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
children,
|
||||
className,
|
||||
cx,
|
||||
innerProps,
|
||||
isDisabled,
|
||||
isFocused,
|
||||
removeProps,
|
||||
components,
|
||||
selectProps,
|
||||
data,
|
||||
}: MultiValueProps<Option, IsMulti, Group>) => {
|
||||
const { Container, Label, Remove } = components
|
||||
|
||||
return (
|
||||
<Container
|
||||
data={data}
|
||||
innerProps={{
|
||||
className: cx({
|
||||
"multi-value": true,
|
||||
"multi-value--is-disabled": isDisabled,
|
||||
}, clsx({
|
||||
"bg-grey-70 text-grey-0": isFocused
|
||||
})),
|
||||
...innerProps,
|
||||
}}
|
||||
selectProps={selectProps}
|
||||
>
|
||||
<Label
|
||||
data={data}
|
||||
innerProps={{
|
||||
className: cx(
|
||||
{
|
||||
"multi-value__label": true,
|
||||
},
|
||||
className
|
||||
),
|
||||
}}
|
||||
selectProps={selectProps}
|
||||
>
|
||||
{children}
|
||||
</Label>
|
||||
<Remove
|
||||
data={data}
|
||||
selectProps={selectProps}
|
||||
innerProps={{
|
||||
className: cx(
|
||||
{
|
||||
"multi-value__remove": true,
|
||||
},
|
||||
className
|
||||
),
|
||||
"aria-label": `Remove ${children || "option"}`,
|
||||
...removeProps,
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default MultiValue
|
||||
|
||||
export const MultiValueContainer = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
children,
|
||||
innerProps: { className },
|
||||
}: MultiValueGenericProps<Option, IsMulti, Group>) => {
|
||||
return (
|
||||
<span className={clsx("flex items-center gap-x-2xsmall py-2xsmall pl-small pr-2.5 rounded-rounded transition-colors bg-grey-20 text-grey-50 inter-small-semibold", className)}>{children}</span>
|
||||
)
|
||||
}
|
||||
|
||||
export const MultiValueRemove = <
|
||||
Option = unknown,
|
||||
IsMulti extends boolean = boolean,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>
|
||||
>(
|
||||
props: MultiValueRemoveProps<Option, IsMulti, Group>
|
||||
) => {
|
||||
const { children, innerProps, data } = props;
|
||||
|
||||
if (optionIsFixed(data) && data.isFixed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
{...innerProps}
|
||||
role="button"
|
||||
className="text-grey-40"
|
||||
>
|
||||
{children || <CrossIcon size={16} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import clsx from "clsx"
|
||||
import React from "react"
|
||||
import { GroupBase, PlaceholderProps } from "react-select"
|
||||
|
||||
const Placeholder = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
innerProps,
|
||||
children,
|
||||
className,
|
||||
cx,
|
||||
}: PlaceholderProps<Option, IsMulti, Group>) => {
|
||||
return (
|
||||
<div
|
||||
{...innerProps}
|
||||
className={cx(
|
||||
{
|
||||
placeholder: true,
|
||||
},
|
||||
clsx(
|
||||
"absolute top-1/2 -translate-y-1/2 select-none inter-base-regular text-grey-50",
|
||||
className
|
||||
)
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Placeholder
|
||||
@@ -0,0 +1,3 @@
|
||||
import { components as SelectPrimitives } from "react-select"
|
||||
|
||||
export default SelectPrimitives
|
||||
@@ -0,0 +1,42 @@
|
||||
import clsx from "clsx"
|
||||
import React from "react"
|
||||
import { GroupBase, SingleValueProps } from "react-select"
|
||||
import { hasPrefix } from "../utils"
|
||||
|
||||
const SingleValue = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
innerProps,
|
||||
children,
|
||||
cx,
|
||||
className,
|
||||
isDisabled,
|
||||
data,
|
||||
}: SingleValueProps<Option, IsMulti, Group>) => {
|
||||
const prefix = hasPrefix(data) ? data.prefix : null
|
||||
|
||||
return (
|
||||
<div
|
||||
{...innerProps}
|
||||
className={cx(
|
||||
{
|
||||
"single-value": true,
|
||||
"single-value--is-disabled": isDisabled,
|
||||
},
|
||||
clsx(
|
||||
"overflow-hidden absolute top-1/2 -translate-y-1/2 whitespace-nowrap overflow-ellipsis",
|
||||
className
|
||||
)
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-x-xsmall inter-base-regular">
|
||||
{prefix && <span className="inter-base-semibold">{prefix}</span>}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SingleValue
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { MutableRefObject, ReactElement, RefAttributes } from "react"
|
||||
import React, { forwardRef } from "react"
|
||||
import type { GroupBase, SelectInstance } from "react-select"
|
||||
import type { CreatableProps } from "react-select/creatable"
|
||||
import CreatableReactSelect from "react-select/creatable"
|
||||
import { AdjacentContainer } from "../components"
|
||||
import { useSelectProps } from "../use-select-props"
|
||||
|
||||
export type CreatableSelectComponent = <
|
||||
Option = unknown,
|
||||
IsMulti extends boolean = false,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>
|
||||
>(
|
||||
props: CreatableProps<Option, IsMulti, Group> &
|
||||
RefAttributes<SelectInstance<Option, IsMulti, Group>>
|
||||
) => ReactElement
|
||||
|
||||
const CreatableSelect = forwardRef(
|
||||
<Option, IsMulti extends boolean, Group extends GroupBase<Option>>(
|
||||
props: CreatableProps<Option, IsMulti, Group>,
|
||||
ref:
|
||||
| ((instance: SelectInstance<Option, IsMulti, Group> | null) => void)
|
||||
| MutableRefObject<SelectInstance<Option, IsMulti, Group> | null>
|
||||
| null
|
||||
) => {
|
||||
const { label, helperText, required, ...rest } = useSelectProps(props)
|
||||
|
||||
return (
|
||||
<AdjacentContainer
|
||||
label={label}
|
||||
helperText={helperText}
|
||||
required={required}
|
||||
>
|
||||
<CreatableReactSelect ref={ref} {...rest} />
|
||||
</AdjacentContainer>
|
||||
)
|
||||
}
|
||||
) as CreatableSelectComponent
|
||||
|
||||
export default CreatableSelect
|
||||
@@ -0,0 +1,4 @@
|
||||
import NextCreateableSelect from "./createable-select"
|
||||
import NextSelect from "./select"
|
||||
|
||||
export { NextSelect, NextCreateableSelect }
|
||||
@@ -0,0 +1,74 @@
|
||||
import { ComponentProps } from "react"
|
||||
import { GroupBase } from "react-select"
|
||||
|
||||
export type SelectSize = "sm" | "md"
|
||||
|
||||
type SelectComponent = "control" | "inner_control" | "menu"
|
||||
|
||||
type SelectComponentStyles = Partial<
|
||||
Record<SelectComponent, ComponentProps<"div">["className"]>
|
||||
>
|
||||
|
||||
declare module "react-select/dist/declarations/src/Select" {
|
||||
export interface Props<
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
> {
|
||||
/**
|
||||
* An optional label to display above the select.
|
||||
*
|
||||
* @defaultValue `undefined`
|
||||
*/
|
||||
label?: string
|
||||
/**
|
||||
* An optional flag to indicate if the select is required.
|
||||
* If set to `true`, an asterisk will be displayed next to the label.
|
||||
*
|
||||
* @defaultValue `false`
|
||||
*/
|
||||
required?: boolean
|
||||
/**
|
||||
* An optional string to display when multiple options are selected in a Select where isMulti is true.
|
||||
*
|
||||
* @defaultValue `undefined`
|
||||
*/
|
||||
selectedPlaceholder?: string
|
||||
/**
|
||||
* An optional flag to indicate if the size of the select.
|
||||
*
|
||||
* @defaultValue `"md"`
|
||||
*/
|
||||
size?: SelectSize
|
||||
/**
|
||||
* An optinal helper text to display below the select.
|
||||
*
|
||||
* @defaultValue `undefined`
|
||||
*/
|
||||
helperText?: string
|
||||
/**
|
||||
* Errors provided by a containing form.
|
||||
*
|
||||
* @defaultValue `undefined`
|
||||
*/
|
||||
errors?: Record<string, unknown>
|
||||
/**
|
||||
* An optional flag to indicate if the select should be able to select all options.
|
||||
*
|
||||
* @defaultValue `false`
|
||||
*/
|
||||
selectAll?: boolean
|
||||
/**
|
||||
* An optinal object that can be used to override the default styles of the select components.
|
||||
*
|
||||
* @defaultValue `undefined`
|
||||
*/
|
||||
customStyles?: SelectComponentStyles
|
||||
/**
|
||||
*
|
||||
* @defaultValue false
|
||||
*
|
||||
*/
|
||||
truncateOption?: boolean
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import {
|
||||
forwardRef,
|
||||
MutableRefObject,
|
||||
ReactElement,
|
||||
RefAttributes,
|
||||
useContext,
|
||||
useRef,
|
||||
} from "react"
|
||||
import type { GroupBase, Props, SelectInstance } from "react-select"
|
||||
import ReactSelect from "react-select"
|
||||
import { ModalContext } from "../../../modal"
|
||||
import { AdjacentContainer } from "../components"
|
||||
import { useSelectProps } from "../use-select-props"
|
||||
|
||||
export type SelectComponent = <
|
||||
Option = unknown,
|
||||
IsMulti extends boolean = true,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>
|
||||
>(
|
||||
props: Props<Option, IsMulti, Group> &
|
||||
RefAttributes<SelectInstance<Option, IsMulti, Group>>
|
||||
) => ReactElement
|
||||
|
||||
const Select = forwardRef(
|
||||
<Option, IsMulti extends boolean, Group extends GroupBase<Option>>(
|
||||
props: Props<Option, IsMulti, Group>,
|
||||
ref:
|
||||
| ((instance: SelectInstance<Option, IsMulti, Group> | null) => void)
|
||||
| MutableRefObject<SelectInstance<Option, IsMulti, Group> | null>
|
||||
| null
|
||||
) => {
|
||||
const selectProps = useSelectProps(props)
|
||||
|
||||
const { label, required, helperText, name, errors } = selectProps
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const { portalRef } = useContext(ModalContext)
|
||||
|
||||
return (
|
||||
<AdjacentContainer
|
||||
ref={containerRef}
|
||||
label={label}
|
||||
htmlFor={name}
|
||||
helperText={helperText}
|
||||
required={required}
|
||||
name={name}
|
||||
errors={errors}
|
||||
>
|
||||
<ReactSelect
|
||||
aria-labelledby={`${name}_label`}
|
||||
ref={ref}
|
||||
name={name}
|
||||
{...selectProps}
|
||||
menuPortalTarget={portalRef?.current?.lastChild || null}
|
||||
menuShouldBlockScroll={true}
|
||||
/>
|
||||
</AdjacentContainer>
|
||||
)
|
||||
}
|
||||
) as SelectComponent
|
||||
|
||||
export default Select
|
||||
@@ -0,0 +1,85 @@
|
||||
import isEqual from "lodash/isEqual"
|
||||
import { useEffect, useState } from "react"
|
||||
import { ActionMeta, GroupBase, OnChangeValue, Props } from "react-select"
|
||||
import Components from "./components"
|
||||
import { formatOptionLabel, hasLabel } from "./utils"
|
||||
|
||||
export const useSelectProps = <
|
||||
Option,
|
||||
IsMulti extends boolean = false,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>
|
||||
>({
|
||||
components = {},
|
||||
isMulti,
|
||||
closeMenuOnScroll = true,
|
||||
hideSelectedOptions = false,
|
||||
closeMenuOnSelect,
|
||||
label,
|
||||
size,
|
||||
options,
|
||||
onChange: changeFn,
|
||||
styles,
|
||||
...props
|
||||
}: Props<Option, IsMulti, Group>): Props<Option, IsMulti, Group> => {
|
||||
const [stateOptions, setStateOptions] = useState(options || [])
|
||||
|
||||
const sortOptions = (values: Option[]) => {
|
||||
const tmp = values || []
|
||||
|
||||
const unselectedOptions = stateOptions.filter(
|
||||
(option) => !tmp.find((op) => isEqual(op, option))
|
||||
)
|
||||
|
||||
const orderedNewOptions = tmp.sort((a, b) => {
|
||||
if (hasLabel(a) && hasLabel(b)) {
|
||||
return a.label > b.label ? 1 : b.label > a.label ? -1 : 0
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
|
||||
setStateOptions(orderedNewOptions.concat(unselectedOptions as Option[]))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isMulti && options) {
|
||||
sortOptions(props.value as Option[])
|
||||
} else {
|
||||
setStateOptions(options || [])
|
||||
}
|
||||
}, [options, props.value, isMulti])
|
||||
|
||||
const onChange = (
|
||||
newValue: OnChangeValue<Option, IsMulti>,
|
||||
actionMeta: ActionMeta<Option>
|
||||
) => {
|
||||
if (isMulti) {
|
||||
sortOptions(newValue as Option[])
|
||||
}
|
||||
|
||||
if (changeFn) {
|
||||
changeFn(newValue, actionMeta)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
label,
|
||||
components: Components,
|
||||
styles: {
|
||||
menuPortal: (base) => ({ ...base, zIndex: 60 }),
|
||||
...styles,
|
||||
},
|
||||
isMulti,
|
||||
closeMenuOnScroll: true,
|
||||
closeMenuOnSelect:
|
||||
closeMenuOnSelect !== undefined ? closeMenuOnSelect : isMulti !== true,
|
||||
hideSelectedOptions,
|
||||
menuPosition: "fixed",
|
||||
maxMenuHeight: size === "sm" ? 154 : 188,
|
||||
formatOptionLabel,
|
||||
size,
|
||||
options: stateOptions,
|
||||
onChange,
|
||||
...props,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { omit } from "lodash"
|
||||
import Highlighter from "react-highlight-words"
|
||||
import type {
|
||||
CommonPropsAndClassName,
|
||||
FormatOptionLabelMeta,
|
||||
GroupBase,
|
||||
} from "react-select"
|
||||
|
||||
export const cleanCommonProps = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>,
|
||||
AdditionalProps
|
||||
>(
|
||||
props: Partial<CommonPropsAndClassName<Option, IsMulti, Group>> &
|
||||
AdditionalProps
|
||||
) => {
|
||||
const innerProps = omit(props, [
|
||||
"className",
|
||||
"clearValue",
|
||||
"cx",
|
||||
"getStyles",
|
||||
"getValue",
|
||||
"hasValue",
|
||||
"isMulti",
|
||||
"isRtl",
|
||||
"options",
|
||||
"selectOption",
|
||||
"selectProps",
|
||||
"setValue",
|
||||
"theme",
|
||||
])
|
||||
return { ...innerProps }
|
||||
}
|
||||
|
||||
export const optionIsFixed = (
|
||||
option: unknown
|
||||
): option is { isFixed: unknown } =>
|
||||
typeof option === "object" && option !== null && "isFixed" in option
|
||||
|
||||
export const optionIsDisabled = (
|
||||
option: unknown
|
||||
): option is { isDisabled: boolean } =>
|
||||
typeof option === "object" && option !== null && "isDisabled" in option
|
||||
|
||||
export const hasLabel = (option: unknown): option is { label: string } => {
|
||||
return typeof option === "object" && option !== null && "label" in option
|
||||
}
|
||||
|
||||
export const hasPrefix = (option: unknown): option is { prefix: string } => {
|
||||
return typeof option === "object" && option !== null && "prefix" in option
|
||||
}
|
||||
|
||||
export const hasSuffix = (option: unknown): option is { suffix: string } => {
|
||||
return typeof option === "object" && option !== null && "suffix" in option
|
||||
}
|
||||
|
||||
export const isCreateOption = (
|
||||
option: unknown
|
||||
): option is { __isNew__: true } => {
|
||||
return typeof option === "object" && option !== null && "__isNew__" in option
|
||||
}
|
||||
|
||||
export const formatOptionLabel = <Option,>(
|
||||
option: Option,
|
||||
{ inputValue }: FormatOptionLabelMeta<Option>
|
||||
) => {
|
||||
if (!hasLabel(option)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isCreateOption(option)) {
|
||||
return option.label
|
||||
}
|
||||
|
||||
return (
|
||||
<Highlighter
|
||||
searchWords={[inputValue]}
|
||||
textToHighlight={option.label}
|
||||
highlightClassName="bg-orange-10"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
import clsx from "clsx"
|
||||
import React from "react"
|
||||
import {
|
||||
ClearIndicatorProps,
|
||||
components,
|
||||
ContainerProps,
|
||||
DropdownIndicatorProps,
|
||||
GroupBase,
|
||||
InputProps,
|
||||
MenuListProps,
|
||||
MenuProps,
|
||||
MultiValueProps,
|
||||
NoticeProps,
|
||||
OptionProps,
|
||||
PlaceholderProps,
|
||||
SingleValueProps,
|
||||
} from "react-select"
|
||||
import CheckIcon from "../../fundamentals/icons/check-icon"
|
||||
import ChevronDownIcon from "../../fundamentals/icons/chevron-down"
|
||||
import SearchIcon from "../../fundamentals/icons/search-icon"
|
||||
import XCircleIcon from "../../fundamentals/icons/x-circle-icon"
|
||||
|
||||
const MultiValueLabel = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
innerProps,
|
||||
data,
|
||||
selectProps: { value, isSearchable, menuIsOpen },
|
||||
children,
|
||||
}: MultiValueProps<Option, IsMulti, Group>) => {
|
||||
const isLast = Array.isArray(value) ? value[value.length - 1] === data : true
|
||||
|
||||
if (menuIsOpen && isSearchable) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
{...innerProps}
|
||||
className={clsx("bg-grey-5 mx-0 inter-base-regular p-0", {
|
||||
"after:content-[',']": !isLast,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Menu = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
className,
|
||||
...props
|
||||
}: MenuProps<Option, IsMulti, Group>) => {
|
||||
return (
|
||||
<components.Menu
|
||||
className={clsx("!rounded-rounded", {
|
||||
"-mt-1 z-60":
|
||||
!props.selectProps.isSearchable && props.menuPlacement === "bottom",
|
||||
"mb-3 z-60":
|
||||
!props.selectProps.isSearchable && props.menuPlacement === "top",
|
||||
})}
|
||||
{...props}
|
||||
>
|
||||
{props.children}
|
||||
</components.Menu>
|
||||
)
|
||||
}
|
||||
|
||||
const MenuList = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
className,
|
||||
...props
|
||||
}: MenuListProps<Option, IsMulti, Group>) => {
|
||||
return (
|
||||
<components.MenuList
|
||||
className={clsx(className, "!rounded-rounded !no-scrollbar")}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const Placeholder = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>(
|
||||
props: PlaceholderProps<Option, IsMulti, Group>
|
||||
) => {
|
||||
return props.selectProps.menuIsOpen ? null : (
|
||||
<components.Placeholder {...props} className="!mx-0 !text-grey-40" />
|
||||
)
|
||||
}
|
||||
|
||||
const SingleValue = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
children,
|
||||
...props
|
||||
}: SingleValueProps<Option, IsMulti, Group>) => {
|
||||
if (props.selectProps.menuIsOpen && props.selectProps.isSearchable) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <components.SingleValue {...props}>{children}</components.SingleValue>
|
||||
}
|
||||
|
||||
const DropdownIndicator = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
innerProps,
|
||||
selectProps: { menuIsOpen },
|
||||
}: DropdownIndicatorProps<Option, IsMulti, Group>) => {
|
||||
return (
|
||||
<div {...innerProps} className="flex items-center justify-center">
|
||||
<ChevronDownIcon
|
||||
size={16}
|
||||
className={clsx("text-grey-50 transition-all", {
|
||||
"rotate-180": menuIsOpen,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const SelectContainer = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>(
|
||||
props: ContainerProps<Option, IsMulti, Group>
|
||||
) => {
|
||||
return (
|
||||
<div className="bg-grey-5 h-10 border border-grey-20 rounded-rounded focus-within:shadow-cta focus-within:border-violet-60 px-small">
|
||||
<components.SelectContainer {...props} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Input = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>(
|
||||
props: InputProps<Option, IsMulti, Group>
|
||||
) => {
|
||||
if (
|
||||
props.isHidden ||
|
||||
!props.selectProps.menuIsOpen ||
|
||||
!props.selectProps.isSearchable
|
||||
) {
|
||||
return <components.Input {...props} className="pointer-events-none" />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full flex items-center h-full space-between">
|
||||
<div className="w-full flex items-center">
|
||||
<span className="text-grey-40 mr-2">
|
||||
<SearchIcon size={16} />
|
||||
</span>
|
||||
<components.Input {...props} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ClearIndicator = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
innerProps,
|
||||
selectProps: { isMulti, menuIsOpen },
|
||||
}: ClearIndicatorProps<Option, IsMulti, Group>) => {
|
||||
if (menuIsOpen || isMulti) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
{...innerProps}
|
||||
className="hover:bg-grey-10 text-grey-50 rounded cursor-pointer"
|
||||
>
|
||||
<XCircleIcon size={16} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const CheckboxAdornment = ({ isSelected }: { isSelected: boolean }) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
`w-5 h-5 flex justify-center text-grey-0 border-grey-30 border rounded-base`,
|
||||
{
|
||||
"bg-violet-60": isSelected,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span className="self-center">
|
||||
{isSelected && <CheckIcon size={16} />}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const RadioAdornment = ({ isSelected }: { isSelected: boolean }) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"radio-outer-ring outline-0",
|
||||
"shrink-0 w-[20px] h-[20px] rounded-circle",
|
||||
{
|
||||
"shadow-[0_0_0_1px] shadow-[#D1D5DB]": !isSelected,
|
||||
"shadow-[0_0_0_2px] shadow-violet-60": isSelected,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{isSelected && (
|
||||
<div
|
||||
className={clsx(
|
||||
"group flex items-center justify-center w-full h-full relative",
|
||||
"after:absolute after:inset-0 after:m-auto after:block after:w-[12px] after:h-[12px] after:bg-violet-60 after:rounded-circle"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const NoOptionsMessage = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
innerProps,
|
||||
selectProps: { isLoading },
|
||||
}: NoticeProps<Option, IsMulti, Group>) => {
|
||||
return (
|
||||
<div
|
||||
className="text-grey-50 inter-small-semibold text-center p-xsmall"
|
||||
{...innerProps}
|
||||
>
|
||||
<p>{isLoading ? "Loading..." : "No options"}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Option = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option>
|
||||
>({
|
||||
className,
|
||||
...props
|
||||
}: OptionProps<Option, IsMulti, Group>) => {
|
||||
return (
|
||||
<components.Option
|
||||
{...props}
|
||||
className="my-1 py-0 px-2 bg-grey-0 active:bg-grey-0"
|
||||
>
|
||||
<div
|
||||
className={`item-renderer h-full hover:bg-grey-10 py-2 px-2 cursor-pointer rounded`}
|
||||
>
|
||||
<div className="items-center h-full flex">
|
||||
{props.data?.value !== "all" && props.data?.label !== "Select All" ? (
|
||||
<>
|
||||
{props.isMulti ? (
|
||||
<CheckboxAdornment isSelected={props.isSelected} />
|
||||
) : (
|
||||
<RadioAdornment isSelected={props.isSelected} />
|
||||
)}
|
||||
<span className="ml-3 text-grey-90 inter-base-regular">
|
||||
{props.data.label}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-grey-90 inter-base-regular">
|
||||
{props.data.label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</components.Option>
|
||||
)
|
||||
}
|
||||
|
||||
export const SelectComponents = {
|
||||
Menu,
|
||||
MenuList,
|
||||
Placeholder,
|
||||
SingleValue,
|
||||
DropdownIndicator,
|
||||
SelectContainer,
|
||||
Input,
|
||||
ClearIndicator,
|
||||
CheckboxAdornment,
|
||||
RadioAdornment,
|
||||
NoOptionsMessage,
|
||||
Option,
|
||||
IndicatorSeparator: () => null,
|
||||
MultiValueRemove: () => null,
|
||||
MultiValueLabel,
|
||||
}
|
||||
Reference in New Issue
Block a user