Handle toggling label state

This commit is contained in:
Jackson Harper 2022-04-10 14:52:30 -07:00
parent 6b84192543
commit 0a4215c57b
2 changed files with 194 additions and 165 deletions

View file

@ -1,5 +1,6 @@
import { Separator } from "@radix-ui/react-separator"
import { ArchiveBox, DotsThree, HighlighterCircle, TagSimple, TextAa } from "phosphor-react"
import { useRef } from "react"
import { useGetUserPreferences, UserPreferences } from "../../../lib/networking/queries/useGetUserPreferences"
import { Button } from "../../elements/Button"
import { Dropdown } from "../../elements/DropdownElements"

View file

@ -9,7 +9,7 @@ import { StyledText } from '../../elements/StyledText'
import { CrossIcon } from '../../elements/images/CrossIcon'
import { styled, theme } from '../../tokens/stitches.config'
import { Label, useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery'
import { ChangeEvent, useCallback, useRef, useState, useMemo } from 'react'
import { ChangeEvent, useCallback, useRef, useState, useMemo, useEffect } from 'react'
import { setLabelsMutation } from '../../../lib/networking/mutations/setLabelsMutation'
import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
import { LabelChip } from '../../elements/LabelChip'
@ -26,6 +26,8 @@ type EditLabelsModalProps = {
type HeaderProps = {
filterText: string
focused: boolean
parentRef: React.RefObject<HTMLDivElement>
setFilterText: (text: string) => void
}
@ -44,70 +46,68 @@ const FormInput = styled('input', {
const StyledLabel = styled('label', {
display: 'flex',
justifyContent: 'flex-start',
'&:focus-visible': {
backgroundColor: 'red',
'&:focus': {
bg: '$grayBgActive',
},
'input:&:focus-within': {
bg: '$grayBgActive',
},
})
const useToggleLabels = () => {
const [selectedLabels, setSelectedLabels] = useState<Label[]>([])
const isSelected = useCallback((label: Label): boolean => {
return selectedLabels.some((other) => {
return other.id === label.id
})
}, [selectedLabels])
const toggleLabel = useCallback((label: Label) => {
if (isSelected(label)) {
setSelectedLabels(selectedLabels.filter((other) => {
return other.id !== label.id
}))
} else {
setSelectedLabels([...selectedLabels, label])
}
}, [isSelected, selectedLabels])
return [isSelected, toggleLabel]
}
function Header(props: HeaderProps): JSX.Element {
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
if (props.focused && inputRef.current) {
inputRef.current.focus()
}
}, [props.focused])
return (
<VStack css={{ width: '100%', my: '0px', borderBottom: '1px solid $grayBorder',
}}>
{/* <HStack
distribution="between"
alignment="center"
css={{
width: '100%',
flex: '0 0 20px',
mx: '0px',
p: '0px',
borderBottom: `1px solid ${theme.colors.grayLine.toString()}`,
}}
> */}
{/* <StyledText style="modalHeadline" css={{ p: '16px', fontSize: '12px', }}>
Apply labels to this page
</StyledText> */}
{/* <Button
css={{ pt: '16px', pr: '16px' }}
style="ghost"
onClick={() => {
}}
>
<CrossIcon
size={20}
strokeColor={theme.colors.grayText.toString()}
/>
</Button> */}
{/* </HStack> */}
<Box css={{
width: '100%',
my: '14px',
px: '14px',
}}>
<form
onSubmit={(event) => {
// event.preventDefault()
// props.applySearchQuery(searchTerm || '')
inputRef.current?.blur()
}}
>
<FormInput
ref={inputRef}
type="text"
tabIndex={0}
tabIndex={props.focused ? 0 : -1}
autoFocus={!isTouchScreenDevice()}
value={props.filterText}
placeholder="Filter for label"
onFocus={(event) => {
event.target.select()
//setFocused(true)
console.log('input::focused()')
}}
onBlur={() => {
//setFocused(false)
console.log('blurred')
// setFocused(false)
console.log('input::blurred')
}}
onKeyDown={(event) => {
console.log('keydown', event.key)
@ -134,94 +134,82 @@ function Header(props: HeaderProps): JSX.Element {
},
}}
/>
</form>
</Box>
</VStack>)
}
type LabelsListProps = {
// pageId: string
// selectedLabels: Label[]
availableLabels: Label[]
// setSelectedLabels: (labels: Label[]) => void
type LabelListItemProps = {
label: Label
focused: boolean
selected: boolean
}
function LabelsList(props: LabelsListProps): JSX.Element {
const isSelected = useCallback((label: Label) => {
// return props.selectedLabels.some((other) => {
// return other.id === label.id
// })
return false
}, [])
function LabelListItem(props: LabelListItemProps): JSX.Element {
const ref = useRef<HTMLLabelElement>(null)
const { label, focused, selected } = props
useEffect(() => {
if (props.focused && ref.current) {
ref.current.focus()
}
}, [props.focused])
return (
<VStack css={{ flexGrow: '1', overflow: 'scroll', width: '100%', height: '100%', maxHeight: '400px', }}>
{props.availableLabels &&
props.availableLabels.map((label, idx) => (
<StyledLabel
key={label.id}
tabIndex={idx + 1}
css={{
width: '100%',
height: '42px',
borderBottom: '1px solid $grayBorder',
}}
onClick={async () => {
// console.log('selected label', label)
// if (props.selectedLabels.includes(label)) {
// props.setSelectedLabels(
// props.selectedLabels.filter((id) => id !== label)
// )
// } else {
// props.setSelectedLabels([...props.selectedLabels, label])
// }
// const result = await setLabelsMutation(props.pageId, props.selectedLabels.map((l) => l.id))
// console.log('result', result)
}}
>
<Box css={{ pl: '5px', width: '25px', height: '45px', display: 'flex', alignItems: 'center' }}>
{isSelected(label) && <Check color={theme.colors.grayText.toString()} weight='bold' />}
</Box>
<Box css={{ width: '30px', height: '100%', display: 'flex', alignItems: 'center' }}>
<Circle width={22} height={22} color={label.color} weight='fill' />
</Box>
<Box css={{ overflow: 'clip', height: '100%', display: 'flex', alignItems: 'center' }}>
<StyledText style="caption">{label.name}</StyledText>
</Box>
<Box css={{ pl: '10px', width: '40px', marginLeft: 'auto', height: '100%', display: 'flex', alignItems: 'center' }}>
{isSelected(label) && <CrossIcon
size={14}
strokeColor={theme.colors.grayText.toString()}
/>}
</Box>
{/*
<CrossIcon
size={14}
strokeColor={theme.colors.grayText.toString()}
/> */}
{/* <input
type="checkbox"
value={label.id}
onChange={handleChange}
checked={selectedLabels.includes(label)}
/> */}
</StyledLabel>
))}
</VStack>)
<StyledLabel
ref={ref}
css={{
width: '100%',
height: '42px',
borderBottom: '1px solid $grayBorder',
bg: props.focused ? '$grayBgActive' : 'unset',
}}
tabIndex={props.focused ? 0 : -1}
onClick={async () => {
console.log('toggling label')
}}
>
<input autoFocus={focused} hidden={true} type="checkbox" checked={selected} readOnly />
<Box css={{ pl: '10px', width: '32px', display: 'flex', alignItems: 'center' }}>
{selected && <Check size={15} color={theme.colors.grayText.toString()} weight='bold' />}
</Box>
<Box css={{ width: '30px', height: '100%', display: 'flex', alignItems: 'center' }}>
<Circle width={22} height={22} color={label.color} weight='fill' />
</Box>
<Box css={{ overflow: 'clip', height: '100%', display: 'flex', alignItems: 'center' }}>
<StyledText style="caption">{label.name}</StyledText>
</Box>
<Box css={{ pl: '10px', width: '40px', marginLeft: 'auto', display: 'flex', alignItems: 'center' }}>
{selected && <CrossIcon
size={14}
strokeColor={theme.colors.grayText.toString()}
/>}
</Box>
</StyledLabel>
)
}
export function EditLabelsModal(props: EditLabelsModalProps): JSX.Element {
const parentRef = useRef<HTMLDivElement>(null)
const [filterText, setFilterText] = useState('')
const [selectedLabels, setSelectedLabels] = useState([])
const { labels } = useGetLabelsQuery()
const saveAndExit = useCallback(async () => {
// const result = await setLabelsMutation(props.article.id, selectedLabels.map((l) => l.id))
// console.log('result of setting labels', result)
// // props.onOpenChange(false)
// // props.setLabels(selectedLabels)
}, [props, selectedLabels])
const [selectedLabels, setSelectedLabels] = useState<Label[]>([])
const isSelected = useCallback((label: Label): boolean => {
return selectedLabels.some((other) => {
return other.id === label.id
})
}, [selectedLabels])
const toggleLabel = useCallback((label: Label) => {
if (isSelected(label)) {
setSelectedLabels(selectedLabels.filter((other) => {
return other.id !== label.id
}))
} else {
setSelectedLabels([...selectedLabels, label])
}
}, [isSelected, selectedLabels])
const filteredLabels = useMemo(() => {
if (!labels) {
@ -232,58 +220,98 @@ export function EditLabelsModal(props: EditLabelsModalProps): JSX.Element {
})
}, [labels, filterText])
useEffect(() => {
setFocusedIndex(undefined)
}, [filterText])
// Move focus through the labels list on tab or arrow up/down keys
const [focusedIndex, setFocusedIndex] = useState<number | undefined>(undefined)
const handleKeyDown = useCallback((event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'ArrowUp') {
event.preventDefault()
if (focusedIndex) {
setFocusedIndex(Math.max(0, focusedIndex - 1))
} else {
setFocusedIndex(undefined)
}
}
if (event.key === 'ArrowDown' || event.key === 'Tab') {
event.preventDefault()
if (focusedIndex === undefined) {
setFocusedIndex(0)
} else {
setFocusedIndex(Math.min(filteredLabels.length - 1, focusedIndex + 1))
}
}
if (event.key === 'Enter') {
event.preventDefault()
if (focusedIndex !== undefined) {
const label = filteredLabels[focusedIndex]
if (label) {
toggleLabel(label)
}
}
}
}, [filteredLabels, focusedIndex])
return (
// <ModalRoot defaultOpen onOpenChange={saveAndExit}>
// <ModalOverlay />
// <ModalContent
// onPointerDownOutside={(event) => {
// event.preventDefault()
// }}
// css={{ p: '0', width: '100%', maxWidth: '400px' }}
// >
<VStack distribution="start" css={{
p: '0', maxHeight: '400px',
maxWidth: '265px',
'@mdDown': {
maxWidth: '100%',
maxHeight: '320px',
<VStack
ref={parentRef}
distribution="start"
onKeyDown={handleKeyDown}
css={{
p: '0',
maxHeight: '92%',
maxWidth: '265px',
'@mdDown': {
maxWidth: '100%',
maxHeight: '92%',
},
}}>
<Header
parentRef={parentRef}
focused={focusedIndex === undefined}
setFilterText={setFilterText} filterText={filterText}
/>
<VStack css={{ flexGrow: '1', overflow: 'scroll', width: '100%', height: '100%', maxHeight: '400px', }}>
{labels &&
labels.map((label, idx) => (
<LabelListItem
key={label.id}
label={label}
focused={idx === focusedIndex}
selected={isSelected(label)}
/>
))}
</VStack>
{filterText && (
<Button style='modalOption' css={{
pl: '26px', color: theme.colors.grayText.toString(), height: '42px', borderBottom: '1px solid $grayBorder'
}}
onClick={() => {
// props.createLabel(filterText)
// setFilterText('')
}}
>
<HStack alignment='center' distribution='start' css={{ gap: '8px' }}>
<Plus size={18} color={theme.colors.grayText.toString()} />
<SpanBox css={{ fontSize: '12px' }}>{`Create new label "${filterText}"`}</SpanBox>
</HStack>
</Button>)}
{/* Footer */}
<HStack distribution="start" alignment="center" css={{
ml: '20px', gap: '8px', width: '100%', fontSize: '12px', p: '8px', height: '42px',
'a:link': {
textDecoration: 'none',
},
}}>
<Header setFilterText={setFilterText} filterText={filterText} />
<LabelsList
availableLabels={filteredLabels}
// selectedLabels={selectedLabels || []}
/>
{filterText && (
<Button style='modalOption' css={{
pl: '26px', color: theme.colors.grayText.toString(), height: '42px', borderBottom: '1px solid $grayBorder'
}}
onClick={() => {
// props.createLabel(filterText)
// setFilterText('')
}}
>
<HStack alignment='center' distribution='start' css={{ gap: '8px' }}>
<Plus size={18} color={theme.colors.grayText.toString()} />
<SpanBox css={{ fontSize: '12px' }}>{`Create new label "${filterText}"`}</SpanBox>
</HStack>
</Button>)}
{/* Footer */}
<HStack distribution="start" alignment="center" css={{
ml: '20px', gap: '8px', width: '100%', fontSize: '12px', p: '8px', height: '42px',
'a:link': {
textDecoration: 'none',
},
'a:visited': {
color: theme.colors.grayText.toString(),
},
}}
>
<PencilSimple size={18} color={theme.colors.grayText.toString()} />
<Link href="/settings/labels">Edit labels</Link>
</HStack>
</VStack>
// </ModalContent>
// </ModalRoot>
'a:visited': {
color: theme.colors.grayText.toString(),
},
}}
>
<PencilSimple size={18} color={theme.colors.grayText.toString()} />
<Link href="/settings/labels">Edit labels</Link>
</HStack>
</VStack>
)
}