import { useCallback, useRef, useState, useMemo, useEffect } from 'react' import Link from 'next/link' import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives' import { Button } from '../../elements/Button' import { StyledText } from '../../elements/StyledText' import { CrossIcon } from '../../elements/images/CrossIcon' import { styled, theme } from '../../tokens/stitches.config' import { Label } from '../../../lib/networking/fragments/labelFragment' import { useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery' import { Check, Circle, PencilSimple, Plus } from 'phosphor-react' import { isTouchScreenDevice } from '../../../lib/deviceType' import { setLabelsMutation } from '../../../lib/networking/mutations/setLabelsMutation' import { createLabelMutation } from '../../../lib/networking/mutations/createLabelMutation' import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers' import { randomLabelColorHex } from '../../../utils/settings-page/labels/labelColorObjects' import { useRouter } from 'next/router' import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery' type SetLabelsControlProps = { linkId: string labels: Label[] | undefined article?: ArticleAttributes articleActionHandler: (action: string, arg?: unknown) => void } type HeaderProps = { filterText: string focused: boolean resetFocusedIndex: () => void setFilterText: (text: string) => void } const FormInput = styled('input', { width: '100%', fontSize: '16px', fontFamily: 'inter', fontWeight: 'normal', lineHeight: '1.8', color: '$grayTextContrast', '&:focus': { outline: 'none', }, }) const StyledLabel = styled('label', { display: 'flex', justifyContent: 'flex-start', }) function Header(props: HeaderProps): JSX.Element { const inputRef = useRef(null) useEffect(() => { if (!isTouchScreenDevice() && props.focused && inputRef.current) { inputRef.current.focus() } }, [props.focused]) return ( { props.setFilterText(event.target.value) }} onFocus={() => { props.resetFocusedIndex() }} css={{ border: '1px solid $grayBorder', borderRadius: '8px', width: '100%', bg: 'transparent', fontSize: '16px', textIndent: '8px', marginBottom: '2px', color: '$grayTextContrast', '&:focus': { outline: 'none', boxShadow: '0px 0px 2px 2px rgba(255, 234, 159, 0.56)', }, }} /> ) } type LabelListItemProps = { label: Label focused: boolean selected: boolean toggleLabel: (label: Label) => void } function LabelListItem(props: LabelListItemProps): JSX.Element { const ref = useRef(null) const { label, focused, selected } = props useEffect(() => { if (props.focused && ref.current) { ref.current.focus() } }, [props.focused]) return ( { event.preventDefault() props.toggleLabel(label) ref.current?.blur() }} > {selected && } {label.name} {selected && } ) } type FooterProps = { focused: boolean } function Footer(props: FooterProps): JSX.Element { const ref = useRef(null) useEffect(() => { if (props.focused && ref.current) { ref.current.focus() } }, [props.focused]) return ( Edit labels ) } export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element { const router = useRouter() const [filterText, setFilterText] = useState('') const { labels, revalidate } = useGetLabelsQuery() const [selectedLabels, setSelectedLabels] = useState(props.labels || []) useEffect(() => { setFocusedIndex(undefined) }, [filterText]) const isSelected = useCallback((label: Label): boolean => { return selectedLabels.some((other) => { return other.id === label.id }) }, [selectedLabels]) const toggleLabel = useCallback(async (label: Label) => { let newSelectedLabels = [...selectedLabels] if (isSelected(label)) { newSelectedLabels = selectedLabels.filter((other) => { return other.id !== label.id }) } else { newSelectedLabels = [...selectedLabels, label] } setSelectedLabels(newSelectedLabels) const result = await setLabelsMutation( props.linkId, newSelectedLabels.map((label) => label.id) ) if (props.article) { props.article.labels = result } props.articleActionHandler('refreshLabels', result) revalidate() }, [isSelected, selectedLabels, setSelectedLabels]) const filteredLabels = useMemo(() => { if (!labels) { return [] } return labels.filter((label) => { return label.name.toLowerCase().includes(filterText.toLowerCase()) }) }, [labels, filterText]) // Move focus through the labels list on tab or arrow up/down keys const [focusedIndex, setFocusedIndex] = useState(undefined) const handleKeyDown = useCallback(async (event: React.KeyboardEvent) => { const maxIndex = filteredLabels.length + 1 if (event.key === 'ArrowUp') { event.preventDefault() let newIndex = focusedIndex if (focusedIndex) { newIndex = Math.max(0, focusedIndex - 1) } else { newIndex = undefined } // If the `Create New label` button isn't visible we skip it // when navigating with the arrow keys if (focusedIndex === maxIndex && !filterText) { newIndex = maxIndex - 2 } setFocusedIndex(newIndex) } if (event.key === 'ArrowDown' || event.key === 'Tab') { event.preventDefault() let newIndex = focusedIndex if (focusedIndex === undefined) { newIndex = 0 } else { newIndex = Math.min(maxIndex, focusedIndex + 1) } // If the `Create New label` button isn't visible we skip it // when navigating with the arrow keys if (focusedIndex === maxIndex - 2 && !filterText) { newIndex = maxIndex } setFocusedIndex(newIndex) } if (event.key === 'Enter') { event.preventDefault() if (focusedIndex === maxIndex) { router.push('/settings/labels') return } if (focusedIndex === maxIndex - 1) { await createLabelFromFilterText() return } if (focusedIndex !== undefined) { const label = filteredLabels[focusedIndex] if (label) { toggleLabel(label) } } } }, [filterText, filteredLabels, focusedIndex, isSelected, selectedLabels, setSelectedLabels]) const createLabelFromFilterText = useCallback(async () => { const label = await createLabelMutation(filterText, randomLabelColorHex(), '') if (label) { showSuccessToast(`Created label ${label.name}`, { position: 'bottom-right' }) toggleLabel(label) } else { showErrorToast('Failed to create label', { position: 'bottom-right' }) } }, [filterText, selectedLabels, setSelectedLabels, toggleLabel]) return (
setFocusedIndex(undefined)} setFilterText={setFilterText} filterText={filterText} /> {filteredLabels && filteredLabels.map((label, idx) => ( ))} {filterText && ( )}