From 419d70dd75e55d0afc6ec049be1bb2544aff5672 Mon Sep 17 00:00:00 2001 From: Katsiaryna Pustakhod Date: Mon, 17 Jul 2023 15:24:30 +0200 Subject: [PATCH 1/5] Reset focused node when data mutated --- src/TreeView/index.tsx | 28 +++++++++++++++++++++++++++- src/TreeView/reducer.ts | 17 +++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/TreeView/index.tsx b/src/TreeView/index.tsx index 64f27964..e33ad7d0 100644 --- a/src/TreeView/index.tsx +++ b/src/TreeView/index.tsx @@ -222,6 +222,32 @@ const useTree = ({ state, ]); + useEffect(() => { + if (prevData !== data) { + const treeParentNode = getTreeParent(data); + const newState = { + type: treeTypes.updateTreeState, + tabbableId: !data.find((node) => node.id === state.tabbableId) + ? treeParentNode.children[0] + : state.tabbableId, + lastInteractedWith: !data.find( + (node) => node.id === state.lastInteractedWith + ) + ? null + : state.lastInteractedWith, + lastManuallyToggled: !data.find( + (node) => node.id === state.lastManuallyToggled + ) + ? null + : state.lastManuallyToggled, + lastUserSelect: !data.find((node) => node.id === state.lastUserSelect) + ? treeParentNode.children[0] + : state.lastUserSelect, + }; + dispatch(newState); + } + }, [data]); + const toggledControlledIds = symmetricDifference( new Set(controlledSelectedIds), controlledIds @@ -242,7 +268,7 @@ const useTree = ({ ids: propagatedIds(data, [id], disabledIds), select: true, multiSelect, - lastInteractedWith: id + lastInteractedWith: id, }); } } diff --git a/src/TreeView/reducer.ts b/src/TreeView/reducer.ts index 27204c0d..3b80b02d 100644 --- a/src/TreeView/reducer.ts +++ b/src/TreeView/reducer.ts @@ -19,6 +19,7 @@ export const treeTypes = { enable: "ENABLE", clearLastManuallyToggled: "CLEAR_MANUALLY_TOGGLED", controlledSelectMany: "CONTROLLED_SELECT_MANY", + updateTreeState: "UPDATE_TREE_STATE", } as const; export type TreeViewAction = @@ -87,6 +88,13 @@ export type TreeViewAction = type: "CONTROLLED_SELECT_MANY"; ids: NodeId[]; multiSelect?: boolean; + } + | { + type: "UPDATE_TREE_STATE"; + tabbableId: NodeId; + lastInteractedWith?: NodeId | null; + lastManuallyToggled?: NodeId | null; + lastUserSelect: NodeId; }; export interface ITreeViewState { @@ -389,6 +397,15 @@ export const treeReducer = ( lastManuallyToggled: null, }; } + case treeTypes.updateTreeState: { + return { + ...state, + tabbableId: action.tabbableId, + lastInteractedWith: action.lastInteractedWith, + lastManuallyToggled: action.lastManuallyToggled, + lastUserSelect: action.lastUserSelect, + }; + } default: throw new Error("Invalid action passed to the reducer"); } From 8cad412af690da7bd374f9db11309173c75c6235 Mon Sep 17 00:00:00 2001 From: Katsiaryna Pustakhod Date: Wed, 19 Jul 2023 12:17:47 +0200 Subject: [PATCH 2/5] Add tests and storybook example --- src/__tests__/CheckboxTree.test.tsx | 89 ++ website/docs/examples-Filtering.mdx | 12 + website/docs/examples/Filtering/index.js | 1411 ++++++++++++++++++++ website/docs/examples/Filtering/styles.css | 84 ++ 4 files changed, 1596 insertions(+) create mode 100644 website/docs/examples-Filtering.mdx create mode 100644 website/docs/examples/Filtering/index.js create mode 100644 website/docs/examples/Filtering/styles.css diff --git a/src/__tests__/CheckboxTree.test.tsx b/src/__tests__/CheckboxTree.test.tsx index 3f5dd185..751bf1f4 100644 --- a/src/__tests__/CheckboxTree.test.tsx +++ b/src/__tests__/CheckboxTree.test.tsx @@ -389,3 +389,92 @@ test("should not set focus without interaction with the tree", () => { expect(nodes[0].childNodes[0]).not.toHaveClass("tree-node--focused"); expect(nodes[0].childNodes[1]).not.toHaveClass("tree-node-group--focused"); }); + +test("should set focus on first node if data has changed", () => { + const firstNodeName = "Beets"; + const vegetables = flattenTree({ + name: "", + children: [ + { name: firstNodeName }, + { name: "Carrots" }, + { name: "Celery" }, + { name: "Lettuce" }, + { name: "Onions" }, + ], + }); + const { queryAllByRole, rerender } = render(); + + const nodes = queryAllByRole("treeitem"); + nodes[0].focus(); + + if (document.activeElement == null) + throw new Error( + `Expected to find an active element on the document (after focusing the second element with role["treeitem"]), but did not.` + ); + fireEvent.keyDown(document.activeElement, { key: "ArrowDown" }); //focused Drinks + + expect(document.querySelector(".tree-node--focused")?.innerHTML).toContain( + "Drinks" + ); + + rerender(); + + expect(document.querySelector(".tree-node--focused")?.innerHTML).toContain( + firstNodeName + ); +}); + +test.only("should preserve focus on node if changed data contains previouslt focused node", () => { + const filteredData = flattenTree({ + name: "", + children: [ + { + name: "Fruits", + children: [ + { name: "Avocados" }, + { name: "Bananas" }, + { name: "Berries" }, + { name: "Oranges" }, + { name: "Pears" }, + ], + }, + { + name: "Drinks", + children: [ + { name: "Apple Juice" }, + { name: "Chocolate" }, + { name: "Coffee" }, + { + name: "Tea", + children: [ + { name: "Black Tea" }, + { name: "Green Tea" }, + { name: "Red Tea" }, + { name: "Matcha" }, + ], + }, + ], + }, + ], + }); + const { queryAllByRole, rerender } = render(); + + const nodes = queryAllByRole("treeitem"); + nodes[0].focus(); + + if (document.activeElement == null) + throw new Error( + `Expected to find an active element on the document (after focusing the second element with role["treeitem"]), but did not.` + ); + fireEvent.keyDown(document.activeElement, { key: "ArrowDown" }); //focused Drinks + + expect(document.querySelector(".tree-node--focused")?.innerHTML).toContain( + "Drinks" + ); + + rerender(); + + expect(document.querySelector(".tree-node--focused")?.innerHTML).toContain( + "Drinks" + ); +}); diff --git a/website/docs/examples-Filtering.mdx b/website/docs/examples-Filtering.mdx new file mode 100644 index 00000000..fba52a84 --- /dev/null +++ b/website/docs/examples-Filtering.mdx @@ -0,0 +1,12 @@ +--- +title: Filtering +--- + +The example how to implement filtering within a tree. + +import TreeView from "./examples/Filtering"; +import js from "!!raw-loader!!./examples/Filtering"; +import css from "!!raw-loader!!./examples/Filtering/styles.css"; +import CodeTabs from "../src/components/CodeTabs"; + + diff --git a/website/docs/examples/Filtering/index.js b/website/docs/examples/Filtering/index.js new file mode 100644 index 00000000..8ed92aa1 --- /dev/null +++ b/website/docs/examples/Filtering/index.js @@ -0,0 +1,1411 @@ +import React, { useState } from "react"; +import { FaSquare, FaCheckSquare, FaMinusSquare } from "react-icons/fa"; +import { IoMdArrowDropright } from "react-icons/io"; +import TreeView, { flattenTree } from "react-accessible-treeview"; +import cx from "classnames"; +import "./styles.css"; + +const countries = { + name: "", + children: [ + { + name: "AFGHANISTAN", + currencyCode: "971", + currencyName: "AFGHANI", + }, + { + name: "ALAND ISLANDS", + currencyCode: "978", + currencyName: "EURO", + }, + { + name: "ALBANIA", + currencyCode: "008", + currencyName: "LEK", + }, + { + name: "ALGERIA", + currencyCode: "012", + currencyName: "ALGERIAN DINAR", + }, + { + name: "AMERICAN SAMOA", + currencyCode: "840", + currencyName: "US DOLLAR", + }, + { + name: "ANDORRA", + currencyCode: "978", + currencyName: "EURO", + }, + { + name: "ANGOLA", + currencyCode: "973", + currencyName: "ANGOLAN KWANZA", + }, + { + name: "ANGUILLA", + currencyCode: "951", + currencyName: "EAST CARRIBEAN DOLLAR", + }, + { + name: "ANTARCTICA", + currencyCode: "578", + currencyName: "NORWEGIAN KRONE", + }, + { + name: "ANTIGUA AND BARBUDA", + currencyCode: "951", + currencyName: "EAST CARRIBEAN DOLLAR", + }, + { + name: "ARGENTINA", + currencyCode: "032", + currencyName: "ARGENTINE PESO", + }, + { + name: "ARMENIA", + currencyCode: "051", + currencyName: "ARMENIAN DRAM", + }, + { + name: "ARUBA", + currencyCode: "533", + currencyName: "ARUBAN GUILDER", + }, + { + name: "AUSTRALIA", + currencyCode: "036", + currencyName: "AUSTRALIAN DOLLAR", + }, + { + name: "AUSTRIA", + currencyCode: "978", + currencyName: "EURO", + }, + { + name: "AZERBAIJAN", + currencyCode: "944", + currencyName: "AZERBAIJANIAN MANAT", + }, + { + name: "BAHAMAS", + currencyCode: "044", + currencyName: "BAHAMIAN DOLLAR", + }, + { + name: "BAHRAIN", + currencyCode: "048", + currencyName: "BAHRAINI DINAR", + }, + { + name: "BANGLADESH", + currencyCode: "050", + currencyName: "TAKA", + }, + { + name: "BARBADOS", + currencyCode: "052", + currencyName: "BARBADOS DOLLAR", + }, + { + name: "BELARUS", + currencyCode: "933", + currencyName: "BELARUSIAN RUBLE", + }, + { + name: "BELGIUM", + currencyCode: "978", + currencyName: "EURO", + }, + { + name: "BELIZE", + currencyCode: "084", + currencyName: "BELIZE DOLLAR", + }, + { + name: "BENIN", + currencyCode: "952", + currencyName: "CFA FRANC BCEAO", + }, + { + name: "BERMUDA", + currencyCode: "060", + currencyName: "BERMUDIAN DOLLAR", + }, + { + name: "BHUTAN", + currencyCode: "064", + currencyName: "BHUTANESE NGULTRUM", + }, + { + name: "BHUTAN", + currencyCode: "356", + currencyName: "INDIAN RUPEE", + }, + { + name: "BOLIVIA", + currencyCode: "068", + currencyName: "BOLIVIANO", + }, + { + name: "BONAIRE, SINT EUSTATIUS AND SABA", + currencyCode: "840", + currencyName: "US DOLLAR", + }, + { + name: "BOSNIA and HERZEGOVINA", + currencyCode: "977", + currencyName: "CONVERTIBLE MARK", + }, + { + name: "BOTSWANA", + currencyCode: "072", + currencyName: "PULA", + }, + { + name: "BOUVET ISLAND", + currencyCode: "578", + currencyName: "NORWEGIAN KRONE", + }, + { + name: "BRAZIL", + currencyCode: "986", + currencyName: "BRAZILIAN REAL", + }, + { + name: "BRAZIL", + currencyCode: "996", + currencyName: "SPANISH PESETA", + }, + { + name: "BRITISH INDIAN OCEAN TERRITORY", + currencyCode: "840", + currencyName: "US DOLLAR", + }, + { + name: "BRUNEI DARUSSALAM", + currencyCode: "096", + currencyName: "BRUNEI DOLLAR", + }, + { + name: "BULGARIA", + currencyCode: "975", + currencyName: "BULGARIAN LEV", + }, + { + name: "BURKINA FASO", + currencyCode: "952", + currencyName: "CFA FRANC BCEAO", + }, + { + name: "BURUNDI", + currencyCode: "108", + currencyName: "BURUNDI FRANC", + }, + { + name: "CAMBODIA", + currencyCode: "116", + currencyName: "RIEL", + }, + { + name: "CAMEROON", + currencyCode: "950", + currencyName: "CFA FRANC BEAC", + }, + { + name: "CANADA", + currencyCode: "124", + currencyName: "CANADIAN DOLLAR", + }, + { + name: "CAPE VERDE", + currencyCode: "132", + currencyName: "CAPE VERDE ESCUDO", + }, + { + name: "CAYMAN ISLANDS", + currencyCode: "136", + currencyName: "CAYMAN ISLANDS DOLLAR", + }, + { + name: "CENTRAL AFRICAN REPUBLIC", + currencyCode: "950", + currencyName: "CFA FRANC BEAC", + }, + { + name: "CHAD", + currencyCode: "950", + currencyName: "CFA FRANC BEAC", + }, + { + name: "CHILE", + currencyCode: "152", + currencyName: "CHILEAN PESO", + }, + { + name: "CHINA", + currencyCode: "156", + currencyName: "CHINESE YUAN RENMINBI", + }, + { + name: "CHINA", + currencyCode: "158", + currencyName: "CHINESE PEOPLE'S BANK", + }, + { + name: "CHRISTMAS ISLAND", + currencyCode: "036", + currencyName: "AUSTRALIAN DOLLAR", + }, + { + name: "COCOS (KEELING) ISLANDS", + currencyCode: "036", + currencyName: "AUSTRALIAN DOLLAR", + }, + { + name: "COLOMBIA", + currencyCode: "170", + currencyName: "COLOMBIAN PESO", + }, + { + name: "COMOROS", + currencyCode: "174", + currencyName: "COMORO FRANC", + }, + { + name: "CONGO", + currencyCode: "950", + currencyName: "CFA FRANC BEAC", + }, + { + name: "COOK ISLANDS", + currencyCode: "554", + currencyName: "NEW ZEALAND DOLLAR", + }, + { + name: "COSTA RICA", + currencyCode: "188", + currencyName: "COSTA RICAN COLON", + }, + { + name: "COTE D’IVOIRE (IVORY COAST)", + currencyCode: "952", + currencyName: "CFA FRANC BCEAO", + }, + { + name: "CROATIA", + currencyCode: "191", + currencyName: "CROATIAN KUNA", + }, + { + name: "CURACAO", + currencyCode: "532", + currencyName: "NETHERLANDS ANTILLEAN", + }, + { + name: "CYPRUS", + currencyCode: "978", + currencyName: "EURO", + }, + { + name: "CZECH REPUBLIC", + currencyCode: "203", + currencyName: "CZECH KORUNA", + }, + { + name: "DEMOCRATIC REPUBLIC OF THE CONGO", + currencyCode: "976", + currencyName: "CONGOLESE", + }, + { + name: "DENMARK", + currencyCode: "208", + currencyName: "DANISH KRONE", + }, + { + name: "DJIBOUTI", + currencyCode: "262", + currencyName: "DJIBOUTI FRANC", + }, + { + name: "DOMINICA", + currencyCode: "951", + currencyName: "EAST CARRIBEAN DOLLAR", + }, + { + name: "DOMINICAN REPUBLIC", + currencyCode: "214", + currencyName: "DOMINICAN PESO", + }, + { + name: "EGYPT", + currencyCode: "818", + currencyName: "EGYPTIAN POUND", + }, + { + name: "EL SALVADOR", + currencyCode: "222", + currencyName: "EL SALVADOR COLON", + }, + { + name: "ECUADOR", + currencyCode: "840", + currencyName: "US DOLLAR", + }, + { + name: "EQUATORIAL GUINEA", + currencyCode: "950", + currencyName: "CFA FRANC BEAC", + }, + { + name: "ERITREA", + currencyCode: "230", + currencyName: "ETHIOPIAN BIRR", + }, + { + name: "ERITREA", + currencyCode: "232", + currencyName: "ERITREAN NAKFA", + }, + { + name: "ESTONIA", + currencyCode: "978", + currencyName: "EURO", + }, + { + name: "ETHIOPIA", + currencyCode: "230", + currencyName: "ETHIOPIAN BIRR", + }, + { + name: "FALKLAND ISLANDS (MALVINAS)", + currencyCode: "238", + currencyName: "FALKLAND ISLANDS POUND", + }, + { + name: "FEDERATED STATES OF MICRONESIA", + currencyCode: "840", + currencyName: "US", + }, + { + name: "FIJI", + currencyCode: "242", + currencyName: "FIJI DOLLAR", + }, + { + name: "FINLAND", + currencyCode: "978", + currencyName: "EURO", + }, + { + name: "FRANCE", + currencyCode: "978", + currencyName: "EURO", + }, + { + name: "FRANCE, METROPOLITAN", + currencyCode: "978", + currencyName: "EURO", + }, + { + name: "FRENCH GUIANA", + currencyCode: "978", + currencyName: "EURO", + }, + { + name: "FRENCH POLYNESIA", + currencyCode: "953", + currencyName: "CFP FRANC", + }, + { + name: "GABON", + currencyCode: "950", + currencyName: "CFA FRANC BEAC", + }, + { + name: "GAMBIA", + currencyCode: "270", + currencyName: "DALASI", + }, + { + name: "GEORGIA", + currencyCode: "981", + currencyName: "LARI", + }, + { + name: "GERMANY", + currencyCode: "978", + currencyName: "EURO", + }, + { + name: "GHANA", + currencyCode: "936", + currencyName: "CEDI", + }, + { + name: "GIBRALTAR", + currencyCode: "292", + currencyName: "GIBRALTAR POUND", + }, + { + name: "GREECE", + currencyCode: "978", + currencyName: "EURO", + }, + { + name: "GREENLAND", + currencyCode: "208", + currencyName: "DANISH KRONE", + }, + { + name: "GRENADA", + currencyCode: "951", + currencyName: "EAST CARRIBEAN DOLLAR", + }, + { + name: "GUADELOUPE", + currencyCode: "978", + currencyName: "EURO", + }, + { + name: "GUAM", + currencyCode: "840", + currencyName: "US DOLLAR", + }, + { + name: "GUATEMALA", + currencyCode: "320", + currencyName: "QUETZAL", + }, + { + name: "GUINEA-BISSAU", + currencyCode: "952", + currencyName: "CFA FRANC BCEAO", + }, + { + name: "GUINEA", + currencyCode: "324", + currencyName: "GUINEA FRANC", + }, + { + name: "GUYANA", + currencyCode: "328", + currencyName: "GUYANA DOLLAR", + }, + { + name: "HAITI", + currencyCode: "332", + currencyName: "GOURDE", + }, + { + name: "HEARD and MCDONALD ISLANDS", + currencyCode: "036", + currencyName: "AUSTRALIAN DOLLAR", + }, + { + name: "HOLY SEE (VATICAN CITY STATE)", + currencyCode: "", + currencyName: "978", + }, + { + name: "HONDURUS", + currencyCode: "340", + currencyName: "LEMPIRA", + }, + { + name: "HONG KONG", + currencyCode: "344", + currencyName: "HONG KONG DOLLAR", + }, + { + name: "HUNGARY", + currencyCode: "348", + currencyName: "FORINT", + }, + { + name: "ICELAND", + currencyCode: "352", + currencyName: "ICELANDIC KRONA", + }, + { + name: "INDIA", + currencyCode: "356", + currencyName: "INDIAN RUPEE", + }, + { + name: "INDONESIA", + currencyCode: "360", + currencyName: "RUPIAH", + }, + { + name: "IRAQ", + currencyCode: "368", + currencyName: "IRAQI DINAR", + }, + { + name: "ISRAEL", + currencyCode: "376", + currencyName: "NEW ISRAELI SHEKEL", + }, + { + name: "IRELAND", + currencyCode: "978", + currencyName: "EURO", + }, + { + name: "ISLE OF MAN", + currencyCode: "826", + currencyName: "POUND STERLING", + }, + { + name: "ITALY", + currencyCode: "978", + currencyName: "EURO", + }, + { + name: "JAMAICA", + currencyCode: "388", + currencyName: "JAMAICAN DOLLAR", + }, + { + name: "JAPAN", + currencyCode: "392", + currencyName: "YEN", + }, + { + name: "JERSEY", + currencyCode: "826", + currencyName: "POUND STERLING", + }, + { + name: "JORDAN", + currencyCode: "400", + currencyName: "JORDANIAN DINAR", + }, + { + name: "KAZAKHSTAN", + currencyCode: "398", + currencyName: "TENGE", + }, + { + name: "KENYA", + currencyCode: "404", + currencyName: "KENYAN SHILLING", + }, + { + name: "KIRIBATI", + currencyCode: "036", + currencyName: "AUSTRALIAN DOLLAR", + }, + { + name: "KOREA, REPUBLIC OF", + currencyCode: "410", + currencyName: "WON", + }, + { + name: "KUWAIT", + currencyCode: "414", + currencyName: "KUWAITI DINAR", + }, + { + name: "KYRGYZSTAN", + currencyCode: "417", + currencyName: "SOM", + }, + { + name: "LAO PEOPLE'S DEMOCRATIC REPUBLIC", + currencyCode: "418", + currencyName: "KIP", + }, + { + name: "LATVIA", + currencyCode: "978", + currencyName: "EURO", + }, + { + name: "LEBANON", + currencyCode: "422", + currencyName: "LEBANESE POUND", + }, + { + name: "LESOTHO", + currencyCode: "426", + currencyName: "LOTI", + }, + { + name: "LESOTHO", + currencyCode: "710", + currencyName: "RAND", + }, + { + name: "LIBERIA", + currencyCode: "430", + currencyName: "LIBERIAN DOLLAR", + }, + { + name: "LIBYAN ARAB JAMAHIRIYA", + currencyCode: "434", + currencyName: "LIBYAN DINAR", + }, + { + name: "LIECHTENSTEIN", + currencyCode: "756", + currencyName: "SWISS FRANC", + }, + { + name: "LITHUANIA", + currencyCode: "978", + currencyName: "EURO", + }, + { + name: "LUXEMBOURG", + currencyCode: "978", + currencyName: "EURO", + }, + { + name: "MACAO", + currencyCode: "446", + currencyName: "PATACA", + }, + { + name: "MACEDONIA", + currencyCode: "807", + currencyName: "DENAR", + }, + { + name: "MADAGASCAR", + currencyCode: "969", + currencyName: "MALAGASY ARIARY", + }, + { + name: "MALAWI", + currencyCode: "454", + currencyName: "MALAWI KWACHA", + }, + { + name: "MALAYSIA", + currencyCode: "458", + currencyName: "MALAYSIAN RINGGIT", + }, + { + name: "MALDIVES", + currencyCode: "462", + currencyName: "RUFIYAA", + }, + { + name: "MALI", + currencyCode: "952", + currencyName: "CFA FRANC BCEAO", + }, + { + name: "MALTA", + currencyCode: "978", + currencyName: "EURO", + }, + { + name: "MARSHALL ISLANDS", + currencyCode: "840", + currencyName: "US DOLLAR", + }, + { + name: "MARTINIQUE", + currencyCode: "978", + currencyName: "EURO", + }, + { + name: "MAURITANIA", + currencyCode: "929", + currencyName: "OUGUIYA", + }, + { + name: "MAURITIUS", + currencyCode: "480", + currencyName: "MAURITIUS RUPEE", + }, + { + name: "MAYOTTE", + currencyCode: "978", + currencyName: "EURO", + }, + { + name: "MEXICO", + currencyCode: "484", + currencyName: "MEXICAN PESO", + }, + { + name: "MOLDOVA, REPUBLIC OF", + currencyCode: "498", + currencyName: "MOLDOVAN LEU", + }, + { + name: "MONACO", + currencyCode: "978", + currencyName: "EURO", + }, + { + name: "MONGOLIA", + currencyCode: "496", + currencyName: "TUGRIK", + }, + { + name: "MONTENEGRO, REPUBLIC OF", + currencyCode: "978", + currencyName: "EURO", + }, + { + name: "MONTSERRAT", + currencyCode: "951", + currencyName: "EAST CARRIBEAN DOLLAR", + }, + { + name: "MOROCCO", + currencyCode: "002", + currencyName: "MOROCCAN DIRHAM", + }, + { + name: "MOZAMBIQUE", + currencyCode: "943", + currencyName: "MOZAMBIQUE METICAL", + }, + { + name: "MYANMAR", + currencyCode: "104", + currencyName: "MYANMAR KYAT", + }, + { + name: "NAMIBIA", + currencyCode: "516", + currencyName: "NAMIBIAN DOLLAR", + }, + { + name: "NAURU", + currencyCode: "036", + currencyName: "AUSTRALIAN DOLLAR", + }, + { + name: "NEPAL", + currencyCode: "524", + currencyName: "NEPALESE RUPEE", + }, + { + name: "NETHERLANDS", + currencyCode: "978", + currencyName: "EURO", + }, + { + name: "NETHERLANDS ANTILLES", + currencyCode: "532", + currencyName: "NETHLANDES ANTILLIAN", + }, + { + name: "NEW CALEDONIA", + currencyCode: "953", + currencyName: "CFP FRANC", + }, + { + name: "NEW ZEALAND", + currencyCode: "554", + currencyName: "NEW ZEALAND DOLLAR", + }, + { + name: "NICARAGUA", + currencyCode: "558", + currencyName: "CORDOBA ORO", + }, + { + name: "NIGER", + currencyCode: "952", + currencyName: "CFA FRANC BCEAO", + }, + { + name: "NIGERIA", + currencyCode: "566", + currencyName: "NAIRA", + }, + { + name: "NIUE", + currencyCode: "554", + currencyName: "NEW ZEALAND DOLLAR", + }, + { + name: "NORFOLK ISLAND", + currencyCode: "036", + currencyName: "AUSTRALIAN DOLLAR", + }, + { + name: "NORTHERN MARIANA ISLANDS", + currencyCode: "840", + currencyName: "US DOLLAR", + }, + { + name: "NORWAY", + currencyCode: "578", + currencyName: "NORWEGIAN KRONE", + }, + { + name: "OMAN", + currencyCode: "512", + currencyName: "RIAL OMANI", + }, + { + name: "PAKISTAN", + currencyCode: "586", + currencyName: "PAKISTAN RUPEE", + }, + { + name: "PALAU", + currencyCode: "840", + currencyName: "US DOLLAR", + }, + { + name: "PANAMA", + currencyCode: "590", + currencyName: "BALBOA", + }, + { + name: "PAPUA NEW GUINEA", + currencyCode: "598", + currencyName: "KINA", + }, + { + name: "PARAGUAY", + currencyCode: "600", + currencyName: "GUARANI", + }, + { + name: "PERU", + currencyCode: "604", + currencyName: "NUEVO SOL", + }, + { + name: "PHILIPPINES", + currencyCode: "608", + currencyName: "PHILIPPINE PESO", + }, + { + name: "PITCAIRN", + currencyCode: "554", + currencyName: "NEW ZEALAND DOLLAR", + }, + { + name: "POLAND", + currencyCode: "985", + currencyName: "ZLOTY", + }, + { + name: "PORTUGAL", + currencyCode: "978", + currencyName: "EURO", + }, + { + name: "PUERTO RICO", + currencyCode: "840", + currencyName: "US DOLLAR", + }, + { + name: "QATAR", + currencyCode: "634", + currencyName: "QATARI RIAL", + }, + { + name: "REUNION", + currencyCode: "978", + currencyName: "EURO", + }, + { + name: "ROMANIA", + currencyCode: "946", + currencyName: "ROMANIAN LEU", + }, + { + name: "RUSSIAN FEDERATION", + currencyCode: "643", + currencyName: "RUSSIAN RUBLE", + }, + { + name: "RWANDA", + currencyCode: "646", + currencyName: "RWANDA FRANC", + }, + { + name: "SAINT HELENA, ASCENSION AND TRISTAN DA CUNHA", + currencyCode: "654", + currencyName: "ST. HELENA", + }, + { + name: "SAMOA", + currencyCode: "882", + currencyName: "TALA", + }, + { + name: "SAN MARINO", + currencyCode: "978", + currencyName: "EURO", + }, + { + name: "SAO TOME AND PRINCIPE", + currencyCode: "930", + currencyName: "DOBRA", + }, + { + name: "SAUDI ARABIA", + currencyCode: "682", + currencyName: "SAUDI RIYAL", + }, + { + name: "SENEGAL", + currencyCode: "952", + currencyName: "CFA FRANC BCEAO", + }, + { + name: "SERBIA", + currencyCode: "941", + currencyName: "SERBIAN DINAR", + }, + { + name: "SEYCHELLES", + currencyCode: "690", + currencyName: "SEYCHELLES RUPEE", + }, + { + name: "SIERRA LEONE", + currencyCode: "694", + currencyName: "LEONE", + }, + { + name: "SINGAPORE", + currencyCode: "702", + currencyName: "SINGAPORE DOLLAR", + }, + { + name: "SINT MAARTEN (DUTCH PART)", + currencyCode: "532", + currencyName: "NETHERLANDS ANTILLES", + }, + { + name: "SLOVAKIA", + currencyCode: "978", + currencyName: "EURO", + }, + { + name: "SLOVENIA", + currencyCode: "978", + currencyName: "EURO", + }, + { + name: "SOLOMON ISLANDS", + currencyCode: "090", + currencyName: "SOLOMON ISLANDS DOLLAR", + }, + { + name: "SOMALIA", + currencyCode: "706", + currencyName: "SOMALI SHILLING", + }, + { + name: "SOUTH AFRICA", + currencyCode: "710", + currencyName: "RAND", + }, + { + name: "SOUTH SUDAN", + currencyCode: "728", + currencyName: "SOUTH SUDANESE POUND", + }, + { + name: "SPAIN", + currencyCode: "978", + currencyName: "EURO", + }, + { + name: "SRI LANKA", + currencyCode: "144", + currencyName: "SRI LANKA RUPEE", + }, + { + name: "ST. KITTS-NEVIS", + currencyCode: "951", + currencyName: "EAST CARRIBEAN DOLLAR", + }, + { + name: "ST. LUCIA", + currencyCode: "951", + currencyName: "EAST CARRIBEAN DOLLAR", + }, + { + name: "ST. PIERRE AND MIQUELON", + currencyCode: "978", + currencyName: "EURO", + }, + { + name: "ST. VINCENT AND THE GRENADINES", + currencyCode: "951", + currencyName: "EAST CARRIBEAN", + }, + { + name: "SURINAME", + currencyCode: "968", + currencyName: "SURINAME DOLLAR", + }, + { + name: "SVALBARD AND JAN MAYEN", + currencyCode: "578", + currencyName: "NORWEGIAN KRONE", + }, + { + name: "SWITZERLAND", + currencyCode: "756", + currencyName: "SWISS FRANC", + }, + { + name: "SWAZILAND", + currencyCode: "748", + currencyName: "LILANGENI", + }, + { + name: "SWEDEN", + currencyCode: "752", + currencyName: "SWEDISH KRONA", + }, + { + name: "TAIWAN", + currencyCode: "901", + currencyName: "NEW TAIWAN DOLLAR", + }, + { + name: "TAJIKISTAN", + currencyCode: "972", + currencyName: "SOMONI", + }, + { + name: "TANZANIA, UNITED REPUBLIC OF", + currencyCode: "834", + currencyName: "TANZANIAN", + }, + { + name: "THAILAND", + currencyCode: "764", + currencyName: "BAHT", + }, + { + name: "TIMOR-LESTE", + currencyCode: "840", + currencyName: "US DOLLAR", + }, + { + name: "TOGO", + currencyCode: "952", + currencyName: "CFA FRANC BCEAO", + }, + { + name: "TOKELAU", + currencyCode: "554", + currencyName: "NEW ZEALAND DOLLAR", + }, + { + name: "TONGA", + currencyCode: "776", + currencyName: "PA'ANGA", + }, + { + name: "TRINIDAD AND TOBAGO", + currencyCode: "780", + currencyName: "TRINIDAD AND TOBAGO", + }, + { + name: "TUNISIA", + currencyCode: "788", + currencyName: "TUNISIAN DINAR", + }, + { + name: "TURKEY", + currencyCode: "949", + currencyName: "TURKISH LIRA", + }, + { + name: "TURKMENISTAN", + currencyCode: "934", + currencyName: "MANAT", + }, + { + name: "TURKS and CAICOS ISLANDS", + currencyCode: "840", + currencyName: "US DOLLAR", + }, + { + name: "TUVALU", + currencyCode: "036", + currencyName: "AUSTRALIAN DOLLAR", + }, + { + name: "UGANDA", + currencyCode: "800", + currencyName: "UGANDAN SHILLING", + }, + { + name: "UKRAINE", + currencyCode: "980", + currencyName: "UKRAINIAN HRYVNIA", + }, + { + name: "UNITED ARAB EMIRATES", + currencyCode: "784", + currencyName: "U.A.E. DIRHAM", + }, + { + name: "UNITED KINGDOM", + currencyCode: "826", + currencyName: "POUND STERLING", + }, + { + name: "UNITED STATES", + currencyCode: "840", + currencyName: "US DOLLAR", + }, + { + name: "URUGUAY", + currencyCode: "858", + currencyName: "PESO URUGUAYO", + }, + { + name: "UZBEKISTAN", + currencyCode: "860", + currencyName: "UZBEKISTAN SUM", + }, + { + name: "VANUATU", + currencyCode: "548", + currencyName: "VATU", + }, + { + name: "VENEZUELA", + currencyCode: "928", + currencyName: "BOLIVAR SOBERANO", + }, + { + name: "VIETNAM", + currencyCode: "704", + currencyName: "DONG", + }, + { + name: "BRITISH VIRGIN ISLANDS", + currencyCode: "840", + currencyName: "US DOLLAR", + }, + { + name: "U.S. VIRGIN ISLANDS", + currencyCode: "840", + currencyName: "US DOLLAR", + }, + { + name: "WALLIS AND FUTUNA ISLANDS", + currencyCode: "953", + currencyName: "CFP FRANC", + }, + { + name: "WESTERN SAHARA", + currencyCode: "504", + currencyName: "MOROCCAN DIRHAM", + }, + { + name: "YEMEN", + currencyCode: "886", + currencyName: "YEMENI RIAL", + }, + { + name: "ZAMBIA", + currencyCode: "967", + currencyName: "ZAMBIAN KWACHA", + }, + { + name: "ZIMBABWE", + currencyCode: "716", + currencyName: "ZIMBABWE DOLLAR", + }, + { + name: "CUBA", + currencyCode: "192", + currencyName: "CUBAN PESO", + }, + { + name: "ECUADOR", + currencyCode: "218", + currencyName: "SUCRE", + }, + { + name: "IRAN", + currencyCode: "364", + currencyName: "IRANIAN RIAL", + }, + { + name: "LATVIA", + currencyCode: "428", + currencyName: "LATVIAN LATS", + }, + { + name: "LITHUANIA", + currencyCode: "440", + currencyName: "LITHUANIAN LITAS", + }, + { + name: "GUINEA-BISSAU", + currencyCode: "624", + currencyName: "GUINEA-BISSAU PESO", + }, + { + name: "SYRIA", + currencyCode: "760", + currencyName: "SYRIAN POUND", + }, + { + name: "TONGA", + currencyCode: "776", + currencyName: "PA’ANGA", + }, + ], +}; + +const data = flattenTree(countries); + +function Filtering() { + const [treeData, setTreeData] = useState(data); + + const filter = (value) => { + const filtered = []; + const includeChildren = (id) => { + data.forEach((item) => { + if (item.parent === id) { + if (!filtered.find((x) => x.id === item.id)) { + filtered.push(item); + } + if (item.children.length) { + includeChildren(item.id); + } + } + }); + }; + data.forEach((item) => { + if (item.id === "ROOT") { + return; + } + if (item.name.includes(value.toUpperCase())) { + if (!filtered.find((x) => x.id === item.id)) { + filtered.push(item); + } + + if (item.children.length) { + includeChildren(item.id); + } + } + }); + filtered.unshift( + Object.assign({ + ...data[0], + children: data[0].children.filter((id) => + filtered.find((fitem) => fitem.id === id) + ), + }) + ); + console.log(filtered); + setTreeData(filtered); + }; + + const filterNodesByText = () => { + const valueToFilter = document.querySelector("#txtToFilter").value.trim(); + if (!!valueToFilter) { + filter(valueToFilter); + } else { + setTreeData(data); + } + }; + + const onKeyDown = (e) => { + if (e.key === "Enter") { + filterNodesByText(); + } + }; + + return ( +
+
+ + + +
+ {treeData.length === 1 ? ( +
No nodes match filter
+ ) : ( +
+ { + return ( +
+
+ {isBranch && } + { + handleSelect(e); + e.stopPropagation(); + }} + variant={ + isHalfSelected ? "some" : isSelected ? "all" : "none" + } + /> + {element.name} +
+
+ ); + }} + /> +
+ )} +
+ ); +} + +const ArrowIcon = ({ isOpen, className }) => { + const baseClass = "arrow"; + const classes = cx( + baseClass, + { [`${baseClass}--closed`]: !isOpen }, + { [`${baseClass}--open`]: isOpen }, + className + ); + return ; +}; + +const CheckBoxIcon = ({ variant, ...rest }) => { + switch (variant) { + case "all": + return ; + case "none": + return ; + case "some": + return ; + default: + return null; + } +}; + +export default Filtering; diff --git a/website/docs/examples/Filtering/styles.css b/website/docs/examples/Filtering/styles.css new file mode 100644 index 00000000..60c5a314 --- /dev/null +++ b/website/docs/examples/Filtering/styles.css @@ -0,0 +1,84 @@ +.filtered { + font-size: 16px; + user-select: none; + min-height: 320px; + padding: 20px; + box-sizing: content-box; +} + +.filtered .tree, +.filtered .tree-node, +.filtered .tree-node-group { + list-style: none; + margin: 0; + padding: 0; +} + +.filtered .tree-branch-wrapper { + outline: none; +} + +.filtered .tree-node { + cursor: pointer; +} + +.filtered .tree-node .name:hover { + background: rgba(0, 0, 0, 0.1); +} + +.filtered .tree-node--focused .name { + background: rgba(0, 0, 0, 0.2); +} + +.filtered .tree-node { + display: inline-block; +} + +.filtered .checkbox-icon { + margin: 0 5px; + vertical-align: middle; +} + +.filtered button { + border: none; + background: transparent; + cursor: pointer; +} + +.filtered .arrow { + margin-left: 5px; + vertical-align: middle; +} + +.filtered .arrow--open { + transform: rotate(90deg); +} + +.filtered [role="treeitem"] { + display: inline-flex; + flex-direction: column; + margin-left: px-to-rem(2); +} + +.filtered [role="treeitem"]:focus-visible { + outline: 0.125rem solid #0064d6 !important; +} + +.filtered + [role="treeitem"]:focus-visible + label:not(.ant-checkbox-wrapper-disabled):hover + > span:nth-of-type(2) { + padding-right: px-to-rem(7); + margin-right: px-to-rem(1); + line-height: px-to-rem(16); +} + +.filtered [role="treeitem"].tree-node--focused { + outline: 0.125rem solid #0064d6 !important; +} + +.filtered .tree-node-group, +.filtered .tree { + display: flex; + flex-direction: column; +} From e36644e56c33506cb49cc6fe4bc20762075256c0 Mon Sep 17 00:00:00 2001 From: Katsiaryna Pustakhod Date: Wed, 19 Jul 2023 16:09:11 +0200 Subject: [PATCH 3/5] Refactoring --- src/TreeView/index.tsx | 45 +++++++------ src/TreeView/reducer.ts | 6 +- website/docs/examples/Filtering/index.js | 85 ++++++++++++------------ 3 files changed, 70 insertions(+), 66 deletions(-) diff --git a/src/TreeView/index.tsx b/src/TreeView/index.tsx index e33ad7d0..f27367cd 100644 --- a/src/TreeView/index.tsx +++ b/src/TreeView/index.tsx @@ -222,29 +222,34 @@ const useTree = ({ state, ]); + /** + * When data changes and the last focused item is no longer present in data, + * we need to reset state with existing nodes, e.g. first node in a tree. + */ useEffect(() => { if (prevData !== data) { const treeParentNode = getTreeParent(data); - const newState = { - type: treeTypes.updateTreeState, - tabbableId: !data.find((node) => node.id === state.tabbableId) - ? treeParentNode.children[0] - : state.tabbableId, - lastInteractedWith: !data.find( - (node) => node.id === state.lastInteractedWith - ) - ? null - : state.lastInteractedWith, - lastManuallyToggled: !data.find( - (node) => node.id === state.lastManuallyToggled - ) - ? null - : state.lastManuallyToggled, - lastUserSelect: !data.find((node) => node.id === state.lastUserSelect) - ? treeParentNode.children[0] - : state.lastUserSelect, - }; - dispatch(newState); + if (treeParentNode.children.length) { + dispatch({ + type: treeTypes.updateTreeStateWhenDataChanged, + tabbableId: !data.find((node) => node.id === state.tabbableId) + ? treeParentNode.children[0] + : state.tabbableId, + lastInteractedWith: !data.find( + (node) => node.id === state.lastInteractedWith + ) + ? null + : state.lastInteractedWith, + lastManuallyToggled: !data.find( + (node) => node.id === state.lastManuallyToggled + ) + ? null + : state.lastManuallyToggled, + lastUserSelect: !data.find((node) => node.id === state.lastUserSelect) + ? treeParentNode.children[0] + : state.lastUserSelect, + }); + } } }, [data]); diff --git a/src/TreeView/reducer.ts b/src/TreeView/reducer.ts index 3b80b02d..c4d71082 100644 --- a/src/TreeView/reducer.ts +++ b/src/TreeView/reducer.ts @@ -19,7 +19,7 @@ export const treeTypes = { enable: "ENABLE", clearLastManuallyToggled: "CLEAR_MANUALLY_TOGGLED", controlledSelectMany: "CONTROLLED_SELECT_MANY", - updateTreeState: "UPDATE_TREE_STATE", + updateTreeStateWhenDataChanged: "UPDATE_TREE_STATE_WHEN_DATA_CHANGED", } as const; export type TreeViewAction = @@ -90,7 +90,7 @@ export type TreeViewAction = multiSelect?: boolean; } | { - type: "UPDATE_TREE_STATE"; + type: "UPDATE_TREE_STATE_WHEN_DATA_CHANGED"; tabbableId: NodeId; lastInteractedWith?: NodeId | null; lastManuallyToggled?: NodeId | null; @@ -397,7 +397,7 @@ export const treeReducer = ( lastManuallyToggled: null, }; } - case treeTypes.updateTreeState: { + case treeTypes.updateTreeStateWhenDataChanged: { return { ...state, tabbableId: action.tabbableId, diff --git a/website/docs/examples/Filtering/index.js b/website/docs/examples/Filtering/index.js index 8ed92aa1..04529c86 100644 --- a/website/docs/examples/Filtering/index.js +++ b/website/docs/examples/Filtering/index.js @@ -1307,7 +1307,6 @@ function Filtering() { ), }) ); - console.log(filtered); setTreeData(filtered); }; @@ -1336,49 +1335,49 @@ function Filtering() { {treeData.length === 1 ? (
No nodes match filter
) : ( -
- { - return ( -
-
- {isBranch && } - { - handleSelect(e); - e.stopPropagation(); - }} - variant={ - isHalfSelected ? "some" : isSelected ? "all" : "none" - } - /> - {element.name} -
+
+ { + return ( +
+
+ {isBranch && } + { + handleSelect(e); + e.stopPropagation(); + }} + variant={ + isHalfSelected ? "some" : isSelected ? "all" : "none" + } + /> + {element.name}
- ); - }} - /> -
+
+ ); + }} + /> +
)}
); From df2a1cdd3af227bc5cefaca4a8401779cc4758cc Mon Sep 17 00:00:00 2001 From: Katsiaryna Pustakhod Date: Wed, 19 Jul 2023 16:09:35 +0200 Subject: [PATCH 4/5] Add one more scenario to data validation helper --- src/TreeView/utils.ts | 14 +++++++++++++- src/__tests__/ValidateTreeViewData.test.ts | 10 ++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/TreeView/utils.ts b/src/TreeView/utils.ts index 9e9c4a75..f02f76d2 100644 --- a/src/TreeView/utils.ts +++ b/src/TreeView/utils.ts @@ -478,6 +478,14 @@ const hasDuplicates = (ids: NodeId[]): boolean => { return ids.length !== uniqueIds.length; }; +/** + * We need to validate a tree data for + * - duplicates + * - node references to itself + * - node has duplicated children + * - no root node in a tree + * - more then one root node in a tree + * */ export const validateTreeViewData = (data: INode[]): void => { if (hasDuplicates(data.map((node) => node.id))) { throw Error( @@ -496,7 +504,11 @@ export const validateTreeViewData = (data: INode[]): void => { } }); - if (data.filter((node) => node.parent === null).length !== 1) { + if (data.filter((node) => node.parent === null).length === 0) { + throw Error(`TreeView must have one root node.`); + } + + if (data.filter((node) => node.parent === null).length > 1) { throw Error(`TreeView can have only one root node.`); } diff --git a/src/__tests__/ValidateTreeViewData.test.ts b/src/__tests__/ValidateTreeViewData.test.ts index 4c15623e..7522d887 100644 --- a/src/__tests__/ValidateTreeViewData.test.ts +++ b/src/__tests__/ValidateTreeViewData.test.ts @@ -1,6 +1,16 @@ import { INode } from "../TreeView/types"; import { validateTreeViewData } from "../TreeView/utils"; +test("Should error when no parent node", () => { + const treeViewData: INode[] = [ + { name: "Fruits", id: 0, parent: 14, children: [] }, + { name: "Vegetables", id: 3, parent: 0, children: [] }, + { name: "Drinks", id: 14, parent: 3, children: [] }, + ]; + const expected = () => validateTreeViewData(treeViewData); + expect(expected).toThrow("TreeView must have one root node."); +}) + test("Should error when more then one parent node", () => { const treeViewData: INode[] = [ { name: "Fruits", id: 0, parent: null, children: [] }, From 413491b344e48881185cb41d2b6a14dd7746f5b3 Mon Sep 17 00:00:00 2001 From: Katsiaryna Pustakhod Date: Wed, 19 Jul 2023 16:31:59 +0200 Subject: [PATCH 5/5] Add no children check to data validator. --- src/TreeView/utils.ts | 17 ++++++++++++----- src/__tests__/ValidateTreeViewData.test.ts | 13 ++++++++++++- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/TreeView/utils.ts b/src/TreeView/utils.ts index f02f76d2..5bae86d3 100644 --- a/src/TreeView/utils.ts +++ b/src/TreeView/utils.ts @@ -296,7 +296,9 @@ interface ITreeNode { metadata?: M; } -export const flattenTree = (tree: ITreeNode): INode[] => { +export const flattenTree = ( + tree: ITreeNode +): INode[] => { let internalCount = 0; const flattenedTree: INode[] = []; @@ -306,7 +308,7 @@ export const flattenTree = (tree: ITreeNode): INode< name: tree.name, children: [], parent, - metadata: tree.metadata ? { ...tree.metadata} : undefined + metadata: tree.metadata ? { ...tree.metadata } : undefined, }; if (flattenedTree.find((x) => x.id === node.id)) { @@ -479,12 +481,13 @@ const hasDuplicates = (ids: NodeId[]): boolean => { }; /** - * We need to validate a tree data for + * We need to validate a tree data for * - duplicates * - node references to itself * - node has duplicated children * - no root node in a tree * - more then one root node in a tree + * - to have nodes to display * */ export const validateTreeViewData = (data: INode[]): void => { if (hasDuplicates(data.map((node) => node.id))) { @@ -505,11 +508,15 @@ export const validateTreeViewData = (data: INode[]): void => { }); if (data.filter((node) => node.parent === null).length === 0) { - throw Error(`TreeView must have one root node.`); + throw Error("TreeView must have one root node."); } if (data.filter((node) => node.parent === null).length > 1) { - throw Error(`TreeView can have only one root node.`); + throw Error("TreeView can have only one root node."); + } + + if (!getTreeParent(data).children.length) { + console.warn("TreeView have no nodes to display."); } return; diff --git a/src/__tests__/ValidateTreeViewData.test.ts b/src/__tests__/ValidateTreeViewData.test.ts index 7522d887..2c6f3155 100644 --- a/src/__tests__/ValidateTreeViewData.test.ts +++ b/src/__tests__/ValidateTreeViewData.test.ts @@ -9,7 +9,7 @@ test("Should error when no parent node", () => { ]; const expected = () => validateTreeViewData(treeViewData); expect(expected).toThrow("TreeView must have one root node."); -}) +}); test("Should error when more then one parent node", () => { const treeViewData: INode[] = [ @@ -21,6 +21,17 @@ test("Should error when more then one parent node", () => { expect(expected).toThrow("TreeView can have only one root node."); }); +test("Should warn of no nodes to display", () => { + const treeViewData: INode[] = [ + { name: "", id: 0, parent: null, children: [] }, + ]; + jest.spyOn(console, "warn"); + validateTreeViewData(treeViewData); + expect(console.warn).toHaveBeenCalledWith( + "TreeView have no nodes to display." + ); +}); + test("Should error when node's parent reference to node", () => { const treeViewData: INode[] = [ { name: "Fruits", id: 0, parent: null, children: [] },