Skip to content

Commit

Permalink
feat: add multiselect and dropdown
Browse files Browse the repository at this point in the history
  • Loading branch information
Emmanuel-Develops committed Nov 19, 2024
1 parent 9b5e160 commit 3c80e79
Show file tree
Hide file tree
Showing 16 changed files with 871 additions and 0 deletions.
128 changes: 128 additions & 0 deletions src/components/select/BaseSelectList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
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;
120 changes: 120 additions & 0 deletions src/components/select/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"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;
92 changes: 92 additions & 0 deletions src/components/select/MultiSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
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;
Loading

0 comments on commit 3c80e79

Please sign in to comment.