move TagInput into dir, use button on desktop
This commit is contained in:
@@ -48,7 +48,7 @@ export class TagsAutocompleteModel {
|
|||||||
isActive = false
|
isActive = false
|
||||||
query = ''
|
query = ''
|
||||||
searchedTags: string[] = []
|
searchedTags: string[] = []
|
||||||
profileTags: string[] = []
|
profileTags: string[] = ['biology']
|
||||||
|
|
||||||
constructor(public rootStore: RootStoreModel) {
|
constructor(public rootStore: RootStoreModel) {
|
||||||
makeAutoObservable(
|
makeAutoObservable(
|
||||||
|
|||||||
+13
-3
@@ -109,11 +109,11 @@ export function TagButton({
|
|||||||
onPointerLeave={hoverOut}
|
onPointerLeave={hoverOut}
|
||||||
style={state => [
|
style={state => [
|
||||||
pal.viewLight,
|
pal.viewLight,
|
||||||
styles.editableTag,
|
styles.tagButton,
|
||||||
{
|
{
|
||||||
opacity: state.pressed || state.focused ? 0.8 : 1,
|
|
||||||
outline: 0,
|
outline: 0,
|
||||||
paddingRight: 6,
|
opacity: state.pressed || state.focused ? 0.6 : 1,
|
||||||
|
paddingRight: 10,
|
||||||
},
|
},
|
||||||
]}>
|
]}>
|
||||||
<Text type="md-medium" style={[pal.textLight]}>
|
<Text type="md-medium" style={[pal.textLight]}>
|
||||||
@@ -145,4 +145,14 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
|
tagButton: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
flexShrink: 1,
|
||||||
|
paddingVertical: 6,
|
||||||
|
paddingTop: 5,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
borderRadius: 20,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,311 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import {
|
|
||||||
TextInput,
|
|
||||||
View,
|
|
||||||
StyleSheet,
|
|
||||||
NativeSyntheticEvent,
|
|
||||||
TextInputKeyPressEventData,
|
|
||||||
Platform,
|
|
||||||
Pressable,
|
|
||||||
} from 'react-native'
|
|
||||||
import {
|
|
||||||
FontAwesomeIcon,
|
|
||||||
FontAwesomeIconStyle,
|
|
||||||
} from '@fortawesome/react-native-fontawesome'
|
|
||||||
import {Pin} from 'pind'
|
|
||||||
|
|
||||||
import {TagsAutocompleteModel} from 'state/models/ui/tags-autocomplete'
|
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
|
||||||
import {EditableTag} from 'view/com/Tag'
|
|
||||||
import {Text} from 'view/com/util/text/Text'
|
|
||||||
|
|
||||||
function uniq(tags: string[]) {
|
|
||||||
return Array.from(new Set(tags))
|
|
||||||
}
|
|
||||||
|
|
||||||
function sanitize(tagString: string) {
|
|
||||||
return tagString.trim().replace(/^#/, '')
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TagInput({
|
|
||||||
max = 8,
|
|
||||||
onChangeTags,
|
|
||||||
tagsAutocompleteModel: model,
|
|
||||||
}: {
|
|
||||||
max?: number
|
|
||||||
onChangeTags: (tags: string[]) => void
|
|
||||||
tagsAutocompleteModel: TagsAutocompleteModel
|
|
||||||
}) {
|
|
||||||
const pal = usePalette('default')
|
|
||||||
const dropdown = React.useRef<HTMLDivElement>(null)
|
|
||||||
const input = React.useRef<HTMLInputElement>(null)
|
|
||||||
const inputWidth = input.current
|
|
||||||
? input.current.getBoundingClientRect().width
|
|
||||||
: 200
|
|
||||||
|
|
||||||
const [value, setValue] = React.useState('')
|
|
||||||
const [tags, setTags] = React.useState<string[]>([])
|
|
||||||
const [dropdownIsOpen, setDropdownIsOpen] = React.useState(false)
|
|
||||||
const [dropdownItems, setDropdownItems] = React.useState<
|
|
||||||
{value: string; label: string}[]
|
|
||||||
>([])
|
|
||||||
const [selectedItemIndex, setSelectedItemIndex] = React.useState(0)
|
|
||||||
|
|
||||||
const close = React.useCallback(() => {
|
|
||||||
setDropdownIsOpen(false)
|
|
||||||
model.setActive(false)
|
|
||||||
setSelectedItemIndex(0)
|
|
||||||
setDropdownItems([])
|
|
||||||
}, [model, setDropdownIsOpen, setSelectedItemIndex, setDropdownItems])
|
|
||||||
|
|
||||||
const addTags = React.useCallback(
|
|
||||||
(_tags: string[]) => {
|
|
||||||
setTags(_tags)
|
|
||||||
onChangeTags(_tags)
|
|
||||||
},
|
|
||||||
[onChangeTags, setTags],
|
|
||||||
)
|
|
||||||
|
|
||||||
const removeTag = React.useCallback(
|
|
||||||
(tag: string) => {
|
|
||||||
addTags(tags.filter(t => t !== tag))
|
|
||||||
},
|
|
||||||
[tags, addTags],
|
|
||||||
)
|
|
||||||
|
|
||||||
const addTagAndReset = React.useCallback(
|
|
||||||
(value: string) => {
|
|
||||||
const tag = sanitize(value)
|
|
||||||
|
|
||||||
// enforce max hashtag length
|
|
||||||
if (tag.length > 0 && tag.length <= 64) {
|
|
||||||
addTags(uniq([...tags, tag]).slice(0, max))
|
|
||||||
}
|
|
||||||
|
|
||||||
setValue('')
|
|
||||||
input.current?.focus()
|
|
||||||
close()
|
|
||||||
},
|
|
||||||
[max, tags, close, setValue, addTags],
|
|
||||||
)
|
|
||||||
|
|
||||||
const onSubmitEditing = React.useCallback(() => {
|
|
||||||
const item = dropdownItems[selectedItemIndex]
|
|
||||||
addTagAndReset(item?.value || value)
|
|
||||||
}, [value, dropdownItems, selectedItemIndex, addTagAndReset])
|
|
||||||
|
|
||||||
const onKeyPress = React.useCallback(
|
|
||||||
(e: NativeSyntheticEvent<TextInputKeyPressEventData>) => {
|
|
||||||
const {key} = e.nativeEvent
|
|
||||||
|
|
||||||
if (key === 'Backspace' && value === '') {
|
|
||||||
addTags(tags.slice(0, -1))
|
|
||||||
} else if (key === ' ') {
|
|
||||||
e.preventDefault() // prevents an additional space on web
|
|
||||||
addTagAndReset(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dropdownIsOpen) {
|
|
||||||
if (key === 'Escape') {
|
|
||||||
close()
|
|
||||||
} else if (key === 'ArrowUp') {
|
|
||||||
e.preventDefault()
|
|
||||||
setSelectedItemIndex(
|
|
||||||
(selectedItemIndex + dropdownItems.length - 1) %
|
|
||||||
dropdownItems.length,
|
|
||||||
)
|
|
||||||
} else if (key === 'ArrowDown') {
|
|
||||||
e.preventDefault()
|
|
||||||
setSelectedItemIndex((selectedItemIndex + 1) % dropdownItems.length)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[
|
|
||||||
value,
|
|
||||||
tags,
|
|
||||||
dropdownIsOpen,
|
|
||||||
selectedItemIndex,
|
|
||||||
dropdownItems.length,
|
|
||||||
close,
|
|
||||||
setSelectedItemIndex,
|
|
||||||
addTags,
|
|
||||||
addTagAndReset,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
const onChangeText = React.useCallback(
|
|
||||||
async (v: string) => {
|
|
||||||
setValue(v)
|
|
||||||
|
|
||||||
if (v.length > 0) {
|
|
||||||
model.setActive(true)
|
|
||||||
await model.search(v)
|
|
||||||
|
|
||||||
setDropdownItems(
|
|
||||||
model.suggestions.map(item => ({
|
|
||||||
value: item,
|
|
||||||
label: item,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
|
|
||||||
setDropdownIsOpen(true)
|
|
||||||
} else {
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[model, setValue, setDropdownIsOpen, close],
|
|
||||||
)
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
// outside click
|
|
||||||
function onClick(e: MouseEvent) {
|
|
||||||
const drop = dropdown.current
|
|
||||||
const control = input.current
|
|
||||||
|
|
||||||
if (
|
|
||||||
!drop ||
|
|
||||||
!control ||
|
|
||||||
e.target === drop ||
|
|
||||||
e.target === control ||
|
|
||||||
drop.contains(e.target as Node) ||
|
|
||||||
control.contains(e.target as Node)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('click', onClick)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('click', onClick)
|
|
||||||
}
|
|
||||||
}, [close])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={styles.outer}>
|
|
||||||
{!tags.length && (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="tags"
|
|
||||||
size={14}
|
|
||||||
style={pal.textLight as FontAwesomeIconStyle}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{tags.map(tag => (
|
|
||||||
<EditableTag key={tag} value={tag} onRemove={removeTag} />
|
|
||||||
))}
|
|
||||||
|
|
||||||
{tags.length >= max ? null : (
|
|
||||||
<TextInput
|
|
||||||
ref={input as any}
|
|
||||||
id="tags-autocomplete-input"
|
|
||||||
role={'listbox' as any}
|
|
||||||
aria-controls="tags-autocomplete-dropdown"
|
|
||||||
aria-haspopup="listbox"
|
|
||||||
aria-expanded={dropdownIsOpen}
|
|
||||||
value={value}
|
|
||||||
onKeyPress={onKeyPress}
|
|
||||||
onSubmitEditing={onSubmitEditing}
|
|
||||||
onChangeText={onChangeText}
|
|
||||||
blurOnSubmit={false}
|
|
||||||
style={[styles.input, pal.textLight]}
|
|
||||||
placeholder="Enter a tag and press enter"
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoCorrect={false}
|
|
||||||
autoComplete="off"
|
|
||||||
accessible={true}
|
|
||||||
accessibilityLabel="Add tags to your post"
|
|
||||||
accessibilityHint={`Type a tag and press enter to add it. You can add up to ${max} tag.`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Pin
|
|
||||||
pinned={Boolean(dropdownIsOpen && dropdownItems.length)}
|
|
||||||
to={input}
|
|
||||||
at="bottomLeft"
|
|
||||||
from="topLeft"
|
|
||||||
style={{width: inputWidth}}>
|
|
||||||
<View
|
|
||||||
ref={dropdown as any}
|
|
||||||
style={[pal.view, pal.borderDark, styles.dropdown]}
|
|
||||||
role={'listbox' as any}
|
|
||||||
id="tags-autocomplete-dropdown">
|
|
||||||
{dropdownItems.map((item, index) => {
|
|
||||||
const isFirst = index === 0
|
|
||||||
const isLast = index === dropdownItems.length - 1
|
|
||||||
return (
|
|
||||||
<Pressable
|
|
||||||
accessibilityRole="button"
|
|
||||||
key={item.value}
|
|
||||||
onPress={() => addTagAndReset(item.value)}
|
|
||||||
style={state => [
|
|
||||||
pal.border,
|
|
||||||
styles.dropdownItem,
|
|
||||||
{
|
|
||||||
backgroundColor: state.hovered
|
|
||||||
? pal.viewLight.backgroundColor
|
|
||||||
: undefined,
|
|
||||||
},
|
|
||||||
selectedItemIndex === index ? pal.viewLight : undefined,
|
|
||||||
isFirst
|
|
||||||
? styles.firstResult
|
|
||||||
: isLast
|
|
||||||
? styles.lastResult
|
|
||||||
: undefined,
|
|
||||||
]}>
|
|
||||||
<Text type="md" style={pal.textLight} numberOfLines={1}>
|
|
||||||
{item.label}
|
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</View>
|
|
||||||
</Pin>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
outer: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 8,
|
|
||||||
},
|
|
||||||
input: {
|
|
||||||
flexGrow: 1,
|
|
||||||
minWidth: 100,
|
|
||||||
fontSize: 15,
|
|
||||||
lineHeight: Platform.select({
|
|
||||||
web: 20,
|
|
||||||
native: 18,
|
|
||||||
}),
|
|
||||||
paddingTop: 4,
|
|
||||||
paddingBottom: 4,
|
|
||||||
},
|
|
||||||
dropdown: {
|
|
||||||
width: '100%',
|
|
||||||
borderRadius: 6,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderStyle: 'solid',
|
|
||||||
padding: 4,
|
|
||||||
},
|
|
||||||
dropdownItem: {
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
flexDirection: 'row',
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
paddingVertical: 8,
|
|
||||||
gap: 4,
|
|
||||||
},
|
|
||||||
firstResult: {
|
|
||||||
borderTopLeftRadius: 2,
|
|
||||||
borderTopRightRadius: 2,
|
|
||||||
},
|
|
||||||
lastResult: {
|
|
||||||
borderBottomLeftRadius: 2,
|
|
||||||
borderBottomRightRadius: 2,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {View, StyleSheet, Pressable} from 'react-native'
|
||||||
|
import {
|
||||||
|
FontAwesomeIcon,
|
||||||
|
FontAwesomeIconStyle,
|
||||||
|
} from '@fortawesome/react-native-fontawesome'
|
||||||
|
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {Text} from 'view/com/util/text/Text'
|
||||||
|
|
||||||
|
export function TagInputEntryButton({
|
||||||
|
tags,
|
||||||
|
onRequestOpen,
|
||||||
|
}: {
|
||||||
|
tags: string[]
|
||||||
|
onRequestOpen: () => void
|
||||||
|
}) {
|
||||||
|
const pal = usePalette('default')
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
accessibilityRole="button"
|
||||||
|
style={state => [
|
||||||
|
styles.selectedTags,
|
||||||
|
{
|
||||||
|
outline: 0,
|
||||||
|
opacity: state.pressed || state.focused ? 0.6 : 1,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onPress={onRequestOpen}>
|
||||||
|
<View style={[pal.viewLight, styles.button]}>
|
||||||
|
{tags.length ? (
|
||||||
|
<Text type="md-medium" style={[pal.textLight]}>
|
||||||
|
Add +
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="tags"
|
||||||
|
size={12}
|
||||||
|
style={pal.textLight as FontAwesomeIconStyle}
|
||||||
|
/>
|
||||||
|
<Text type="md-medium" style={[pal.textLight]}>
|
||||||
|
Click to add tags to your post
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{tags.map(tag => (
|
||||||
|
<View key={tag} style={[pal.viewLight, styles.button]}>
|
||||||
|
<Text type="md-medium" style={[pal.textLight]}>
|
||||||
|
#{tag}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
selectedTags: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
flexShrink: 1,
|
||||||
|
paddingVertical: 6,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
borderRadius: 20,
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,366 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
TextInput,
|
||||||
|
View,
|
||||||
|
StyleSheet,
|
||||||
|
NativeSyntheticEvent,
|
||||||
|
TextInputKeyPressEventData,
|
||||||
|
Platform,
|
||||||
|
Pressable,
|
||||||
|
} from 'react-native'
|
||||||
|
import {
|
||||||
|
FontAwesomeIcon,
|
||||||
|
FontAwesomeIconStyle,
|
||||||
|
} from '@fortawesome/react-native-fontawesome'
|
||||||
|
import {Pin} from 'pind'
|
||||||
|
|
||||||
|
import {TagsAutocompleteModel} from 'state/models/ui/tags-autocomplete'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {TagButton} from 'view/com/Tag'
|
||||||
|
import {Text} from 'view/com/util/text/Text'
|
||||||
|
import {useStores} from 'state/index'
|
||||||
|
import {TagInputEntryButton} from './TagInputEntryButton'
|
||||||
|
import {TextInputFocusEventData} from 'react-native'
|
||||||
|
|
||||||
|
function uniq(tags: string[]) {
|
||||||
|
return Array.from(new Set(tags))
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitize(tagString: string) {
|
||||||
|
return tagString.trim().replace(/^#/, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TagInput({
|
||||||
|
max = 8,
|
||||||
|
onChangeTags,
|
||||||
|
}: {
|
||||||
|
max?: number
|
||||||
|
onChangeTags: (tags: string[]) => void
|
||||||
|
}) {
|
||||||
|
const store = useStores()
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const dropdown = React.useRef<HTMLDivElement>(null)
|
||||||
|
const input = React.useRef<HTMLInputElement>(null)
|
||||||
|
const inputWidth = input.current
|
||||||
|
? input.current.getBoundingClientRect().width
|
||||||
|
: 200
|
||||||
|
const model = React.useMemo(() => new TagsAutocompleteModel(store), [store])
|
||||||
|
const containerRef = React.useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const [value, setValue] = React.useState('')
|
||||||
|
const [tags, setTags] = React.useState<string[]>([])
|
||||||
|
const [dropdownIsOpen, setDropdownIsOpen] = React.useState(false)
|
||||||
|
const [dropdownItems, setDropdownItems] = React.useState<
|
||||||
|
{value: string; label: string}[]
|
||||||
|
>([])
|
||||||
|
const [selectedItemIndex, setSelectedItemIndex] = React.useState(0)
|
||||||
|
const [open, setOpen] = React.useState(false)
|
||||||
|
|
||||||
|
const openTagInput = React.useCallback(async () => {
|
||||||
|
setOpen(true)
|
||||||
|
}, [setOpen])
|
||||||
|
|
||||||
|
const closeDropdownAndReset = React.useCallback(() => {
|
||||||
|
setDropdownIsOpen(false)
|
||||||
|
model.setActive(false)
|
||||||
|
setSelectedItemIndex(0)
|
||||||
|
setDropdownItems([])
|
||||||
|
}, [model, setDropdownIsOpen, setSelectedItemIndex, setDropdownItems])
|
||||||
|
|
||||||
|
const addTags = React.useCallback(
|
||||||
|
(_tags: string[]) => {
|
||||||
|
setTags(_tags)
|
||||||
|
onChangeTags(_tags)
|
||||||
|
|
||||||
|
if (!_tags.length && document.activeElement !== input.current)
|
||||||
|
setOpen(false)
|
||||||
|
},
|
||||||
|
[onChangeTags, setTags, setOpen],
|
||||||
|
)
|
||||||
|
|
||||||
|
const removeTag = React.useCallback(
|
||||||
|
(tag: string) => {
|
||||||
|
addTags(tags.filter(t => t !== tag))
|
||||||
|
},
|
||||||
|
[tags, addTags],
|
||||||
|
)
|
||||||
|
|
||||||
|
const addTagAndReset = React.useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
const tag = sanitize(value)
|
||||||
|
|
||||||
|
// enforce max hashtag length
|
||||||
|
if (tag.length > 0 && tag.length <= 64) {
|
||||||
|
addTags(uniq([...tags, tag]).slice(0, max))
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue('')
|
||||||
|
input.current?.focus()
|
||||||
|
closeDropdownAndReset()
|
||||||
|
},
|
||||||
|
[max, tags, closeDropdownAndReset, setValue, addTags],
|
||||||
|
)
|
||||||
|
|
||||||
|
const onSubmitEditing = React.useCallback(() => {
|
||||||
|
const item = dropdownItems[selectedItemIndex]
|
||||||
|
addTagAndReset(item?.value || value)
|
||||||
|
}, [value, dropdownItems, selectedItemIndex, addTagAndReset])
|
||||||
|
|
||||||
|
const onKeyPress = React.useCallback(
|
||||||
|
(e: NativeSyntheticEvent<TextInputKeyPressEventData>) => {
|
||||||
|
const {key} = e.nativeEvent
|
||||||
|
|
||||||
|
if (key === 'Backspace' && value === '') {
|
||||||
|
addTags(tags.slice(0, -1))
|
||||||
|
} else if (key === ' ') {
|
||||||
|
e.preventDefault() // prevents an additional space on web
|
||||||
|
addTagAndReset(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dropdownIsOpen) {
|
||||||
|
if (key === 'Escape') {
|
||||||
|
closeDropdownAndReset()
|
||||||
|
} else if (key === 'ArrowUp') {
|
||||||
|
e.preventDefault()
|
||||||
|
setSelectedItemIndex(
|
||||||
|
(selectedItemIndex + dropdownItems.length - 1) %
|
||||||
|
dropdownItems.length,
|
||||||
|
)
|
||||||
|
} else if (key === 'ArrowDown') {
|
||||||
|
e.preventDefault()
|
||||||
|
setSelectedItemIndex((selectedItemIndex + 1) % dropdownItems.length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
value,
|
||||||
|
tags,
|
||||||
|
dropdownIsOpen,
|
||||||
|
selectedItemIndex,
|
||||||
|
dropdownItems.length,
|
||||||
|
closeDropdownAndReset,
|
||||||
|
setSelectedItemIndex,
|
||||||
|
addTags,
|
||||||
|
addTagAndReset,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
const onChangeText = React.useCallback(
|
||||||
|
async (v: string) => {
|
||||||
|
setValue(v)
|
||||||
|
|
||||||
|
if (v.length > 0) {
|
||||||
|
model.setActive(true)
|
||||||
|
await model.search(v)
|
||||||
|
|
||||||
|
setDropdownItems(
|
||||||
|
model.suggestions.map(item => ({
|
||||||
|
value: item,
|
||||||
|
label: item,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
|
setDropdownIsOpen(true)
|
||||||
|
} else {
|
||||||
|
closeDropdownAndReset()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[model, setValue, setDropdownIsOpen, closeDropdownAndReset],
|
||||||
|
)
|
||||||
|
|
||||||
|
const onFocus = React.useCallback(async () => {
|
||||||
|
model.setActive(true)
|
||||||
|
await model.search('')
|
||||||
|
|
||||||
|
setDropdownItems(
|
||||||
|
model.suggestions.map(item => ({
|
||||||
|
value: item,
|
||||||
|
label: item,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
|
setDropdownIsOpen(true)
|
||||||
|
}, [model, setDropdownIsOpen])
|
||||||
|
|
||||||
|
const onBlur = React.useCallback(
|
||||||
|
(e: NativeSyntheticEvent<TextInputFocusEventData>) => {
|
||||||
|
// @ts-ignore
|
||||||
|
const target = e.nativeEvent.relatedTarget as HTMLElement | undefined
|
||||||
|
|
||||||
|
if (
|
||||||
|
!tags.length &&
|
||||||
|
(!target || !target.id.includes('tag_autocomplete_option'))
|
||||||
|
) {
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[tags, setOpen],
|
||||||
|
)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// outside click
|
||||||
|
function onClick(e: MouseEvent) {
|
||||||
|
const drop = dropdown.current
|
||||||
|
const control = input.current
|
||||||
|
|
||||||
|
if (
|
||||||
|
!drop ||
|
||||||
|
!control ||
|
||||||
|
e.target === drop ||
|
||||||
|
e.target === control ||
|
||||||
|
drop.contains(e.target as Node) ||
|
||||||
|
control.contains(e.target as Node)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
closeDropdownAndReset()
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', onClick)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('click', onClick)
|
||||||
|
}
|
||||||
|
}, [closeDropdownAndReset])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View ref={containerRef as any}>
|
||||||
|
{!open && (
|
||||||
|
<TagInputEntryButton tags={tags} onRequestOpen={openTagInput} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{open && (
|
||||||
|
<View>
|
||||||
|
<View style={styles.outer}>
|
||||||
|
{!tags.length && (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="tags"
|
||||||
|
size={14}
|
||||||
|
style={pal.textLight as FontAwesomeIconStyle}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tags.map(tag => (
|
||||||
|
<TagButton key={tag} value={tag} onClick={removeTag} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
{tags.length >= max ? null : (
|
||||||
|
<TextInput
|
||||||
|
ref={input as any}
|
||||||
|
autoFocus={true}
|
||||||
|
id="tags-autocomplete-input"
|
||||||
|
role={'listbox' as any}
|
||||||
|
aria-controls="tags-autocomplete-dropdown"
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-expanded={dropdownIsOpen}
|
||||||
|
value={value}
|
||||||
|
onFocus={onFocus}
|
||||||
|
onBlur={onBlur}
|
||||||
|
onKeyPress={onKeyPress}
|
||||||
|
onSubmitEditing={onSubmitEditing}
|
||||||
|
onChangeText={onChangeText}
|
||||||
|
blurOnSubmit={false}
|
||||||
|
style={[styles.input, pal.textLight]}
|
||||||
|
placeholder="Enter a tag and press enter"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
autoComplete="off"
|
||||||
|
accessible={true}
|
||||||
|
accessibilityLabel="Add tags to your post"
|
||||||
|
accessibilityHint={`Type a tag and press enter to add it. You can add up to ${max} tag.`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Pin
|
||||||
|
pinned={Boolean(dropdownIsOpen && dropdownItems.length)}
|
||||||
|
to={input}
|
||||||
|
at="bottomLeft"
|
||||||
|
from="topLeft"
|
||||||
|
style={{width: inputWidth}}>
|
||||||
|
<View
|
||||||
|
ref={dropdown as any}
|
||||||
|
style={[pal.view, pal.borderDark, styles.dropdown]}
|
||||||
|
role={'listbox' as any}
|
||||||
|
id="tags-autocomplete-dropdown">
|
||||||
|
{dropdownItems.map((item, index) => {
|
||||||
|
const isFirst = index === 0
|
||||||
|
const isLast = index === dropdownItems.length - 1
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
id={`tag_autocomplete_option_${item.value}`}
|
||||||
|
accessibilityRole="button"
|
||||||
|
key={item.value}
|
||||||
|
onPress={() => addTagAndReset(item.value)}
|
||||||
|
style={state => [
|
||||||
|
pal.border,
|
||||||
|
styles.dropdownItem,
|
||||||
|
{
|
||||||
|
backgroundColor: state.hovered
|
||||||
|
? pal.viewLight.backgroundColor
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
selectedItemIndex === index ? pal.viewLight : undefined,
|
||||||
|
isFirst
|
||||||
|
? styles.firstResult
|
||||||
|
: isLast
|
||||||
|
? styles.lastResult
|
||||||
|
: undefined,
|
||||||
|
]}>
|
||||||
|
<Text type="md" style={pal.textLight} numberOfLines={1}>
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
</Pin>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
outer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
flexGrow: 1,
|
||||||
|
minWidth: 100,
|
||||||
|
fontSize: 15,
|
||||||
|
lineHeight: Platform.select({
|
||||||
|
web: 20,
|
||||||
|
native: 18,
|
||||||
|
}),
|
||||||
|
paddingTop: 5,
|
||||||
|
paddingBottom: 5,
|
||||||
|
},
|
||||||
|
dropdown: {
|
||||||
|
width: '100%',
|
||||||
|
borderRadius: 6,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderStyle: 'solid',
|
||||||
|
padding: 4,
|
||||||
|
},
|
||||||
|
dropdownItem: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
flexDirection: 'row',
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 8,
|
||||||
|
gap: 4,
|
||||||
|
},
|
||||||
|
firstResult: {
|
||||||
|
borderTopLeftRadius: 2,
|
||||||
|
borderTopRightRadius: 2,
|
||||||
|
},
|
||||||
|
lastResult: {
|
||||||
|
borderBottomLeftRadius: 2,
|
||||||
|
borderBottomRightRadius: 2,
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user