diff --git a/.changeset/rotten-hairs-impress.md b/.changeset/rotten-hairs-impress.md new file mode 100644 index 00000000000..190b02f671b --- /dev/null +++ b/.changeset/rotten-hairs-impress.md @@ -0,0 +1,5 @@ +--- +'@primer/react': patch +--- + +TreeView: aria status description is now accurate diff --git a/src/TreeView/TreeView.features.stories.tsx b/src/TreeView/TreeView.features.stories.tsx index a2ce51614ae..bf19cdd7b0f 100644 --- a/src/TreeView/TreeView.features.stories.tsx +++ b/src/TreeView/TreeView.features.stories.tsx @@ -609,6 +609,84 @@ export const EmptyDirectories: Story = () => { ) } +export const NestedTrees: Story = () => { + const [isLoading, setIsLoading] = React.useState(false) + const [asyncItems, setAsyncItems] = React.useState([]) + + let state: SubTreeState = 'initial' + + if (isLoading) { + state = 'loading' + } else if (asyncItems.length > 0) { + state = 'done' + } + + return ( + + ) +} + export const NestedScrollContainer: Story = () => { return ( diff --git a/src/TreeView/TreeView.test.tsx b/src/TreeView/TreeView.test.tsx index 5e443db9edc..8ad4f28df92 100644 --- a/src/TreeView/TreeView.test.tsx +++ b/src/TreeView/TreeView.test.tsx @@ -1077,7 +1077,14 @@ describe('Asyncronous loading', () => { Parent - Child + Child Item + + Child Subtree + + Child 1 + Child 2 + + @@ -1100,6 +1107,7 @@ describe('Asyncronous loading', () => { }) // Live region should be updated + expect(liveRegion).not.toHaveTextContent('Child 2 is empty') expect(liveRegion).toHaveTextContent('Parent content loaded') }) diff --git a/src/TreeView/TreeView.tsx b/src/TreeView/TreeView.tsx index 580d045a9a0..0e44e0ff776 100644 --- a/src/TreeView/TreeView.tsx +++ b/src/TreeView/TreeView.tsx @@ -75,7 +75,7 @@ const UlBox = styled.ul` * We define styles for the tree items at the root level of the tree * to avoid recomputing the styles for each item when the tree updates. * We're sacraficing maintainability for performance because TreeView - * needs to be performant enough to handle large trees (thousands of items). + * needs to be performant enough to handle large trees (thousands of items). * * This is intended to be a temporary solution until we can improve the * performance of our styling patterns. @@ -508,6 +508,13 @@ const SubTree: React.FC = ({count, state, children}) => { const {safeSetTimeout} = useSafeTimeout() const loadingItemRef = React.useRef(null) const ref = React.useRef(null) + const [isPending, setPending] = React.useState(state === 'loading') + + React.useEffect(() => { + if (state === 'loading') { + setPending(true) + } + }, [state]) React.useEffect(() => { // If `state` is undefined, we're working in a synchronous context and need @@ -523,9 +530,11 @@ const SubTree: React.FC = ({count, state, children}) => { } }, [state, isSubTreeEmpty, setIsSubTreeEmpty, children]) - // Announce when content has loaded + // If a consumer sets state="done" without having a previous state (like `loading`), + // then it would announce on the first render. Using isPending is to only + // announce being "loaded" when the state has changed from `loading` --> `done`. React.useEffect(() => { - if (state === 'done') { + if (isPending && state === 'done') { const parentItem = document.getElementById(itemId) if (!parentItem) return @@ -540,8 +549,10 @@ const SubTree: React.FC = ({count, state, children}) => { announceUpdate(`${parentName} is empty`) } }) + + setPending(false) } - }, [state, itemId, announceUpdate, safeSetTimeout]) + }, [state, itemId, announceUpdate, safeSetTimeout, isPending]) // Manage loading indicator state React.useEffect(() => {