-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
1f58a4c
commit 6a2147b
Showing
17 changed files
with
918 additions
and
2 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
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,137 @@ | ||
import { LightningIconSolid } from "../../icons"; | ||
import { numberFormat } from "../../utils"; | ||
import { cn } from "../../utils/cn"; | ||
import React from "react"; | ||
|
||
export type BaseSelectContextTypeForList = { | ||
isListOpen: boolean; | ||
currentNavigateCheckbox: string; | ||
containerRef: React.MutableRefObject<HTMLDivElement> | null; | ||
}; | ||
|
||
export type SelectOption = { | ||
label: string; | ||
count?: number; | ||
value: string; | ||
selected: boolean; | ||
}; | ||
|
||
type StyleConfig = { | ||
container?: string; | ||
optionWrapper?: string; | ||
selectedOption?: string; | ||
optionInner?: string; | ||
icon?: string; | ||
label?: string; | ||
count?: string; | ||
noResults?: string; | ||
}; | ||
|
||
export type OnOptionSelect = ({ | ||
action, | ||
value, | ||
event, | ||
}: { | ||
action: "select" | "deselect"; | ||
value: string; | ||
event: React.MouseEvent; | ||
}) => void; | ||
|
||
export type SelectListProps = { | ||
options: SelectOption[]; | ||
label: string; | ||
onOptionSelect: OnOptionSelect; | ||
className?: string; | ||
styles?: StyleConfig; | ||
noResultsMessage?: string; // New: Customizable empty state | ||
selectContextData: BaseSelectContextTypeForList; | ||
}; | ||
|
||
const defaultStyles = { | ||
container: | ||
"scroller font-medium mt-2 max-h-[300px] py-[6px] overflow-auto border border-bdp-stroke rounded-xl data-[is-open='false']:hidden", | ||
optionWrapper: `flex gap-1 py-1 2xl:py-2 px-[14px] group/checkOption hover:bg-bdp-hover-state data-[current-navigated=true]:bg-bdp-hover-state | ||
group-hover/container:data-[current-navigated=true]:bg-transparent | ||
group-hover/container:data-[current-navigated=true]:hover:bg-bdp-hover-state | ||
data-[selected=true]:text-bdp-accent text-bdp-primary-text`, | ||
optionInner: "selectable-option flex grow items-center gap-3", | ||
icon: "shrink-0 group-data-[selected=false]/checkOption:invisible w-[12px] 2xl:w-[16px] h-auto", | ||
label: | ||
"grow capitalize text-sm 2xl:text-base group-data-[selected=true]/checkOption:font-bold", | ||
count: "shrink-0 group-data-[selected=true]/checkOption:font-medium", | ||
noResults: "w-full text-sm 2xl:text-base text-center px-2", | ||
} as const; | ||
|
||
const BaseSelectList = ({ | ||
options, | ||
label, | ||
onOptionSelect, | ||
className, | ||
styles = {}, | ||
noResultsMessage = "No matching options", | ||
selectContextData, | ||
}: SelectListProps) => { | ||
const { isListOpen, currentNavigateCheckbox, containerRef } = | ||
selectContextData; | ||
return ( | ||
<div | ||
data-is-open={isListOpen} | ||
ref={containerRef} | ||
className={cn( | ||
defaultStyles.container, | ||
// "data-[is-open='false']:hidden", | ||
styles.container, | ||
className, | ||
)} | ||
> | ||
{options.length < 1 && ( | ||
<p className={cn(defaultStyles.noResults, styles.noResults)}> | ||
{noResultsMessage} | ||
</p> | ||
)} | ||
{options?.map((option) => { | ||
const checked = option.selected; | ||
const value = option.value; | ||
return ( | ||
<label | ||
key={option.label} | ||
htmlFor={`checkbox-${label}-${option.label}`} | ||
data-checkbox={option.label} | ||
> | ||
<div | ||
data-selected={checked} | ||
data-current-navigated={option.label === currentNavigateCheckbox} | ||
className={cn(defaultStyles.optionWrapper, styles.optionWrapper)} | ||
onClick={(event) => | ||
onOptionSelect({ action: "select", value, event }) | ||
} | ||
role="button" | ||
aria-label={`${ | ||
checked ? "uncheck" : "check" | ||
} filter ${label}:${option.label}`} | ||
> | ||
<div | ||
className={cn(defaultStyles.optionInner, styles.optionInner)} | ||
id={`example_facet_${label}${option.label}`} | ||
> | ||
<LightningIconSolid | ||
className={cn(defaultStyles.icon, styles.icon)} | ||
/> | ||
<span className={cn(defaultStyles.label, styles.label)}> | ||
{option.label} | ||
</span> | ||
</div> | ||
{option.count ? ( | ||
<span className={cn(defaultStyles.count, styles.count)}> | ||
{numberFormat.format(option.count)} | ||
</span> | ||
) : null} | ||
</div> | ||
</label> | ||
); | ||
})} | ||
</div> | ||
); | ||
}; | ||
|
||
export default BaseSelectList; |
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,130 @@ | ||
"use client"; | ||
|
||
import React, { createContext, useCallback, useState } from "react"; | ||
import SingleSelectList, { | ||
SingleSelectListProps, | ||
SingleSelectOption, | ||
} from "./SingleSelectList"; | ||
import SingleSelectTrigger, { | ||
SingleSelectTriggerProps, | ||
} from "./SingleSelectInput"; | ||
|
||
type StyleConfig = { | ||
container?: string; | ||
input?: string; | ||
list?: string; | ||
option?: string; | ||
}; | ||
|
||
type SelectContextType = { | ||
isListOpen: boolean; | ||
toggleListOpen: () => void; | ||
selectedOption: SingleSelectOption | null; | ||
setSelectedOption: (option: SingleSelectOption | null) => void; | ||
containerRef: React.MutableRefObject<HTMLDivElement> | null; | ||
setContainerRef: React.Dispatch< | ||
React.SetStateAction<React.MutableRefObject<HTMLDivElement> | null> | ||
>; | ||
handleSelectOption: (option: SingleSelectOption) => void; | ||
triggerRef: React.RefObject<HTMLDivElement>; | ||
}; | ||
|
||
const SingleSelectContext = createContext<SelectContextType | null>(null); | ||
export const useSingleSelect = () => { | ||
const context = React.useContext(SingleSelectContext); | ||
if (!context) { | ||
throw new Error( | ||
"useSingleSelect must be used within a SingleSelectProvider", | ||
); | ||
} | ||
return context; | ||
}; | ||
|
||
type SingleSelectProviderProps = { | ||
children: React.ReactNode; | ||
triggerRef: React.RefObject<HTMLDivElement>; | ||
className?: string; | ||
styles?: StyleConfig; | ||
disabled?: boolean; | ||
}; | ||
|
||
const SingleSelectProvider = ({ | ||
children, | ||
triggerRef, | ||
disabled = false, | ||
}: SingleSelectProviderProps) => { | ||
const [isListOpen, setIsListOpen] = useState(false); | ||
const [containerRef, setContainerRef] = | ||
useState<React.MutableRefObject<HTMLDivElement> | null>(null); | ||
const [selectedOption, setSelectedOption] = | ||
useState<SingleSelectOption | null>(null); | ||
|
||
const toggleListOpen = () => { | ||
if (!disabled) { | ||
setIsListOpen((prev) => !prev); | ||
} | ||
}; | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
const handleSelectOption = (_option: SingleSelectOption) => { | ||
setIsListOpen(false); | ||
}; | ||
|
||
const handleClickOutside = useCallback( | ||
(event: MouseEvent) => { | ||
if ( | ||
containerRef?.current && | ||
triggerRef?.current && | ||
!containerRef.current.contains(event.target as Node) && | ||
!triggerRef.current.contains(event.target as Node) | ||
) { | ||
setIsListOpen(false); | ||
} | ||
}, | ||
[containerRef, isListOpen], | ||
); | ||
|
||
React.useEffect(() => { | ||
document.addEventListener("mousedown", handleClickOutside); | ||
return () => { | ||
document.removeEventListener("mousedown", handleClickOutside); | ||
}; | ||
}, [containerRef]); | ||
|
||
const contextValue = { | ||
isListOpen, | ||
toggleListOpen, | ||
selectedOption, | ||
setSelectedOption, | ||
handleSelectOption, | ||
containerRef, | ||
setContainerRef, | ||
triggerRef, | ||
}; | ||
|
||
return ( | ||
<SingleSelectContext.Provider value={contextValue}> | ||
<div className="relative">{children}</div> | ||
</SingleSelectContext.Provider> | ||
); | ||
}; | ||
|
||
export const SingleSelect: React.FC< | ||
Omit<SingleSelectProviderProps, "triggerRef"> | ||
> & { | ||
List: React.FC<SingleSelectListProps>; | ||
Trigger: React.FC<SingleSelectTriggerProps>; | ||
} = ({ | ||
children, | ||
disabled = false, | ||
}: Omit<SingleSelectProviderProps, "triggerRef">) => { | ||
const triggerRef = React.useRef<HTMLDivElement>(null); | ||
return ( | ||
<SingleSelectProvider disabled={disabled} triggerRef={triggerRef}> | ||
{children} | ||
</SingleSelectProvider> | ||
); | ||
}; | ||
|
||
SingleSelect.List = SingleSelectList; | ||
SingleSelect.Trigger = SingleSelectTrigger; |
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,99 @@ | ||
import React, { useState } from "react"; | ||
import useCheckboxNavigate from "./useSelectNavigate"; | ||
import SelectInput, { SelectInputProps } from "./SelectInput"; | ||
import SelectList, { MultiSelectListProps } from "./SelectList"; | ||
|
||
export type SelectContextType = { | ||
containerRef: React.MutableRefObject<HTMLDivElement> | null; | ||
setContainerRef: React.Dispatch< | ||
React.SetStateAction<React.MutableRefObject<HTMLDivElement> | null> | ||
>; | ||
searchInputRef: React.MutableRefObject<HTMLInputElement> | null; | ||
setSearchInputRef: React.Dispatch< | ||
React.SetStateAction<React.MutableRefObject<HTMLInputElement> | null> | ||
>; | ||
isListOpen: boolean; | ||
toggleListOpen: () => void; | ||
currentNavigateCheckbox: string; | ||
toggleRefocus: () => void; | ||
onSearch: (value: string) => void; | ||
inputValue: string; | ||
}; | ||
|
||
type SelectProviderProps = { | ||
children: React.ReactNode; | ||
isCollapsible?: boolean; | ||
}; | ||
|
||
const SelectContext = React.createContext<SelectContextType | null>(null); | ||
export const useMultiSelect = () => { | ||
const context = React.useContext(SelectContext); | ||
if (!context) { | ||
throw new Error("useMultiSelect must be used within a MultiSelectProvider"); | ||
} | ||
return context; | ||
}; | ||
|
||
export const MultiSelectProvider = ({ | ||
children, | ||
isCollapsible = true, | ||
}: SelectProviderProps) => { | ||
const [containerRef, setContainerRef] = | ||
useState<React.MutableRefObject<HTMLDivElement> | null>(null); | ||
const [searchInputRef, setSearchInputRef] = | ||
useState<React.MutableRefObject<HTMLInputElement> | null>(null); | ||
|
||
const [isListOpen, setIsListOpen] = useState(true); | ||
|
||
const toggleListOpen = () => { | ||
if (!isCollapsible) return; | ||
setIsListOpen((prev) => !prev); | ||
}; | ||
|
||
const [inputValue, setInputValue] = useState(""); | ||
|
||
const { currentNavigateCheckbox, toggleRefocus } = useCheckboxNavigate({ | ||
checkboxContainer: containerRef, | ||
searchEl: searchInputRef, | ||
options: [], | ||
}); | ||
|
||
// const [currentNavigateCheckbox, setcurrentNavigateCheckbox] = useState("") | ||
const onSearch = (value: string) => { | ||
const newValue = value.trim(); | ||
setInputValue(newValue); | ||
}; | ||
|
||
return ( | ||
<SelectContext.Provider | ||
value={{ | ||
containerRef, | ||
setContainerRef, | ||
searchInputRef, | ||
setSearchInputRef, | ||
isListOpen, | ||
toggleListOpen, | ||
currentNavigateCheckbox, | ||
toggleRefocus, | ||
onSearch, | ||
inputValue, | ||
}} | ||
> | ||
{children} | ||
</SelectContext.Provider> | ||
); | ||
}; | ||
|
||
export const MultiSelect: React.FC<SelectProviderProps> & { | ||
Input: React.FC<SelectInputProps>; | ||
List: React.FC<MultiSelectListProps>; | ||
} = ({ children, isCollapsible = true }: SelectProviderProps) => { | ||
return ( | ||
<MultiSelectProvider isCollapsible={isCollapsible}> | ||
{children} | ||
</MultiSelectProvider> | ||
); | ||
}; | ||
|
||
MultiSelect.Input = SelectInput; | ||
MultiSelect.List = SelectList; |
Oops, something went wrong.