perf: optimize icon selector
This commit is contained in:
parent
cf959c7088
commit
5e3cee079b
@ -228,16 +228,8 @@ export const HabitCreateForm: React.FC<HabitCreateFormProps> = ({ user }) => {
|
|||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, onBlur, value } }) => {
|
render={({ field: { onChange, value } }) => {
|
||||||
return (
|
return (
|
||||||
// <TextInput
|
|
||||||
// placeholder="Icon"
|
|
||||||
// onBlur={onBlur}
|
|
||||||
// onChangeText={onChange}
|
|
||||||
// value={value}
|
|
||||||
// style={[styles.spacing, { width: "90%" }]}
|
|
||||||
// mode="outlined"
|
|
||||||
// />
|
|
||||||
<HabitIconSelectorModal
|
<HabitIconSelectorModal
|
||||||
visible
|
visible
|
||||||
value={value}
|
value={value}
|
||||||
|
@ -0,0 +1,56 @@
|
|||||||
|
import type { IconName } from "@fortawesome/fontawesome-svg-core"
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome"
|
||||||
|
import React, { memo } from "react"
|
||||||
|
import { View, StyleSheet } from "react-native"
|
||||||
|
import { IconButton, Text } from "react-native-paper"
|
||||||
|
|
||||||
|
export interface HabitIconListProps {
|
||||||
|
selectedIcon?: string
|
||||||
|
possibleIcons: string[]
|
||||||
|
handleIconSelect: (icon: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const HabitIconListWithoutMemo: React.FC<HabitIconListProps> = (props) => {
|
||||||
|
const { selectedIcon, possibleIcons, handleIconSelect } = props
|
||||||
|
if (possibleIcons.length > 0) {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
{possibleIcons.map((icon) => {
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
key={icon}
|
||||||
|
containerColor="white"
|
||||||
|
icon={({ size }) => {
|
||||||
|
return (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={icon as IconName}
|
||||||
|
size={size}
|
||||||
|
color={selectedIcon === icon ? "blue" : "black"}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
size={30}
|
||||||
|
onPress={() => {
|
||||||
|
handleIconSelect(icon)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<View style={styles.noResults}>
|
||||||
|
<Text>No results found</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
noResults: {
|
||||||
|
marginTop: 20,
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const HabitIconList = memo(HabitIconListWithoutMemo)
|
@ -1,22 +1,16 @@
|
|||||||
import React, { useState, useEffect } from "react"
|
import { library } from "@fortawesome/fontawesome-svg-core"
|
||||||
import {
|
|
||||||
Modal,
|
|
||||||
IconButton,
|
|
||||||
Portal,
|
|
||||||
List,
|
|
||||||
Button,
|
|
||||||
TextInput,
|
|
||||||
Text,
|
|
||||||
} from "react-native-paper"
|
|
||||||
import { ScrollView, View, StyleSheet } from "react-native"
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome"
|
|
||||||
import type {
|
|
||||||
IconDefinition,
|
|
||||||
IconName,
|
|
||||||
} from "@fortawesome/fontawesome-svg-core"
|
|
||||||
import { library, findIconDefinition } from "@fortawesome/fontawesome-svg-core"
|
|
||||||
import { fas } from "@fortawesome/free-solid-svg-icons"
|
import { fas } from "@fortawesome/free-solid-svg-icons"
|
||||||
|
import React, {
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
useTransition,
|
||||||
|
} from "react"
|
||||||
|
import { ScrollView, StyleSheet, View } from "react-native"
|
||||||
|
import { Button, List, Modal, Portal, TextInput } from "react-native-paper"
|
||||||
|
|
||||||
|
import { HabitIconList } from "./HabitIconList"
|
||||||
export interface HabitIconSelectorModalProps {
|
export interface HabitIconSelectorModalProps {
|
||||||
visible?: boolean
|
visible?: boolean
|
||||||
value: string
|
value: string
|
||||||
@ -25,6 +19,34 @@ export interface HabitIconSelectorModalProps {
|
|||||||
onPress: () => void
|
onPress: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SearchInputProps {
|
||||||
|
searchText: string
|
||||||
|
handleSearch: (text: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
library.add(fas)
|
||||||
|
|
||||||
|
const iconNames = Object.keys(fas).map((key) => {
|
||||||
|
return fas[key]?.iconName ?? ""
|
||||||
|
})
|
||||||
|
|
||||||
|
const findIconsInLibrary = (icon: string): string[] => {
|
||||||
|
return iconNames
|
||||||
|
.filter((name, index, self) => {
|
||||||
|
return name.includes(icon) && self.indexOf(name) === index
|
||||||
|
})
|
||||||
|
.slice(0, 20)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SearchInputWithoutMemo: React.FC<SearchInputProps> = (props) => {
|
||||||
|
const { searchText, handleSearch } = props
|
||||||
|
return (
|
||||||
|
<TextInput label="Search" value={searchText} onChangeText={handleSearch} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SearchInput = memo(SearchInputWithoutMemo)
|
||||||
|
|
||||||
export const HabitIconSelectorModal: React.FC<HabitIconSelectorModalProps> = ({
|
export const HabitIconSelectorModal: React.FC<HabitIconSelectorModalProps> = ({
|
||||||
visible = false,
|
visible = false,
|
||||||
value,
|
value,
|
||||||
@ -33,36 +55,32 @@ export const HabitIconSelectorModal: React.FC<HabitIconSelectorModalProps> = ({
|
|||||||
onPress,
|
onPress,
|
||||||
}) => {
|
}) => {
|
||||||
const [selectedIcon, setSelectedIcon] = useState<string>()
|
const [selectedIcon, setSelectedIcon] = useState<string>()
|
||||||
const [possibleIcons, setPossibleIcons] = useState<IconDefinition[]>([])
|
const [possibleIcons, setPossibleIcons] = useState<string[]>([])
|
||||||
const [searchText, setSearchText] = useState<string>(value)
|
const [searchText, setSearchText] = useState<string>(value)
|
||||||
|
const [isPending, startTransition] = useTransition()
|
||||||
|
|
||||||
|
const handleSearch = useCallback((text: string): void => {
|
||||||
|
setSearchText(text)
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const delay = setTimeout(() => {
|
||||||
|
startTransition(() => {
|
||||||
setPossibleIcons(findIconsInLibrary(searchText))
|
setPossibleIcons(findIconsInLibrary(searchText))
|
||||||
}, [searchText])
|
})
|
||||||
|
}, 500)
|
||||||
|
return () => {
|
||||||
|
return clearTimeout(delay)
|
||||||
|
}
|
||||||
|
}, [searchText, isPending])
|
||||||
|
|
||||||
library.add(fas)
|
const handleIconSelect = useCallback(
|
||||||
|
(icon: string): void => {
|
||||||
const handleIconSelect = (icon: string): void => {
|
|
||||||
setSelectedIcon(icon)
|
setSelectedIcon(icon)
|
||||||
onSelect(icon)
|
onSelect(icon)
|
||||||
}
|
},
|
||||||
|
[onSelect],
|
||||||
const findIconsInLibrary = (icon: string): IconDefinition[] => {
|
)
|
||||||
const iconNames = Object.keys(fas).map((key) => {
|
|
||||||
return fas[key]?.iconName ?? ""
|
|
||||||
})
|
|
||||||
const matchedValue: string[] = iconNames.filter((name) => {
|
|
||||||
return name.includes(icon)
|
|
||||||
})
|
|
||||||
return matchedValue.length > 0
|
|
||||||
? matchedValue.map((name) => {
|
|
||||||
return findIconDefinition({
|
|
||||||
prefix: "fas",
|
|
||||||
iconName: name as IconName,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
: []
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -77,46 +95,15 @@ export const HabitIconSelectorModal: React.FC<HabitIconSelectorModalProps> = ({
|
|||||||
<ScrollView style={styles.scrollView}>
|
<ScrollView style={styles.scrollView}>
|
||||||
<List.Section title="Select an icon">
|
<List.Section title="Select an icon">
|
||||||
<View style={styles.iconContainer}>
|
<View style={styles.iconContainer}>
|
||||||
<TextInput
|
<SearchInput
|
||||||
label="Search"
|
searchText={searchText}
|
||||||
value={searchText}
|
handleSearch={handleSearch}
|
||||||
onChangeText={(text) => {
|
|
||||||
return setSearchText(text)
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
{possibleIcons.length > 0 ? (
|
<HabitIconList
|
||||||
<View>
|
selectedIcon={selectedIcon}
|
||||||
{possibleIcons.map((icon) => {
|
possibleIcons={possibleIcons}
|
||||||
return (
|
handleIconSelect={handleIconSelect}
|
||||||
<IconButton
|
|
||||||
key={icon.iconName}
|
|
||||||
containerColor="white"
|
|
||||||
icon={({ size }) => {
|
|
||||||
return (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={icon}
|
|
||||||
size={size}
|
|
||||||
color={
|
|
||||||
selectedIcon === icon.iconName
|
|
||||||
? "blue"
|
|
||||||
: "black"
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)
|
|
||||||
}}
|
|
||||||
size={30}
|
|
||||||
onPress={() => {
|
|
||||||
handleIconSelect(icon.iconName)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<View style={styles.noResults}>
|
|
||||||
<Text>No results found</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
</List.Section>
|
</List.Section>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
Reference in New Issue
Block a user