Sad to see it go (#8230)

pull/8505/head
sean-brydon 2023-04-24 15:07:29 +01:00 committed by GitHub
parent 88d17165ec
commit c037a62a9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 0 additions and 667 deletions

View File

@ -1,88 +0,0 @@
import { Check } from "lucide-react";
import React from "react";
import { classNames as cn } from "@calcom/lib";
import { useSelectContext } from "./SelectProvider";
import type { Option } from "./type";
interface ItemProps {
item: Option;
index?: number;
focused: boolean;
}
const Item: React.FC<ItemProps> = ({ item, index, focused }) => {
const { classNames, selectedItems, handleValueChange } = useSelectContext();
const isMultiple = Array.isArray(selectedItems);
const isSelected =
(isMultiple && selectedItems?.some((selection) => selection.value === item.value)) ||
(!isMultiple && selectedItems?.value === item.value);
if (item.disabled) {
return (
<li
className={cn(
" flex cursor-not-allowed select-none justify-between truncate rounded-[4px] px-3 py-2 text-gray-300 ",
focused ? "dark:bg-darkgray-200 bg-muted" : "dark:hover:bg-darkgray-200 hover:bg-subtle"
)}>
<>
<div className="space-x flex items-center">
{item.leftNode && item.leftNode}
<p>{item.label}</p>
</div>
{isMultiple ? (
<div
className={cn(
"flex h-4 w-4 items-center justify-center rounded-[4px] border opacity-70 ltr:mr-2 rtl:ml-2",
isSelected
? "dark:bg-darkgray-200 border-subtle bg-gray-800 text-gray-50"
: " dark:bg-darkgray-200 border-subtle border-default bg-mutedext-gray-600"
)}>
{isSelected && <Check className="h-3 w-3 text-current" />}
</div>
) : (
isSelected && <Check className="text-emphasis h-3 w-3" strokeWidth={2} />
)}
</>
</li>
);
}
return (
<li
aria-selected={isSelected}
tabIndex={index}
role="option"
onClick={() => handleValueChange(item)}
className={cn(
"block flex cursor-pointer select-none items-center justify-between truncate border-transparent px-3 py-2 transition duration-200",
isSelected
? "dark:bg-darkgray-200 bg-subtle text-emphasis"
: " dark:hover:bg-darkgray-200 text-default hover:bg-subtle",
focused && "dark:bg-darkgray-200 bg-muted",
classNames?.listItem
)}>
<div className="flex items-center space-x-2">
{item.leftNode && item.leftNode}
<p>{item.label}</p>
</div>
{isMultiple ? (
<div
className={cn(
"flex h-4 w-4 items-center justify-center rounded-[4px] border ltr:mr-2 rtl:ml-2",
isSelected
? "dark:bg-darkgray-200 border-subtle bg-gray-800 text-gray-50"
: " dark:bg-darkgray-200 border-subtle border-default bg-mutedext-gray-600"
)}>
{isSelected && <Check className="h-3 w-3 text-current" />}
</div>
) : (
isSelected && <Check className="text-emphasis h-3 w-3" strokeWidth={2} />
)}
</li>
);
};
export default Item;

View File

@ -1,136 +0,0 @@
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import classNames from "@calcom/lib/classNames";
import { useKeyPress } from "@calcom/lib/hooks/useKeyPress";
import { Label } from "../../inputs/Label";
import Item from "./Item";
import { SelectContext } from "./SelectProvider";
import type { Option } from "./type";
interface OptionsProps<T extends Option> {
list: T[];
inputValue: string;
isMultiple: boolean;
selected: T | T[] | null;
searchBoxRef: React.RefObject<HTMLInputElement>;
}
type FlattenedOption = Option & { current: number; groupedIndex?: number };
const flattenOptions = (options: Option[], groupCount?: number): FlattenedOption[] => {
return options.reduce((acc, option, current) => {
if (option.options) {
return [...acc, ...flattenOptions(option.options, current + (groupCount || 0))];
}
return [...acc, { ...option, current, groupedIndex: groupCount }];
}, [] as FlattenedOption[]);
};
function FilteredItem<T extends Option>({
index,
keyboardFocus,
item,
inputValue,
list,
}: {
index: number;
keyboardFocus: number;
item: FlattenedOption;
inputValue: string;
list: T[];
}) {
const focused = index === keyboardFocus;
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (ref.current && focused) {
ref.current.scrollIntoView({ behavior: "smooth" });
}
}, [ref, focused]);
return (
<div key={index} ref={ref}>
{item.current === 0 && item.groupedIndex !== undefined && !inputValue && (
<div>
{index !== 0 && <hr className="mt-2" />}
<Label
className={classNames(
" text-default mb-2 pl-3 text-xs font-normal uppercase leading-none",
index !== 0 ? "mt-5" : "mt-4" // rest, first
)}>
{list[item.groupedIndex].label}
</Label>
</div>
)}
<Item item={item} index={index} focused={focused} />
</div>
);
}
function Options<T extends Option>({ list, inputValue, searchBoxRef }: OptionsProps<T>) {
const { classNames, handleValueChange } = useContext(SelectContext);
const [keyboardFocus, setKeyboardFocus] = useState(-1);
const enterPress = useKeyPress("Enter", searchBoxRef);
const flattenedList = useMemo(() => flattenOptions(list), [list]);
const totalOptionsLength = useMemo(() => {
return flattenedList.length;
}, [flattenedList]);
useKeyPress("ArrowDown", searchBoxRef, () => setKeyboardFocus((prev) => (prev + 1) % totalOptionsLength));
useKeyPress("ArrowUp", searchBoxRef, () =>
setKeyboardFocus((prev) => (prev - 1 + totalOptionsLength) % totalOptionsLength)
);
useEffect(() => {
if (enterPress) {
const item = filteredList[keyboardFocus];
if (!item || item.disabled) return;
handleValueChange(item);
}
// We don't want to re-run this effect when handleValueChange changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [enterPress, keyboardFocus, list]);
const search = useCallback((optionsArray: FlattenedOption[], searchTerm: string) => {
// search options by label, or group label or options.options
return optionsArray.reduce((acc: FlattenedOption[], option: FlattenedOption) => {
// @TODO: add search by lavbel group gets awkward as it doesnt exist in the flattened list
if (option.label.toLowerCase().includes(searchTerm.toLowerCase())) {
acc.push(option);
}
return acc;
}, [] as FlattenedOption[]);
}, []);
const filteredList = useMemo(() => {
if (inputValue.length > 0) {
return search(flattenedList, inputValue);
}
return flattenedList;
}, [inputValue, flattenedList, search]);
return (
<div
className={
classNames?.list ?? "flex max-h-72 flex-col space-y-[1px] overflow-y-auto overflow-y-scroll"
}>
{filteredList?.map((item, index) => (
<FilteredItem
key={index}
item={item}
index={index}
keyboardFocus={keyboardFocus}
inputValue={inputValue}
list={list}
/>
))}
</div>
);
}
export default Options;

View File

@ -1,52 +0,0 @@
import React, { useContext } from "react";
import { Search } from "../../../icon";
import { SelectContext } from "./SelectProvider";
interface SearchInputProps {
placeholder?: string;
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
name?: string;
searchInputRef?: React.RefObject<HTMLInputElement>;
}
const SearchInput: React.FC<SearchInputProps> = ({
placeholder = "",
value = "",
onChange,
name = "",
searchInputRef,
}) => {
const { classNames } = useContext(SelectContext);
return (
<div
className={
classNames && classNames.searchContainer ? classNames.searchContainer : "relative py-1 px-2.5"
}>
<Search
className={
classNames && classNames.searchIcon
? classNames.searchIcon
: " text-subtle absolute mt-2.5 ml-2 h-5 w-5 pb-0.5"
}
/>
<input
ref={searchInputRef}
className={
classNames && classNames.searchBox
? classNames.searchBox
: "border-subtle dark:bg-darkgray-100 focus:border-darkgray-900 text-subtle border-subtle w-full rounded-[6px] border py-2 pl-8 text-sm focus:border-gray-900 focus:outline-none focus:ring-0"
}
autoFocus
type="text"
placeholder={placeholder}
value={value}
onChange={onChange}
name={name}
/>
</div>
);
};
export default SearchInput;

View File

@ -1,233 +0,0 @@
import * as Popover from "@radix-ui/react-popover";
import React, { useCallback, useRef, useState } from "react";
import { classNames as cn } from "@calcom/lib";
import { X, ChevronDown } from "../../../icon";
import Options from "./Options";
import SearchInput from "./SearchInput";
import SelectProvider from "./SelectProvider";
import type { Option } from "./type";
interface SelectProps<T extends Option> {
options: T[];
selectedItems: T | T[] | null;
onChange: (value?: Option | Option[] | null) => void;
placeholder?: string;
isMultiple?: boolean;
isClearable?: boolean;
isSearchable?: boolean;
isDisabled?: boolean;
loading?: boolean;
menuIsOpen?: boolean;
searchInputPlaceholder?: string;
noOptionsMessage?: string;
classNames?: {
menuButton?: ({ isDisabled }: { isDisabled: boolean }) => string;
menu?: string;
tagItem?: ({ isDisabled }: { isDisabled: boolean }) => string;
tagItemText?: string;
tagItemIconContainer?: string;
tagItemIcon?: string;
list?: string;
listGroupLabel?: string;
listItem?: ({ isSelected }: { isSelected: boolean }) => string;
listDisabledItem?: string;
ChevronIcon?: ({ open }: { open: boolean }) => string;
searchContainer?: string;
searchBox?: string;
searchIcon?: string;
closeIcon?: string;
};
}
function Select<T extends Option>({
options = [],
selectedItems = null,
onChange,
placeholder = "Select...",
searchInputPlaceholder = "Search...",
isMultiple = false,
isClearable = false,
isSearchable = false,
isDisabled = false,
menuIsOpen = false,
classNames,
}: SelectProps<T>) {
const [inputValue, setInputValue] = useState<string>("");
const [open, setOpen] = useState(menuIsOpen);
const searchBoxRef = useRef<HTMLInputElement>(null);
const isMultipleValue = Array.isArray(selectedItems) && isMultiple;
const removeItem = useCallback(
(item: Option) => {
// remove the item from the selected items
if (Array.isArray(selectedItems)) {
const newSelectedItems = selectedItems.filter((selectedItem) => selectedItem.value !== item.value);
onChange(newSelectedItems);
} else {
onChange(null);
}
},
[onChange, selectedItems]
);
const closeDropDown = useCallback(() => {
console.log({ open });
if (open) setOpen(false);
}, [open]);
const handleValueChange = useCallback(
(selected: Option) => {
function update() {
if (!isMultiple && !Array.isArray(selectedItems)) {
// Close dropdown when you select an item when we are on single select
closeDropDown();
onChange(selected);
return;
}
// check if the selected item is already selected
if (Array.isArray(selectedItems)) {
const isAlreadySelected = selectedItems.some((item) => item.value === selected.value);
if (isAlreadySelected) {
removeItem(selected);
return;
}
onChange(selectedItems === null ? [selected] : [...selectedItems, selected]);
}
}
if (selected.disabled) return;
if (selected !== selectedItems) {
update();
}
},
[closeDropDown, isMultiple, onChange, selectedItems, removeItem]
);
const clearValue = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
onChange(isMultiple ? [] : null);
},
[onChange, isMultiple]
);
return (
<SelectProvider
options={{
classNames,
}}
selectedItems={selectedItems}
handleValueChange={handleValueChange}>
<div className="relative w-full">
<Popover.Root>
<Popover.Trigger>
<div
className={cn(
"min-w-64 border-default text-muted flex max-h-[36px] items-center justify-between rounded-md border text-sm transition-all duration-300 focus:outline-none",
isDisabled
? " dark:bg-darkgray-200 border-subtle bg-subtle dark:text-subtle text-muted border-subtle"
: "border-subtle dark:bg-darkgray-50 dark:focus:border-darkgray-700 dark:focus:bg-darkgray-100 dark:focus:text-darkgray-900 dark:hover:text-darkgray-900 bg-default hover:border-empthasis focus:border-gray-900"
)}>
<div className="flex w-full grow-0 items-center gap-1 overflow-x-hidden">
<>
{((isMultipleValue && selectedItems.length === 0) || selectedItems === null) && (
<div className="text-muted py-2.5 px-3 dark:text-current">
<p>{placeholder}</p>
</div>
)}
{Array.isArray(selectedItems) ? (
<div className="flex gap-1 overflow-x-scroll p-1 ">
{selectedItems.map((item, index) => (
<div
className={cn(
"dark:bg-darkgray-200 bg-emphasis flex items-center space-x-2 rounded px-2 py-[6px]"
)}
key={index}>
<p
className={cn(
classNames?.tagItemText ??
" text-default cursor-default select-none truncate text-sm leading-none"
)}>
{item.label}
</p>
{!isDisabled && (
<button
onClick={(e) => {
e.stopPropagation();
removeItem(item);
}}>
<X
className={
classNames?.tagItemIcon ??
" dark:hover:text-darkgray-900 hover:text-emphasis text-subtle h-4 w-4"
}
/>
</button>
)}
</div>
))}
</div>
) : (
<div className=" text-emphasis py-2.5 px-3 text-sm leading-none">
<p>{selectedItems?.label}</p>
</div>
)}
</>
</div>
<div className=" text-emphasis flex flex-none items-center rounded-[6px] p-1.5 opacity-75 ">
<>
{isClearable && !isDisabled && selectedItems !== null && (
<div className="cursor-pointer" onClick={clearValue}>
<X
className={
classNames && classNames.closeIcon ? classNames.closeIcon : "h-5 w-5 p-0.5"
}
/>
</div>
)}
<ChevronDown className={cn("h-5 w-5 transition duration-300")} />
</>
</div>
</div>
</Popover.Trigger>
<Popover.Portal>
<Popover.Content>
<div
className={
classNames?.menu ??
"dark:bg-darkgray-100 border-subtle min-w-64 bg-default text-default z-10 mt-1.5 overflow-x-hidden rounded border py-1 text-sm shadow-sm"
}>
{isSearchable && (
<SearchInput
searchInputRef={searchBoxRef}
value={inputValue}
placeholder={searchInputPlaceholder}
onChange={(e) => setInputValue(e.target.value)}
/>
)}
<Options
searchBoxRef={searchBoxRef}
list={options}
inputValue={inputValue}
isMultiple={isMultiple}
selected={selectedItems}
/>
</div>
</Popover.Content>
</Popover.Portal>
</Popover.Root>
</div>
</SelectProvider>
);
}
export default Select;

