From 84a16e597edb3b3948554415622f73f2fb47cd74 Mon Sep 17 00:00:00 2001 From: andrico Date: Sat, 16 Nov 2019 11:16:16 +0000 Subject: [PATCH 1/6] create a filter context create an filter index with all each skill mapped to it's parent tree abstract the top level skillprovider from the app context to accomodate multiple providers that need to be wrapped around the app create stubs for filtering functionality --- src/components/AddToFilterIndex.tsx | 40 +++++++++++++++ src/components/SkillProvider.tsx | 17 ++++++ src/components/SkillTree.tsx | 2 + src/components/SkillTreeGroup.tsx | 5 ++ src/components/__tests__/SkillTree.test.tsx | 2 +- src/context/AppContext.tsx | 2 +- src/context/FilterContext.tsx | 57 +++++++++++++++++++++ src/index.tsx | 2 +- src/models/index.ts | 4 ++ 9 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 src/components/AddToFilterIndex.tsx create mode 100644 src/components/SkillProvider.tsx create mode 100644 src/context/FilterContext.tsx diff --git a/src/components/AddToFilterIndex.tsx b/src/components/AddToFilterIndex.tsx new file mode 100644 index 0000000..565d95c --- /dev/null +++ b/src/components/AddToFilterIndex.tsx @@ -0,0 +1,40 @@ +import { useContext, useEffect } from 'react'; +import { Skill, SkillMap } from '../models'; +import FilterContext from '../context/FilterContext'; + +interface Props { + treeId: string; + skills: Skill[]; +} + +function createSkillsTreeMap(treeId: string, skills: Skill[]) { + const skillsTreeMap: SkillMap = {}; + + function addSkillToMap(currentSkill: Skill[]) { + currentSkill.forEach(skill => { + if (skill.children.length > 0) { + addSkillToMap(skill.children); + } + + skillsTreeMap[skill.id] = treeId; + }); + } + + addSkillToMap(skills); + + return skillsTreeMap; +} + +function AddToFilterIndex(props: Props) { + const { skills, treeId } = props; + const { addToSkillMap } = useContext(FilterContext); + + useEffect(() => { + const skillsTreeMap = createSkillsTreeMap(treeId, skills); + addToSkillMap(skillsTreeMap); + }, []); + + return null; +} + +export default AddToFilterIndex; diff --git a/src/components/SkillProvider.tsx b/src/components/SkillProvider.tsx new file mode 100644 index 0000000..479c902 --- /dev/null +++ b/src/components/SkillProvider.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { AppProvider } from '../context/AppContext'; +import { FilterProvider } from '../context/FilterContext'; + +interface Props { + children: React.ReactNode; +} + +function SkillProvider({ children }: Props) { + return ( + + {children} + + ); +} + +export default SkillProvider; diff --git a/src/components/SkillTree.tsx b/src/components/SkillTree.tsx index 4fb19f2..926fe39 100644 --- a/src/components/SkillTree.tsx +++ b/src/components/SkillTree.tsx @@ -8,6 +8,7 @@ import styled, { BaseThemedCssFunction } from 'styled-components'; import MobileContext from '../context/MobileContext'; import { SkillTheme } from '../theme'; import SkillTreeHeader from './SkillTreeHeader'; +import AddToFilterIndex from './AddToFilterIndex'; const css: BaseThemedCssFunction = require('styled-components').css; @@ -58,6 +59,7 @@ function SkillTree({ handleSave={handleSave} > + React.ReactNode; @@ -19,12 +20,16 @@ function SkillTreeGroup({ theme, children }: Props) { AppContext ); + const { filtersMatches, handleFilter } = React.useContext(FilterContext); + const skillTreeTheme = { ...defaultTheme, ...theme }; const treeData = { skillCount, selectedSkillCount, resetSkills, + handleFilter, + filtersMatches, }; return ( diff --git a/src/components/__tests__/SkillTree.test.tsx b/src/components/__tests__/SkillTree.test.tsx index 2d6cf63..3b502bb 100644 --- a/src/components/__tests__/SkillTree.test.tsx +++ b/src/components/__tests__/SkillTree.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { render, fireEvent, act } from '@testing-library/react'; import SkillTree, { Props } from '../SkillTree'; import MockLocalStorage from '../../__mocks__/mockLocalStorage'; -import { SkillProvider } from '../../context/AppContext'; +import SkillProvider from '../../components/SkillProvider'; import SkillTreeGroup from '../../components/SkillTreeGroup'; import { Skill, SkillCount } from '../../models/index'; import { SavedDataType } from '../../'; diff --git a/src/context/AppContext.tsx b/src/context/AppContext.tsx index a858a6b..bcc2053 100644 --- a/src/context/AppContext.tsx +++ b/src/context/AppContext.tsx @@ -38,7 +38,7 @@ export const initialState = { }, }; -export function SkillProvider({ children }: Props) { +export function AppProvider({ children }: Props) { const [resetId, setResetId] = React.useState(''); const [skillCount, setSkillCount] = React.useState({ required: 0, diff --git a/src/context/FilterContext.tsx b/src/context/FilterContext.tsx new file mode 100644 index 0000000..26f1010 --- /dev/null +++ b/src/context/FilterContext.tsx @@ -0,0 +1,57 @@ +import * as React from 'react'; +import { SkillMap } from '../models'; + +interface Props { + children: React.ReactNode; +} + +interface IFilterContext { + filtersMatches: Set | null; + handleFilter: (query: string) => void; + addToSkillMap: (skillMap: SkillMap) => void; +} + +const FilterContext = React.createContext({ + filtersMatches: null, + handleFilter: () => null, + addToSkillMap: () => null, +}); + +export function FilterProvider(props: Props) { + const [skillMap, setSkillMap] = React.useState({}); + const [filtersMatches, setMatches] = React.useState | null>(null); + // keep the map, also keep track of the sorted keys. (if performance becomes a concern). + + function handleFilter(query: string) { + if (query.trim() === '') { + return setMatches(null); + } + + const skills = Object.keys(skillMap); + const filteredSkills = skills.filter(key => key.includes(query)); + const treeIds = new Set(filteredSkills.map(skill => skillMap[skill])); + + return setMatches(treeIds); + } + + function addToSkillMap(skillMap: SkillMap) { + return setSkillMap(prev => ({ + ...prev, + ...skillMap, + })); + } + + return ( + + {props.children} + + ); +} + +export default FilterContext; diff --git a/src/index.tsx b/src/index.tsx index 18167af..f38848e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,6 +1,6 @@ export { default as SkillTreeGroup } from './components/SkillTreeGroup'; export { default as SkillTree } from './components/SkillTree'; -export { SkillProvider } from './context/AppContext'; +export { default as SkillProvider } from './components/SkillProvider'; export { Skill as SkillType } from './models/index'; export { SavedDataType } from './models/index'; export { SkillTheme as SkillThemeType } from './theme/index'; diff --git a/src/models/index.ts b/src/models/index.ts index 27ce880..4f29c39 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -6,10 +6,14 @@ export type TooltipDirection = 'right' | 'left' | 'top' | 'bottom'; export type NodeState = 'locked' | 'unlocked' | 'selected'; +export type SkillMap = Record; + export interface SkillGroupData { skillCount: SkillCount; selectedSkillCount: SkillCount; resetSkills: VoidFunction; + filtersMatches: Set | null; + handleFilter: (query: string) => void; } export type SavedDataType = Dictionary; From f190abeb7c05dbacfe517501f3fcc8c12a4acf62 Mon Sep 17 00:00:00 2001 From: andrico Date: Sat, 16 Nov 2019 17:10:32 +0000 Subject: [PATCH 2/6] add filter example in the example repo skill tree listens and collapses trees when treeid doesn't match filter query add memoization to functions for small performance iprovements update broken type --- example/components/FIlterInput.tsx | 18 +++ example/index.tsx | 52 +++++++++ src/components/SkillTree.tsx | 103 ++++++++++-------- src/components/SkillTreeGroup.tsx | 2 +- src/components/SkillTreeHeader.tsx | 18 ++- .../{ => filter}/AddToFilterIndex.tsx | 6 +- src/components/filter/FilterListener.tsx | 32 ++++++ src/context/FilterContext.tsx | 3 +- src/models/index.ts | 1 - src/models/utils.ts | 2 +- 10 files changed, 178 insertions(+), 59 deletions(-) create mode 100644 example/components/FIlterInput.tsx rename src/components/{ => filter}/AddToFilterIndex.tsx (82%) create mode 100644 src/components/filter/FilterListener.tsx diff --git a/example/components/FIlterInput.tsx b/example/components/FIlterInput.tsx new file mode 100644 index 0000000..f937fd4 --- /dev/null +++ b/example/components/FIlterInput.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; + +interface Props { + handleFilter: (query: string) => void; +} + +function FilterInput(props: Props) { + const { handleFilter } = props; + return ( + handleFilter(e.target.value)} + placeholder="Search for skill..." + /> + ); +} + +export default FilterInput; diff --git a/example/index.tsx b/example/index.tsx index 700da83..c47bfe0 100644 --- a/example/index.tsx +++ b/example/index.tsx @@ -18,6 +18,7 @@ import { import './index.css'; import { legsPushData, legsPullData, hpSavedData } from './mockData'; import { ContextStorage } from '../src/models'; +import FilterInput from './components/FIlterInput'; function handleSave( storage: ContextStorage, @@ -35,6 +36,7 @@ const App = () => { skillCount, selectedSkillCount, resetSkills, + handleFilter, }: SkillGroupDataType) => { const totalSkillCount = skillCount.optional + skillCount.required; const totalSelectedCount = @@ -60,6 +62,56 @@ const App = () => { Reset + + + + + + + + = require('styled-components').css; @@ -46,54 +47,64 @@ function SkillTree({ const { isMobile } = useContext(MobileContext); const [isVisible, setVisibility] = useState(true); - function toggleVisibility() { - if (!collapsible) return; + const memoizedToggleVisibility = useCallback( + function toggleVisibility() { + if (!collapsible) return; - return setVisibility(!isVisible); - } + return setVisibility(!isVisible); + }, + [isVisible] + ); return ( - - + - - - - - {data.map((skill, i) => { - const displaySeparator = data.length - 1 !== i && isMobile; - - return ( - - - - - ); - })} - - - - + + + + + + + + {data.map((skill, i) => { + const displaySeparator = data.length - 1 !== i && isMobile; + + return ( + + + + + ); + })} + + + + + ); } diff --git a/src/components/SkillTreeGroup.tsx b/src/components/SkillTreeGroup.tsx index a539424..55eafdc 100644 --- a/src/components/SkillTreeGroup.tsx +++ b/src/components/SkillTreeGroup.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import AppContext from '../context/AppContext'; import styled, { ThemeProvider } from 'styled-components'; import defaultTheme from '../theme/index'; -import { DeepPartial } from 'models/utils'; +import { DeepPartial } from '../models/utils'; import { SkillGroupData } from '../models'; import { MobileProvider } from '../context/MobileContext'; import FilterContext from '../context/FilterContext'; diff --git a/src/components/SkillTreeHeader.tsx b/src/components/SkillTreeHeader.tsx index dde097a..b8a04e4 100644 --- a/src/components/SkillTreeHeader.tsx +++ b/src/components/SkillTreeHeader.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import styled, { BaseThemedCssFunction } from 'styled-components'; import SkillCountSubtitle from './SkillCountSubtitle'; import { SkillTheme } from '../theme'; @@ -26,14 +26,20 @@ interface CollapsibleContainerProps { function SkillTreeHeader(props: Props) { const { handleClick, collapsible, isVisible, id, title, description } = props; + + const memoizedHandleKeyDown = useCallback( + function handleKeyDown(e: React.KeyboardEvent) { + if (e.keyCode === 13) { + handleClick(); + } + }, + [handleClick] + ); + return ( ) => { - if (e.keyCode === 13) { - handleClick(); - } - }} + onKeyDown={memoizedHandleKeyDown} onClick={handleClick} isCollapsible={collapsible} > diff --git a/src/components/AddToFilterIndex.tsx b/src/components/filter/AddToFilterIndex.tsx similarity index 82% rename from src/components/AddToFilterIndex.tsx rename to src/components/filter/AddToFilterIndex.tsx index 565d95c..1e396c3 100644 --- a/src/components/AddToFilterIndex.tsx +++ b/src/components/filter/AddToFilterIndex.tsx @@ -1,6 +1,6 @@ import { useContext, useEffect } from 'react'; -import { Skill, SkillMap } from '../models'; -import FilterContext from '../context/FilterContext'; +import { Skill, SkillMap } from '../../models'; +import FilterContext from '../../context/FilterContext'; interface Props { treeId: string; @@ -16,7 +16,7 @@ function createSkillsTreeMap(treeId: string, skills: Skill[]) { addSkillToMap(skill.children); } - skillsTreeMap[skill.id] = treeId; + skillsTreeMap[skill.title.toLowerCase()] = treeId; }); } diff --git a/src/components/filter/FilterListener.tsx b/src/components/filter/FilterListener.tsx new file mode 100644 index 0000000..ef275eb --- /dev/null +++ b/src/components/filter/FilterListener.tsx @@ -0,0 +1,32 @@ +import { useEffect, useContext } from 'react'; +import FilterContext from '../../context/FilterContext'; + +interface Props { + setVisibility: (isVisible: boolean) => void; + isVisible: boolean; + treeId: string; +} + +function FilterListener({ setVisibility, isVisible, treeId }: Props) { + const { filtersMatches } = useContext(FilterContext); + + useEffect(() => { + if (!filtersMatches) { + if (isVisible === true) return; + + return setVisibility(true); + } + + if (!filtersMatches.has(treeId)) { + if (isVisible === false) return; + return setVisibility(false); + } + + if (isVisible === true) return; + return setVisibility(true); + }, [filtersMatches]); + + return null; +} + +export default FilterListener; diff --git a/src/context/FilterContext.tsx b/src/context/FilterContext.tsx index 26f1010..f79ba40 100644 --- a/src/context/FilterContext.tsx +++ b/src/context/FilterContext.tsx @@ -27,8 +27,9 @@ export function FilterProvider(props: Props) { return setMatches(null); } + const sanitizedQuery = query.toLowerCase(); const skills = Object.keys(skillMap); - const filteredSkills = skills.filter(key => key.includes(query)); + const filteredSkills = skills.filter(key => key.includes(sanitizedQuery)); const treeIds = new Set(filteredSkills.map(skill => skillMap[skill])); return setMatches(treeIds); diff --git a/src/models/index.ts b/src/models/index.ts index 4f29c39..aaa6841 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -12,7 +12,6 @@ export interface SkillGroupData { skillCount: SkillCount; selectedSkillCount: SkillCount; resetSkills: VoidFunction; - filtersMatches: Set | null; handleFilter: (query: string) => void; } diff --git a/src/models/utils.ts b/src/models/utils.ts index c9cfded..f24e2aa 100644 --- a/src/models/utils.ts +++ b/src/models/utils.ts @@ -5,5 +5,5 @@ export type Dictionary = { export type Nullable = T | null; export type DeepPartial = { - [P in keyof T]: DeepPartial; + [P in keyof T]?: DeepPartial; }; From fb118fa5d2acbd02e77fc06ba79be14eb539f8b9 Mon Sep 17 00:00:00 2001 From: andrico Date: Sat, 16 Nov 2019 19:48:59 +0000 Subject: [PATCH 3/6] update read me replace mobile context performance improvements --- README.md | 13 ++++++ example/components/FIlterInput.tsx | 2 +- src/components/SkillNode.tsx | 2 +- src/components/SkillTree.tsx | 6 +-- src/components/SkillTreeGroup.tsx | 5 +-- src/components/tooltip/Tooltip.tsx | 17 ++++++-- src/components/ui/Node.tsx | 70 ++++++++++++++++-------------- src/context/MobileContext.tsx | 44 ------------------- src/hooks/useMobile.tsx | 24 ++++++++++ 9 files changed, 94 insertions(+), 89 deletions(-) delete mode 100644 src/context/MobileContext.tsx create mode 100644 src/hooks/useMobile.tsx diff --git a/README.md b/README.md index f34a77f..1b5caa4 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,7 @@ type SkillGroupData = { skillCount: SkillCount; selectedSkillCount: SkillCount; resetSkills: () => void; + handleFilter: (query: string) => void; }; type SkillCount = { @@ -180,6 +181,18 @@ type SavedDataType = { ## Features +### Filtering + +The `` component exposes the `handleFilter()` method which can be used to close any trees that don't contain skills that match the query. This can be used in conjunction with your own input component like so: + +```tsx + handleFilter(e.target.value)} + placeholder="Filter through trees..." +/> +``` + ### Custom Themes It's likely that you're application won't look to hot with a dark blue/rainbow themed skill tree. Fortunately, a custom theme can be supplied to the `SkillTreeGroup` component. The styles passed through will override the defaults to allow your skill tree to fit nicely into your application. The theme object's type is exported in the package as `SkillThemeType`. I don't perform any object merging between the default styles and the user-defined object, so you'll need to fill out the whole object. diff --git a/example/components/FIlterInput.tsx b/example/components/FIlterInput.tsx index f937fd4..9c5d6c4 100644 --- a/example/components/FIlterInput.tsx +++ b/example/components/FIlterInput.tsx @@ -10,7 +10,7 @@ function FilterInput(props: Props) { handleFilter(e.target.value)} - placeholder="Search for skill..." + placeholder="Filter through trees..." /> ); } diff --git a/src/components/SkillNode.tsx b/src/components/SkillNode.tsx index c52334a..094e711 100644 --- a/src/components/SkillNode.tsx +++ b/src/components/SkillNode.tsx @@ -113,7 +113,7 @@ function SkillNode({ return ( = require('styled-components').css; @@ -44,7 +44,7 @@ function SkillTree({ handleSave, collapsible = false, }: Props) { - const { isMobile } = useContext(MobileContext); + const isMobile = useMobile(); const [isVisible, setVisibility] = useState(true); const memoizedToggleVisibility = useCallback( diff --git a/src/components/SkillTreeGroup.tsx b/src/components/SkillTreeGroup.tsx index 55eafdc..e686c94 100644 --- a/src/components/SkillTreeGroup.tsx +++ b/src/components/SkillTreeGroup.tsx @@ -4,7 +4,6 @@ import styled, { ThemeProvider } from 'styled-components'; import defaultTheme from '../theme/index'; import { DeepPartial } from '../models/utils'; import { SkillGroupData } from '../models'; -import { MobileProvider } from '../context/MobileContext'; import FilterContext from '../context/FilterContext'; type Props = { @@ -34,9 +33,7 @@ function SkillTreeGroup({ theme, children }: Props) { return ( - - {children(treeData)} - + {children(treeData)} ); } diff --git a/src/components/tooltip/Tooltip.tsx b/src/components/tooltip/Tooltip.tsx index 7698a77..7179e04 100644 --- a/src/components/tooltip/Tooltip.tsx +++ b/src/components/tooltip/Tooltip.tsx @@ -3,7 +3,7 @@ import TooltipContent from './TooltipContent'; import styled from 'styled-components'; import Tippy from '@tippy.js/react'; import { Tooltip } from '../../models'; -import MobileContext from '../../context/MobileContext'; +import useMobile from '../../hooks/useMobile'; import 'tippy.js/dist/tippy.css'; import 'tippy.js/animations/shift-away.css'; @@ -17,18 +17,27 @@ interface Props { function Tooltip(props: Props) { const { children, tooltip, title } = props; const { direction = 'top', content } = tooltip; - const { isMobile } = React.useContext(MobileContext); + const isMobile = useMobile(); + + const placement = React.useMemo(() => (isMobile ? 'top' : direction), [ + isMobile, + direction, + ]); + + const memoizedContent = React.useMemo(() => { + return ; + }, [content, title]); return ( } + content={memoizedContent} > {children} diff --git a/src/components/ui/Node.tsx b/src/components/ui/Node.tsx index c74dede..6016f6d 100644 --- a/src/components/ui/Node.tsx +++ b/src/components/ui/Node.tsx @@ -25,42 +25,48 @@ interface StyledNodeProps { isIOS: boolean; } -const Node = React.forwardRef( - (props: Props, ref: React.Ref) => { +const isIOS = isIOSDevice(); + +const Node = React.forwardRef(function Node( + props: Props, + ref: React.Ref +) { + const { handleClick, id, currentState, skill } = props; + + const memoizedHandleKeyDown = React.useCallback( function handleKeyDown(e: React.KeyboardEvent) { if (e.keyCode === 13) { handleClick(); } - } - - const { handleClick, id, currentState, skill } = props; - - return ( - - {'icon' in skill ? ( - - - - ) : ( - - {skill.title} - - )} - - ); - } -); + }, + [handleClick] + ); + + return ( + + {'icon' in skill ? ( + + + + ) : ( + + {skill.title} + + )} + + ); +}); export default Node; diff --git a/src/context/MobileContext.tsx b/src/context/MobileContext.tsx deleted file mode 100644 index 9653515..0000000 --- a/src/context/MobileContext.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React, { createContext, useState, useEffect } from 'react'; -import { throttle } from 'lodash'; - -type Props = { - children: React.ReactNode; -}; - -interface IMobileContext { - isMobile: boolean; -} - -const MobileContext = createContext({ - isMobile: true, -}); - -export function MobileProvider({ children }: Props) { - const [isMobile, setMobileState] = useState(false); - - function handleResize() { - setMobileState(window.innerWidth < 1200); - } - - useEffect(() => { - setMobileState(window.innerWidth < 1200); - }, []); - - useEffect(() => { - const throttleHandleResize = throttle(handleResize, 500); - - window.addEventListener('resize', throttleHandleResize); - - return function cleanup() { - window.removeEventListener('resize', throttleHandleResize); - }; - }); - - return ( - - {children} - - ); -} - -export default MobileContext; diff --git a/src/hooks/useMobile.tsx b/src/hooks/useMobile.tsx new file mode 100644 index 0000000..b7f0922 --- /dev/null +++ b/src/hooks/useMobile.tsx @@ -0,0 +1,24 @@ +import { useEffect, useState } from 'react'; +import { throttle } from 'lodash'; + +const useMobile = () => { + const [width, setWidth] = useState(window.innerWidth); + + useEffect(() => { + function handler() { + setWidth(window.innerWidth); + } + + const throttledHandler = throttle(handler, 500); + + window.addEventListener('resize', throttledHandler); + + return () => { + window.removeEventListener('resize', throttledHandler); + }; + }, []); + + return width < 1200; +}; + +export default useMobile; From d60d44ad92ce91d76d85cfcfba5530d65a7c71d6 Mon Sep 17 00:00:00 2001 From: andrico Date: Sat, 16 Nov 2019 20:45:05 +0000 Subject: [PATCH 4/6] test the filter --- example/index.tsx | 1 - src/__tests__/integration.test.tsx | 164 +++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+), 1 deletion(-) diff --git a/example/index.tsx b/example/index.tsx index c47bfe0..67061cb 100644 --- a/example/index.tsx +++ b/example/index.tsx @@ -75,7 +75,6 @@ const App = () => { title="Squat Progression" description="These are the progressions for squats" data={legsPushData} - collapsible /> { const totalSkillCount = skillCount.required + skillCount.optional; const totalSelectedSkillCount = @@ -167,6 +168,10 @@ function renderComponent( + handleFilter(target.value)} + /> { expect(htmlNode).toHaveStyleRule('background', 'grey'); }); }); + + describe('filtering', () => { + it('should display the tree when no query has been made', () => { + const { getByPlaceholderText, getAllByTestId } = renderComponent([]); + + const filterInput = getByPlaceholderText('filter'); + const [visibilityContainer] = getAllByTestId('visibility-container'); + + expect(visibilityContainer).toHaveStyle('opacity: 1'); + + fireEvent.change(filterInput, { + target: { + value: 'ppppa', + }, + }); + + expect(visibilityContainer).toHaveStyle('opacity: 0'); + }); + + it('should display the tree if the query contains a matching skillid', () => { + const { getByPlaceholderText, getAllByTestId } = renderComponent([]); + + const filterInput = getByPlaceholderText('filter'); + const [visibilityContainer] = getAllByTestId('visibility-container'); + + expect(visibilityContainer).toHaveStyle('opacity: 1'); + + fireEvent.change(filterInput, { + target: { + value: 'java', + }, + }); + + expect(visibilityContainer).toHaveStyle('opacity: 1'); + }); + + it('should display the tree if no valid query is present', () => { + const { getByPlaceholderText, getAllByTestId } = renderComponent([]); + + const filterInput = getByPlaceholderText('filter'); + const [visibilityContainer] = getAllByTestId('visibility-container'); + + expect(visibilityContainer).toHaveStyle('opacity: 1'); + + fireEvent.change(filterInput, { + target: { + value: null, + }, + }); + + expect(visibilityContainer).toHaveStyle('opacity: 1'); + }); + + it('should not display the tree if the query does not contain a matching skillid', () => { + const { getByPlaceholderText, getAllByTestId } = renderComponent([]); + + const filterInput = getByPlaceholderText('filter'); + const [visibilityContainer] = getAllByTestId('visibility-container'); + + expect(visibilityContainer).toHaveStyle('opacity: 1'); + + fireEvent.change(filterInput, { + target: { + value: 'javascrooo', + }, + }); + + expect(visibilityContainer).toHaveStyle('opacity: 0'); + }); + + it('should cause a tree to hide/show when the filter query changes', () => { + const { getByPlaceholderText, getAllByTestId } = renderComponent([]); + + const filterInput = getByPlaceholderText('filter'); + const [visibilityContainer] = getAllByTestId('visibility-container'); + + expect(visibilityContainer).toHaveStyle('opacity: 1'); + + fireEvent.change(filterInput, { + target: { + value: 'javascrooo', + }, + }); + + expect(visibilityContainer).toHaveStyle('opacity: 0'); + + fireEvent.change(filterInput, { + target: { + value: 'javascroo', + }, + }); + + expect(visibilityContainer).toHaveStyle('opacity: 0'); + + fireEvent.change(filterInput, { + target: { + value: 'java', + }, + }); + + expect(visibilityContainer).toHaveStyle('opacity: 1'); + + fireEvent.change(filterInput, { + target: { + value: 'javajava', + }, + }); + + expect(visibilityContainer).toHaveStyle('opacity: 0'); + + fireEvent.change(filterInput, { + target: { + value: null, + }, + }); + + expect(visibilityContainer).toHaveStyle('opacity: 1'); + }); + + it('should only display the trees that contain skillIds that match the filter query', () => { + const { getByPlaceholderText, getAllByTestId } = renderComponent( + complexData + ); + + const filterInput = getByPlaceholderText('filter'); + const [visibilityContainerOne, visibilityContainerTwo] = getAllByTestId( + 'visibility-container' + ); + + expect(visibilityContainerOne).toHaveStyle('opacity: 1'); + expect(visibilityContainerTwo).toHaveStyle('opacity: 1'); + + fireEvent.change(filterInput, { + target: { + value: 'css', + }, + }); + + expect(visibilityContainerOne).toHaveStyle('opacity: 1'); + expect(visibilityContainerTwo).toHaveStyle('opacity: 0'); + }); + + it('should display the trees when the filter query contains only spaces', () => { + const { getByPlaceholderText, getAllByTestId } = renderComponent([]); + + const filterInput = getByPlaceholderText('filter'); + const [visibilityContainerOne] = getAllByTestId('visibility-container'); + + expect(visibilityContainerOne).toHaveStyle('opacity: 1'); + + fireEvent.change(filterInput, { + target: { + value: ' ', + }, + }); + + expect(visibilityContainerOne).toHaveStyle('opacity: 1'); + }); + }); }); From 3c5a99bb6f840a631b1454b1d143c467a5821c69 Mon Sep 17 00:00:00 2001 From: andrico Date: Sat, 16 Nov 2019 21:01:43 +0000 Subject: [PATCH 5/6] memoize theme object --- src/components/SkillTreeGroup.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/SkillTreeGroup.tsx b/src/components/SkillTreeGroup.tsx index e686c94..2b70acc 100644 --- a/src/components/SkillTreeGroup.tsx +++ b/src/components/SkillTreeGroup.tsx @@ -19,16 +19,16 @@ function SkillTreeGroup({ theme, children }: Props) { AppContext ); - const { filtersMatches, handleFilter } = React.useContext(FilterContext); + const { handleFilter } = React.useContext(FilterContext); + const skillTreeTheme = React.useMemo(() => ({ ...defaultTheme, ...theme }), [ + theme, + ]); - const skillTreeTheme = { ...defaultTheme, ...theme }; - - const treeData = { + const treeData: SkillGroupData = { skillCount, selectedSkillCount, resetSkills, handleFilter, - filtersMatches, }; return ( From 0c33d38f50fc532566d331ec8622ee11e4db0438 Mon Sep 17 00:00:00 2001 From: andrico Date: Sat, 16 Nov 2019 21:15:03 +0000 Subject: [PATCH 6/6] remove dummy skill trees from test --- example/index.tsx | 48 ----------------------------------------------- 1 file changed, 48 deletions(-) diff --git a/example/index.tsx b/example/index.tsx index 67061cb..ed03173 100644 --- a/example/index.tsx +++ b/example/index.tsx @@ -70,54 +70,6 @@ const App = () => { data={legsPushData} collapsible /> - - - - - - -