Skip to content

Commit 9a8e3a1

Browse files
authored
Merge branch 'master' into dependabot/npm_and_yarn/store2-2.14.4
2 parents 0dd7fdc + a8bb387 commit 9a8e3a1

File tree

6 files changed

+184
-136
lines changed

6 files changed

+184
-136
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "react-accessible-treeview",
33
"description": "A react component that implements the treeview pattern as described by the WAI-ARIA Authoring Practices.",
4-
"version": "2.10.0",
4+
"version": "2.11.0",
55
"author": "lissitz (https://github.com/lissitz)",
66
"main": "dist/react-accessible-treeview.cjs.js",
77
"module": "dist/react-accessible-treeview.esm.js",

src/TreeView/index.tsx

Lines changed: 134 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,7 @@ const useTree = ({
341341
) {
342342
idsToUpdate.add(lastInteractedWith);
343343
}
344+
344345
//========START FILTER OUT NOT EXISTING IDS=========
345346
// This block of code filters out from propagation check ids that aren't in data anymore
346347
const idsNotInData: NodeId[] = [];
@@ -351,6 +352,7 @@ const useTree = ({
351352
});
352353
idsNotInData.forEach((id) => idsToUpdate.delete(id));
353354
//========END FILTER OUT NOT EXISTING IDS===========
355+
354356
const { every, some, none } = propagateSelectChange(
355357
data,
356358
idsToUpdate,
@@ -359,6 +361,20 @@ const useTree = ({
359361
halfSelectedIds,
360362
multiSelect
361363
);
364+
365+
// when controlling select ids, ensure that propagatedIds are selected when controlledSelectedIds are changed.
366+
if (controlledSelectedIds) {
367+
idsToUpdate.forEach((id) => {
368+
if (isBranchNode(data, id)) {
369+
const descendantIds = getDescendants(data, id, new Set<number>());
370+
const isPropagatedId = descendantIds.every((childId) =>
371+
selectedIds.has(childId)
372+
);
373+
isPropagatedId && every.add(id);
374+
}
375+
});
376+
}
377+
362378
for (const id of every) {
363379
if (!selectedIds.has(id)) {
364380
dispatch({
@@ -554,128 +570,125 @@ export interface ITreeViewProps<M extends IFlatMetadata = IFlatMetadata> {
554570
focusedId?: NodeId;
555571
}
556572

557-
const TreeView = React.forwardRef(
558-
function TreeView<M extends IFlatMetadata = IFlatMetadata>(
559-
{
560-
data,
561-
selectedIds,
562-
nodeRenderer,
563-
onSelect = noop,
564-
onNodeSelect = noop,
565-
onExpand = noop,
566-
onLoadData,
567-
className = "",
568-
multiSelect = false,
569-
propagateSelect = false,
570-
propagateSelectUpwards = false,
571-
propagateCollapse = false,
572-
expandOnKeyboardSelect = false,
573-
togglableSelect = false,
574-
defaultExpandedIds = [],
575-
defaultSelectedIds = [],
576-
defaultDisabledIds = [],
577-
clickAction = clickActions.select,
578-
nodeAction = "select",
579-
expandedIds,
580-
focusedId,
581-
onBlur,
582-
...other
583-
}: ITreeViewProps<M>,
584-
ref: React.ForwardedRef<HTMLUListElement>
585-
) {
586-
validateTreeViewData(data);
587-
const nodeRefs = useRef({});
588-
const leafRefs = useRef({});
589-
let innerRef = useRef<HTMLUListElement | null>(null);
590-
if (ref != null) {
591-
innerRef = ref as React.MutableRefObject<HTMLUListElement>;
592-
}
593-
const [state, dispatch] = useTree({
594-
data,
595-
controlledSelectedIds: selectedIds,
596-
controlledExpandedIds: expandedIds,
597-
defaultExpandedIds,
598-
defaultSelectedIds,
599-
defaultDisabledIds,
600-
nodeRefs,
601-
leafRefs,
602-
onSelect,
603-
onNodeSelect,
604-
onExpand,
605-
onLoadData,
606-
togglableSelect,
607-
multiSelect,
608-
propagateSelect,
609-
propagateSelectUpwards,
610-
treeRef: innerRef,
611-
focusedId,
612-
});
613-
propagateSelect = propagateSelect && multiSelect;
614-
615-
return (
616-
<ul
617-
className={cx(baseClassNames.root, className)}
618-
role="tree"
619-
aria-multiselectable={nodeAction === "select" ? multiSelect : undefined}
620-
ref={innerRef}
621-
onBlur={(event) => {
622-
onComponentBlur(event, innerRef.current, () => {
623-
onBlur &&
624-
onBlur({
625-
treeState: state,
626-
dispatch,
627-
});
628-
dispatch({ type: treeTypes.blur });
629-
});
630-
}}
631-
onKeyDown={handleKeyDown({
632-
data,
633-
tabbableId: state.tabbableId,
634-
expandedIds: state.expandedIds,
635-
selectedIds: state.selectedIds,
636-
disabledIds: state.disabledIds,
637-
halfSelectedIds: state.halfSelectedIds,
638-
clickAction,
639-
dispatch,
640-
propagateCollapse,
641-
propagateSelect,
642-
multiSelect,
643-
expandOnKeyboardSelect,
644-
togglableSelect,
645-
})}
646-
{...other}
647-
>
648-
{getTreeParent(data).children.map((x, index) => (
649-
<Node
650-
key={`${x}-${typeof x}`}
651-
data={data}
652-
element={getTreeNode(data, x) as INode<M>}
653-
setsize={getTreeParent(data).children.length}
654-
posinset={index + 1}
655-
level={1}
656-
{...state}
657-
state={state}
658-
dispatch={dispatch}
659-
nodeRefs={nodeRefs}
660-
leafRefs={leafRefs}
661-
baseClassNames={baseClassNames}
662-
nodeRenderer={nodeRenderer}
663-
propagateCollapse={propagateCollapse}
664-
propagateSelect={propagateSelect}
665-
propagateSelectUpwards={propagateSelectUpwards}
666-
multiSelect={multiSelect}
667-
togglableSelect={togglableSelect}
668-
clickAction={clickAction}
669-
nodeAction={nodeAction}
670-
/>
671-
))}
672-
</ul>
673-
)
674-
})
675-
676-
677-
573+
const TreeView = React.forwardRef(function TreeView<
574+
M extends IFlatMetadata = IFlatMetadata
575+
>(
576+
{
577+
data,
578+
selectedIds,
579+
nodeRenderer,
580+
onSelect = noop,
581+
onNodeSelect = noop,
582+
onExpand = noop,
583+
onLoadData,
584+
className = "",
585+
multiSelect = false,
586+
propagateSelect = false,
587+
propagateSelectUpwards = false,
588+
propagateCollapse = false,
589+
expandOnKeyboardSelect = false,
590+
togglableSelect = false,
591+
defaultExpandedIds = [],
592+
defaultSelectedIds = [],
593+
defaultDisabledIds = [],
594+
clickAction = clickActions.select,
595+
nodeAction = "select",
596+
expandedIds,
597+
focusedId,
598+
onBlur,
599+
...other
600+
}: ITreeViewProps<M>,
601+
ref: React.ForwardedRef<HTMLUListElement>
602+
) {
603+
validateTreeViewData(data);
604+
const nodeRefs = useRef({});
605+
const leafRefs = useRef({});
606+
let innerRef = useRef<HTMLUListElement | null>(null);
607+
if (ref != null) {
608+
innerRef = ref as React.MutableRefObject<HTMLUListElement>;
609+
}
610+
const [state, dispatch] = useTree({
611+
data,
612+
controlledSelectedIds: selectedIds,
613+
controlledExpandedIds: expandedIds,
614+
defaultExpandedIds,
615+
defaultSelectedIds,
616+
defaultDisabledIds,
617+
nodeRefs,
618+
leafRefs,
619+
onSelect,
620+
onNodeSelect,
621+
onExpand,
622+
onLoadData,
623+
togglableSelect,
624+
multiSelect,
625+
propagateSelect,
626+
propagateSelectUpwards,
627+
treeRef: innerRef,
628+
focusedId,
629+
});
630+
propagateSelect = propagateSelect && multiSelect;
678631

632+
return (
633+
<ul
634+
className={cx(baseClassNames.root, className)}
635+
role="tree"
636+
aria-multiselectable={nodeAction === "select" ? multiSelect : undefined}
637+
ref={innerRef}
638+
onBlur={(event) => {
639+
onComponentBlur(event, innerRef.current, () => {
640+
onBlur &&
641+
onBlur({
642+
treeState: state,
643+
dispatch,
644+
});
645+
dispatch({ type: treeTypes.blur });
646+
});
647+
}}
648+
onKeyDown={handleKeyDown({
649+
data,
650+
tabbableId: state.tabbableId,
651+
expandedIds: state.expandedIds,
652+
selectedIds: state.selectedIds,
653+
disabledIds: state.disabledIds,
654+
halfSelectedIds: state.halfSelectedIds,
655+
clickAction,
656+
dispatch,
657+
propagateCollapse,
658+
propagateSelect,
659+
multiSelect,
660+
expandOnKeyboardSelect,
661+
togglableSelect,
662+
})}
663+
{...other}
664+
>
665+
{getTreeParent(data).children.map((x, index) => (
666+
<Node
667+
key={`${x}-${typeof x}`}
668+
data={data}
669+
element={getTreeNode(data, x) as INode<M>}
670+
setsize={getTreeParent(data).children.length}
671+
posinset={index + 1}
672+
level={1}
673+
{...state}
674+
state={state}
675+
dispatch={dispatch}
676+
nodeRefs={nodeRefs}
677+
leafRefs={leafRefs}
678+
baseClassNames={baseClassNames}
679+
nodeRenderer={nodeRenderer}
680+
propagateCollapse={propagateCollapse}
681+
propagateSelect={propagateSelect}
682+
propagateSelectUpwards={propagateSelectUpwards}
683+
multiSelect={multiSelect}
684+
togglableSelect={togglableSelect}
685+
clickAction={clickAction}
686+
nodeAction={nodeAction}
687+
/>
688+
))}
689+
</ul>
690+
);
691+
});
679692

680693
const handleKeyDown = ({
681694
data,
@@ -979,7 +992,6 @@ const handleKeyDown = ({
979992
}
980993
};
981994

982-
983995
TreeView.propTypes = {
984996
/** Tree data*/
985997
data: PropTypes.array.isRequired,

src/TreeView/reducer.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,12 +199,18 @@ export const treeReducer = (
199199

200200
const halfSelectedIds = new Set<NodeId>(state.halfSelectedIds);
201201
halfSelectedIds.delete(action.id);
202+
203+
const tabbableId = action.keepFocus ? state.tabbableId : action.id;
204+
const isFocused =
205+
tabbableId === action.lastInteractedWith ||
206+
action.NotUserAction !== true;
207+
202208
return {
203209
...state,
204210
selectedIds,
205211
halfSelectedIds,
206-
tabbableId: action.keepFocus ? state.tabbableId : action.id,
207-
isFocused: action.NotUserAction !== true,
212+
tabbableId,
213+
isFocused,
208214
lastUserSelect: action.NotUserAction ? state.lastUserSelect : action.id,
209215
lastAction: action.type,
210216
lastInteractedWith: action.lastInteractedWith,

src/__tests__/ControlledTree.test.tsx

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -753,16 +753,18 @@ describe("Data with ids", () => {
753753
expect(newNodes[4]).toHaveAttribute("aria-checked", "false");
754754
expect(newNodes[5]).toHaveAttribute("aria-checked", "false");
755755
});
756-
756+
757757
test("SelectedIds should not steal focus if another control has it", () => {
758758
let selectedIds = [42];
759-
const renderContent = (<div>
760-
<input type="text" id="editor"/>
761-
<MultiSelectCheckboxControlled
762-
selectedIds={selectedIds}
763-
data={dataWithIds}
764-
/>
765-
</div>);
759+
const renderContent = (
760+
<div>
761+
<input type="text" id="editor" />
762+
<MultiSelectCheckboxControlled
763+
selectedIds={selectedIds}
764+
data={dataWithIds}
765+
/>
766+
</div>
767+
);
766768
const { rerender } = render(renderContent);
767769

768770
const editorElement = document?.getElementById("editor");
@@ -771,4 +773,32 @@ describe("Data with ids", () => {
771773
rerender(renderContent);
772774
expect(document.activeElement).toEqual(editorElement);
773775
});
776+
777+
test("SelectedIds should not reset propagated id after previous controlled ids", () => {
778+
const { rerender, queryAllByRole } = render(
779+
<MultiSelectCheckboxControlled
780+
selectedIds={[8, 9, 10, 11]}
781+
defaultExpandedIds={[7]}
782+
data={dataWithoutIds}
783+
/>
784+
);
785+
rerender(
786+
<MultiSelectCheckboxControlled
787+
selectedIds={[8, 9, 10, 11, 1]}
788+
defaultExpandedIds={[7]}
789+
data={dataWithoutIds}
790+
/>
791+
);
792+
const newNodes = queryAllByRole("treeitem");
793+
expect(newNodes).toHaveLength(8);
794+
795+
expect(newNodes[0].children[0]).toHaveClass("tree-node--focused"); // Fruits
796+
expect(newNodes[0]).toHaveAttribute("aria-checked", "true"); // Fruits
797+
expect(newNodes[1]).toHaveAttribute("aria-checked", "true"); // Drinks
798+
expect(newNodes[2]).toHaveAttribute("aria-checked", "true"); // Drinks->Apple Juice
799+
expect(newNodes[3]).toHaveAttribute("aria-checked", "true"); // Drinks->Chocolate
800+
expect(newNodes[4]).toHaveAttribute("aria-checked", "true"); // Drinks->Coffee
801+
expect(newNodes[5]).toHaveAttribute("aria-checked", "true"); // Drinks->Tea
802+
expect(newNodes[6]).toHaveAttribute("aria-checked", "false"); // Vegetables
803+
});
774804
});

0 commit comments

Comments
 (0)