View File

@ -1,44 +0,0 @@
import React, { createContext, useContext, useMemo } from "react";
import type { ClassNames, Option } from "./type";
interface Store {
selectedItems: Option | Option[] | null;
handleValueChange: (selected: Option) => void;
classNames?: ClassNames;
}
interface Props {
selectedItems: Option | Option[] | null;
handleValueChange: (selected: Option) => void;
children: JSX.Element;
options: {
classNames?: ClassNames;
};
}
export const SelectContext = createContext<Store>({
selectedItems: null,
handleValueChange: (selected) => {
return selected;
},
classNames: undefined,
});
export const useSelectContext = (): Store => {
return useContext(SelectContext);
};
const SelectProvider: React.FC<Props> = ({ selectedItems, handleValueChange, options, children }) => {
const store = useMemo(() => {
return {
selectedItems,
handleValueChange,
classNames: options?.classNames,
} as Store;
}, [handleValueChange, options, selectedItems]);
return <SelectContext.Provider value={store}>{children}</SelectContext.Provider>;
};
export default SelectProvider;

View File

@ -1,26 +0,0 @@
export interface Option {
value?: string;
label: string;
disabled?: boolean;
isSelected?: boolean;
options?: Option[];
leftNode?: React.ReactNode;
}
export interface ClassNames {
menuButton?: (args: { isDisabled: boolean }) => string;
menu?: string;
tagItem?: (args: { isDisabled: boolean }) => string;
tagItemText?: string;
tagItemIconContainer?: string;
tagItemIcon?: string;
list?: string;
listGroupLabel?: string;
listItem?: (args: { isSelected: boolean }) => string;
listDisabledItem?: string;
ChevronIcon?: (args: { open: boolean }) => string;
searchContainer?: string;
searchBox?: string;
searchIcon?: string;
closeIcon?: string;
}

