From f190abeb7c05dbacfe517501f3fcc8c12a4acf62 Mon Sep 17 00:00:00 2001 From: andrico Date: Sat, 16 Nov 2019 17:10:32 +0000 Subject: [PATCH] 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; };