-
-
Save clegirar/df4d2d805683f24d979c62c82be53ba8 to your computer and use it in GitHub Desktop.
Example atomic pattern for `MultiSelectDropdown`
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
<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" }} | |
/> |
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
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