View File

@ -1,3 +0,0 @@
import Select from "./components/Select";
export default Select;

View File

@ -1,85 +0,0 @@
import { Canvas, Meta, Story, ArgsTable } from "@storybook/addon-docs";
import {
Examples,
Example,
Note,
Title,
CustomArgsTable,
VariantRow,
VariantsTable,
} from "@calcom/storybook/components";
import Select from "./components/Select";
<Meta title="UI/Form/CustomSelect" component={Select} />
<Title title="Select" suffix="Brief" subtitle="Version 2.0 — Last Update: 22 Aug 2022" />
export const options = [
{
//4
label: "Cal.com Inc",
options: [
{ value: "UserA", label: "Pro" }, // 5
{ value: "UserB", label: "Teampro" }, // 6
{ value: "UserC", label: "Example" },
{ value: "UserD", label: "Admin", disabled: true },
],
},
{
// 5
label: "Acme Inc",
options: [
{ value: "UserE", label: "Acme Pro" }, // 1 == 6
{ value: "UserF", label: "Acme Teampro" },
{ value: "UserG", label: "Acme example" },
{ value: "UserH", label: "Acme Admin", disabled: true },
],
},
];
export const SelectWithState = (...args) => {
const [value, setValue] = React.useState(options[0].options[0]);
return <Select options={options} selectedItems={value} onChange={(e) => setValue(e)} isClearable />;
};
export const MultiWithState = (...args) => {
const [value, setValue] = React.useState([options[0].options[0]]);
return (
<Select
options={options}
selectedItems={value}
onChange={(e) => {
setValue(e);
}}
isSearchable
isMultiple
isClearable
/>
);
};
<Examples title="State">
<Example title="Single" isFullWidth>
<SelectWithState />
</Example>
<Example title="Multi" isFullWidth>
<MultiWithState />
</Example>
</Examples>
<Canvas>
<Story name="Default">
<div className="flex flex-col space-y-4">
<VariantsTable titles={[]} columnMinWidth={300}>
<VariantRow variant="Single">
<SelectWithState />
</VariantRow>
<VariantRow variant="Multi">
<MultiWithState />
</VariantRow>
</VariantsTable>
</div>
</Story>
</Canvas>