Skip to content

Instantly share code, notes, and snippets.

@clegirar
Created January 7, 2025 17:45
Show Gist options
  • Save clegirar/df4d2d805683f24d979c62c82be53ba8 to your computer and use it in GitHub Desktop.
Save clegirar/df4d2d805683f24d979c62c82be53ba8 to your computer and use it in GitHub Desktop.
Example atomic pattern for `MultiSelectDropdown`
<MultiSelectDropdownWithInput
options={roles.map((role) => ({
id: role.id,
name: role.name,
}))}
onPressItem={(item, value) => {
const retValues = [...member.roles];
if (value === false) {
retValues.splice(retValues.indexOf(item.name), 1);
} else {
retValues.push(item.name);
}
updateMemberRole(member, index, retValues);
}}
values={member.roles}
required={false}
placeHolder="Roles..."
style={{ alignContent: "center" }}
/>
import React, { FC, useState } from "react";
import { TouchableOpacity, View, ViewStyle } from "react-native";
import chevronDownSVG from "@/assets/icons/chevron-down.svg";
import chevronUpSVG from "@/assets/icons/chevron-up.svg";
import { BrandText } from "@/components/BrandText";
import { Checkbox } from "@/components/Checkbox";
import { SVG } from "@/components/SVG";
import { PrimaryBox } from "@/components/boxes/PrimaryBox";
import { TertiaryBox } from "@/components/boxes/TertiaryBox";
import { CustomPressable } from "@/components/buttons/CustomPressable";
import { Label } from "@/components/inputs/TextInputCustom";
import { Separator } from "@/components/separators/Separator";
import { SpacerColumn } from "@/components/spacer";
import { useDropdowns } from "@/hooks/useDropdowns";
import {
neutral22,
neutral44,
neutral55,
neutral77,
secondaryColor,
} from "@/utils/style/colors";
import { fontRegular14 } from "@/utils/style/fonts";
import { layout } from "@/utils/style/layout";
// Miss naming because it's not an input
// Values instead of items ? better naming ?
interface Options {
id: string;
name: string;
}
interface DropdownProps {
options: Options[];
onPressItem: (item: Options, value: boolean) => void;
values: string[];
}
// The MultipleSelectDropdown component (we can do the same atomic design for this component too)
// Reusable easily
const MultiSelectDropdownBase: FC<DropdownProps> = ({
options,
values,
onPressItem,
}) => {
return (
<PrimaryBox
style={{
position: "absolute",
top: 44,
right: 0,
width: "100%",
paddingHorizontal: layout.spacing_x1_5,
paddingBottom: layout.spacing_x1,
backgroundColor: neutral44,
borderColor: neutral55,
}}
>
{options.map((item, index) => (
<TouchableOpacity
onPress={() => {
onPressItem(item, !values.includes(item.name));
}}
style={{ paddingTop: layout.spacing_x1_5, width: "100%" }}
>
<View style={{ flexDirection: "row" }}>
<Checkbox
isChecked={values.includes(item.name)}
style={{ marginRight: layout.spacing_x1 }}
/>
<BrandText style={[fontRegular14, { color: secondaryColor }]}>
{item.name}
</BrandText>
</View>
{options.length - 1 !== index && (
<>
<SpacerColumn size={1} />
<Separator color={neutral55} />
</>
)}
</TouchableOpacity>
))}
</PrimaryBox>
);
};
interface Props {
options: Options[];
values: string[];
onPressItem: (item: Options, value: boolean) => void;
label?: string;
style?: ViewStyle;
onDropdownClosed?: () => void;
placeHolder?: string;
required?: boolean;
}
// This component is the base, and have to stay the base, only callable by less atomic components
// For example `children` props is a very generic props so we want to limitate the uses of this component
// And only be callable by other components but not directly from screens
// This is what i call `Base`
const MultiSelectDropdownWithLogicBase: FC<
Omit<Props, "placeHolder"> & {
children?: React.ReactNode;
// proposal to have the possibility to recup the state of the base component
// we can put the logic in the higher levels components but: more props to pass (5 props), code repetition (not very problematic for me i think, because it's logic, and not components)
// but seems logic to let higher levels comonents to have the logic and let the base dumb, but pass 5 props i don't fint it good neither..
// i don't have opinion for the moment, continue to thinking about it
handleState?: (isDropdownOpen: boolean, isHovered: boolean) => void;
}
> = ({
style,
options,
values,
label,
onPressItem,
required = true,
children,
handleState,
}) => {
const [isDropdownOpen, setDropdownState, ref] = useDropdowns();
const [hovered, setHovered] = useState(false);
React.useEffect(() => {
handleState && handleState(isDropdownOpen, hovered);
}, [isDropdownOpen, hovered, handleState]);
return (
<View
style={[{ zIndex: 2, width: "100%", minHeight: 40 }, style]}
ref={ref}
>
<CustomPressable
onHoverIn={() => setHovered(true)}
onHoverOut={() => setHovered(false)}
onPress={() => setDropdownState(!isDropdownOpen)}
>
{label && (
<>
<Label isRequired={required} hovered={hovered}>
{label}
</Label>
<SpacerColumn size={1.5} />
</>
)}
{children && children}
{isDropdownOpen && (
<MultiSelectDropdownBase
options={options}
values={values}
onPressItem={onPressItem}
/>
)}
</CustomPressable>
</View>
);
};
// And here you want to say "why don't use directly the base component, because we add nothing here"
// NO ! We have to keep the same layer level than other components that call the base (more understandable)
// Title more clear
// Future proof, if you want to add something here, component already exist and the base can handle a children
// Can be consider as the `default` one
export const MultiSelectDropdown: FC<Props> = (props) => {
return <MultiSelectDropdownWithLogicBase {...props} />;
};
// An example of `MultipleSelectDropdownBase` with a children, here an input (it's not an input actually)
// And an example of the call of handleState, but i don't think that i find that good
export const MultiSelectDropdownWithInput: FC<Props> = (props) => {
const [dropdownState, setDropdownState] = useState<{
isDropdownOpen: boolean;
isHovered: boolean;
}>({ isDropdownOpen: false, isHovered: false });
const handleDropdownState = (isDropdownOpen: boolean, isHovered: boolean) => {
setDropdownState({ isDropdownOpen, isHovered });
};
return (
<MultiSelectDropdownWithLogicBase
{...props}
handleState={handleDropdownState}
>
{/* The `input` */}
<View>
<TertiaryBox
style={[
{
width: "100%",
height: 40,
flexDirection: "row",
paddingHorizontal: layout.spacing_x1_5,
backgroundColor: neutral22,
alignItems: "center",
},
dropdownState.isHovered && { borderColor: secondaryColor },
]}
>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
flex: 1,
}}
>
<BrandText
style={[
fontRegular14,
{
marginRight: layout.spacing_x1,
color: props.values?.length > 0 ? secondaryColor : neutral77,
},
]}
numberOfLines={1}
>
{props.values?.length > 0
? props.values.join(", ")
: props.placeHolder}
</BrandText>
<SVG
source={
dropdownState.isDropdownOpen ? chevronUpSVG : chevronDownSVG
}
width={16}
height={16}
color={secondaryColor}
/>
</View>
</TertiaryBox>
</View>
</MultiSelectDropdownWithLogicBase>
);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment