Skip to content

Commit

Permalink
feat: add collapse expand functionality to tree view
Browse files Browse the repository at this point in the history
  • Loading branch information
andybywire committed Jun 10, 2023
2 parents 23656e3 + 4538587 commit 51d21ef
Show file tree
Hide file tree
Showing 11 changed files with 401 additions and 157 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format of this document is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [unreleased]

### Added
- Collapse / expand functionality to tree view
- Keyboard and screen reader support for collapse/expand, editing actions, and detail links

## [2.2.2] - 2023-06-06

### Added
Expand Down
3 changes: 2 additions & 1 deletion src/components/ChildConcepts.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
/**
* Child Concepts
* This component renders a list of child concepts for a given concept.
* - provides the <ul> wrapper for each level of nesting
*/

import {StyledChildConcepts} from '../styles'
import {ChildConceptTerm} from '../types'
import {StyledChildConcepts} from '../styles'
import {Children} from './Children'

export const ChildConcepts = ({concepts}: {concepts: ChildConceptTerm[]}) => {
Expand Down
97 changes: 77 additions & 20 deletions src/components/Children.tsx
Original file line number Diff line number Diff line change
@@ -1,60 +1,103 @@
/**
* Child Concept Component
* Renders a list of child concepts for a given concept.
* @todo consider modularizing add and remove buttons
* @todo Add dialogue explaining max depth
* @todo Improve accessibility of hidden children and max depth disclosures
* @todo Handle childConcept and level definition checks more elegantly
*/

import {useCallback, useContext} from 'react'
import {useCallback, useContext, useState} from 'react'
import {Inline, Tooltip, Box, Stack, Text} from '@sanity/ui'
import {ErrorOutlineIcon, InfoOutlineIcon, AddCircleIcon, TrashIcon} from '@sanity/icons'
import {
ErrorOutlineIcon,
InfoOutlineIcon,
AddCircleIcon,
TrashIcon,
ToggleArrowRightIcon,
SquareIcon,
} from '@sanity/icons'
import {useCreateConcept, useRemoveConcept} from '../hooks'
import {StyledChildConcept} from '../styles'
import {ChildConceptTerm} from '../types'
import {StyledChildConcept, StyledTreeButton, StyledTreeToggle} from '../styles'
import {SchemeContext} from './TreeView'
import {TreeContext} from './Hierarchy'
import {ChildConcepts} from './ChildConcepts'
import {ConceptDetailLink} from './ConceptDetailLink'
import {ConceptDetailDialogue} from './ConceptDetailDialogue'

export const Children = ({concept}: {concept: ChildConceptTerm}) => {
const document: any = useContext(SchemeContext) || {}
//@ts-expect-error — This is part of the same complaint as in Hierarchy.tsx
const {treeVisibility} = useContext(TreeContext) || {}
const createConcept = useCreateConcept(document)
const removeConcept = useRemoveConcept(document)

const handleAddChild = useCallback(() => {
if (document.displayed?._id === concept?.id) {
// eslint-disable-next-line no-console
console.log('Concept and document ids are the same.')
return
}
createConcept('concept', concept?.id, concept?.prefLabel)
}, [concept?.id, concept?.prefLabel, createConcept, document.displayed?._id])
}, [concept?.id, concept?.prefLabel, createConcept])

const handleRemoveConcept = useCallback(() => {
removeConcept(concept.id, 'concept', concept?.prefLabel)
}, [concept.id, concept?.prefLabel, removeConcept])

const [levelVisibility, setLevelVisibility] = useState(treeVisibility)

const handleToggle = useCallback(() => {
if (levelVisibility == 'open') {
setLevelVisibility('closed')
} else if (levelVisibility == 'closed') {
setLevelVisibility('open')
}
}, [levelVisibility])

return (
<StyledChildConcept>
<StyledChildConcept className={levelVisibility}>
<Inline space={2}>
<Inline space={1}>
<Text size={2}>
<Inline space={1}>
{concept?.childConcepts && concept.childConcepts.length > 0 && (
<StyledTreeToggle
onClick={handleToggle}
type="button"
aria-expanded={levelVisibility == 'open'}
>
<ToggleArrowRightIcon />
</StyledTreeToggle>
)}
{concept?.childConcepts && concept.childConcepts.length == 0 && (
<SquareIcon className="spacer" />
)}
{!concept?.prefLabel && <span className="untitled">[new concept]</span>}
<ConceptDetailLink concept={concept} />
</Text>
</Inline>
{!document.displayed?.controls && <ConceptDetailDialogue concept={concept} />}
</Inline>
{document.displayed?.controls && concept?.level && concept.level < 5 && (
<Inline space={1}>
<AddCircleIcon className="normal" onClick={handleAddChild} />
<TrashIcon className="critical" onClick={handleRemoveConcept} />
<Inline space={2}>
<StyledTreeButton
onClick={handleAddChild}
type="button"
className="action"
aria-label="Add child a child concept"
>
<AddCircleIcon className="add" />
</StyledTreeButton>
<StyledTreeButton
onClick={handleRemoveConcept}
type="button"
className="action"
aria-label="Remove concept from scheme"
>
<TrashIcon className="remove" />
</StyledTreeButton>
</Inline>
)}

{document.displayed?.controls &&
concept.childConcepts?.length == 0 &&
concept.level == 5 && (
<Inline space={1}>
<Inline space={2}>
<Tooltip
content={
<Box padding={2} sizing="border">
Expand All @@ -68,9 +111,16 @@ export const Children = ({concept}: {concept: ChildConceptTerm}) => {
fallbackPlacements={['right', 'left']}
placement="top"
>
<InfoOutlineIcon className="warning" />
<InfoOutlineIcon className="info warning" />
</Tooltip>
<TrashIcon className="critical" onClick={handleRemoveConcept} />
<StyledTreeButton
onClick={handleRemoveConcept}
type="button"
className="action"
aria-label="Remove concept from scheme"
>
<TrashIcon className="remove" />
</StyledTreeButton>
</Inline>
)}

Expand All @@ -92,10 +142,17 @@ export const Children = ({concept}: {concept: ChildConceptTerm}) => {
fallbackPlacements={['right', 'left']}
placement="top"
>
<ErrorOutlineIcon className="error" />
<ErrorOutlineIcon className="info error" />
</Tooltip>
{document.displayed?.controls && (
<TrashIcon className="critical" onClick={handleRemoveConcept} />
<StyledTreeButton
onClick={handleRemoveConcept}
type="button"
className="action"
aria-label="Remove concept from scheme"
>
<TrashIcon className="remove" />
</StyledTreeButton>
)}
</Inline>
)}
Expand Down
44 changes: 9 additions & 35 deletions src/components/ConceptDetailDialogue.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,26 @@
/**
* Information Icon and Dialogue with Concept Details
* Affords Tree View access to Definition, Examples, and Scope Notes
* - affords Tree View access to Definition, Examples, and Scope Notes
* - is rendered only when concept details are present
*/

import {useCallback, useState} from 'react'
import {Dialog, Box, Text, Stack, Label} from '@sanity/ui'
import {InfoOutlineIcon} from '@sanity/icons'
import {InfoDialog} from '../styles'
import {StyledTreeButton} from '../styles'

export const ConceptDetailDialogue = ({concept}: {concept: any}) => {
const [open, setOpen] = useState(false)
const onClose = useCallback(() => setOpen(false), [])
const onOpen = useCallback(() => setOpen(true), [])

if (!concept) return null
else if (!concept.definition && !concept.example && !concept.scopeNote) {
return null

// To Investigate: Showing disabled icons in the absence of explanatory notes.
// The goal is to encourage editors to provide descriptions, but the better
// practice is likely simply not to show the icon if there is no content there.
// For the moment, defaulting to showing no icon.

// return (
// <InfoDialog>
// <Tooltip
// content={
// <Box padding={2} sizing="border">
// <Stack padding={1} space={2}>
// <Text muted size={1}>
// This concept has no explanatory notes.
// </Text>
// </Stack>
// </Box>
// }
// fallbackPlacements={['right', 'left']}
// placement="top"
// >
// <InfoOutlineIcon className="default" />
// </Tooltip>
// </InfoDialog>
// )
}
if (!concept || (!concept.definition && !concept.example && !concept.scopeNote)) return null

return (
<InfoDialog>
<InfoOutlineIcon className="brand" onClick={onOpen} />
<>
<StyledTreeButton className="action" aria-label="info" onClick={onOpen} type="button">
<InfoOutlineIcon className="info" />
</StyledTreeButton>

{open && (
<Dialog
Expand Down Expand Up @@ -81,11 +56,10 @@ export const ConceptDetailDialogue = ({concept}: {concept: any}) => {
</Text>
</Stack>
)}
{/* <pre>{JSON.stringify(concept, null, 2)}</pre> */}
</Stack>
</Box>
</Dialog>
)}
</InfoDialog>
</>
)
}
9 changes: 6 additions & 3 deletions src/components/ConceptDetailLink.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
/**
* Concept Detail Link
* Renders a link to a concept in the hierarchy tree that opens in a new pane.
* @todo Adapt to use the New Concept Pane hook (it's death spiraling at the moment)
*/

import {useCallback, useContext} from 'react'
import {usePaneRouter} from 'sanity/desk'
import {RouterContext} from 'sanity/router'
import {StyledConceptLink} from '../styles'
import {ChildConceptTerm} from '../types'
import {StyledConceptLink} from '../styles'

export function ConceptDetailLink({concept}: {concept: ChildConceptTerm}) {
const routerContext = useContext(RouterContext)
Expand All @@ -33,5 +32,9 @@ export function ConceptDetailLink({concept}: {concept: ChildConceptTerm}) {
routerContext.navigateUrl({path: href})
}, [id, routerContext, routerPanesState, groupIndex])

return <StyledConceptLink onClick={openInNewPane}>{prefLabel}</StyledConceptLink>
return (
<StyledConceptLink href="#" onClick={openInNewPane}>
{prefLabel}
</StyledConceptLink>
)
}
Loading

0 comments on commit 51d21ef

Please sign in to comment.