Skip to content

Commit

Permalink
feat: handle navigation between the grid and list views
Browse files Browse the repository at this point in the history
  • Loading branch information
d-rita committed Oct 30, 2024
1 parent 0579262 commit 9d6d6ca
Show file tree
Hide file tree
Showing 19 changed files with 635 additions and 471 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
import { render } from '@testing-library/react'
import { render as originalRender } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import PropTypes from 'prop-types'
import React from 'react'
import CommandPalette from '../command-palette.js'
import { CommandPaletteContextProvider } from '../context/command-palette-context.js'

const CommandPaletteProviderWrapper = ({ children }) => {
return (
<CommandPaletteContextProvider>
{children}
</CommandPaletteContextProvider>
)
}

CommandPaletteProviderWrapper.propTypes = {
children: PropTypes.node,
}

const render = (ui, options) =>
originalRender(ui, { wrapper: CommandPaletteProviderWrapper, ...options })

describe('Command Palette Component', () => {
const headerBarIconTest = 'headerbar-apps-icon'
Expand Down Expand Up @@ -144,7 +161,6 @@ describe('Command Palette Component', () => {
expect(searchField).toHaveValue('Command')

expect(queryByTestId('headerbar-top-apps-list')).not.toBeInTheDocument()
expect(queryByTestId('headerbar-search-results')).toBeInTheDocument()
expect(queryByText(/Results for "Command"/i)).toBeInTheDocument()
expect(queryByText(/Test Command/)).toBeInTheDocument()
expect(queryByText(/Test App/)).not.toBeInTheDocument()
Expand All @@ -153,9 +169,8 @@ describe('Command Palette Component', () => {
const clearButton = getAllByRole('button')[1]
userEvent.click(clearButton)
expect(searchField).toHaveValue('')
expect(
queryByTestId('headerbar-search-results')
).not.toBeInTheDocument()
// back to default view
expect(queryByTestId('headerbar-top-apps-list')).toBeInTheDocument()
})

it('renders Browse Apps View', () => {
Expand Down
136 changes: 57 additions & 79 deletions components/header-bar/src/command-palette/command-palette.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,72 +3,66 @@ import { IconApps24 } from '@dhis2/ui-icons'
import PropTypes from 'prop-types'
import React, { useState, useCallback, useRef, useEffect } from 'react'
import i18n from '../locales/index.js'
import ActionsMenu from './sections/actions-menu.js'
import { useCommandPaletteContext } from './context/command-palette-context.js'
import { useFilter } from './hooks/use-filter.js'
import { useNavigation } from './hooks/use-navigation.js'
import BackButton from './sections/back-button.js'
import ModalContainer from './sections/container.js'
import Search from './sections/search-field.js'
import { filterItemsArray } from './utils/filterItemsArray.js'
import BrowseApps from './views/browse-apps.js'
import BrowseCommands from './views/browse-commands.js'
import BrowseShortcuts from './views/browse-shortcuts.js'
import HomeView from './views/home-view.js'

const MIN_APPS_NUM = 8
import {
BrowseApps,
BrowseCommands,
BrowseShortcuts,
} from './views/list-view.js'

const CommandPalette = ({ apps, commands, shortcuts }) => {
const containerEl = useRef(null)
const [show, setShow] = useState(false)
const [filter, setFilter] = useState('')

const [currentView, setCurrentView] = useState('home')

const showActions = filter.length <= 0 && currentView === 'home'
const { currentView, filter, setFilter } = useCommandPaletteContext()

const handleVisibilityToggle = useCallback(() => setShow(!show), [show])
const handleFilterChange = useCallback(({ value }) => setFilter(value), [])
const handleFilterChange = useCallback(
({ value }) => setFilter(value),
[setFilter]
)

const goToDefaultView = () => {
setFilter('')
setCurrentView('home')
}
const {
filteredApps,
filteredCommands,
filteredShortcuts,
currentViewItemsArray,
} = useFilter({ apps, commands, shortcuts })

const filteredApps = filterItemsArray(apps, filter)
const filteredCommands = filterItemsArray(commands, filter)
const filteredShortcuts = filterItemsArray(shortcuts, filter)
const { handleKeyDown, goToDefaultView, modalRef } = useNavigation({
setShow,
itemsArray: currentViewItemsArray,
show,
})

const handleKeyDown = useCallback(
(event) => {
switch (event.key) {
case 'Escape':
event.preventDefault()
if (currentView === 'home') {
setShow(false)
} else {
goToDefaultView()
}
break
}
useEffect(() => {
const activeItem = document.querySelector('.highlighted')
if (activeItem && typeof activeItem.scrollIntoView === 'function') {
activeItem?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
}
})

if ((event.metaKey || event.ctrlKey) && event.key === '/') {
setShow(!show)
}
},
[currentView, show]
)
useEffect(() => {
if (modalRef.current) {
modalRef.current?.focus()
}
})

const handleFocus = (e) => {
// this is about the focus of the element
// on launch: focus entire element
console.log(e.target, 'e.target')
console.log(document.activeElement, 'active element')
const handleFocus = (event) => {
if (event.target === modalRef?.current) {
modalRef.current?.querySelector('input').focus()
}
}

useEffect(() => {
document.addEventListener('keydown', handleKeyDown)
document.addEventListener('focus', handleFocus)
return () => {
document.removeEventListener('keydown', handleKeyDown)
document.removeEventListener('focus', handleFocus)
}
}, [handleKeyDown])

Expand All @@ -82,68 +76,53 @@ const CommandPalette = ({ apps, commands, shortcuts }) => {
</button>
{show ? (
<ModalContainer setShow={setShow} show={show}>
<div data-test="headerbar-menu" className="headerbar-menu">
<div
data-test="headerbar-menu"
className="headerbar-menu"
ref={modalRef}
tabIndex={0}
onFocus={handleFocus}
>
<Search
value={filter}
onChange={handleFilterChange}
placeholder={
currentView === 'home'
? i18n.t('Search apps, shortcuts, commands')
: currentView === 'apps'
currentView === 'apps'
? i18n.t('Search apps')
: currentView === 'commands'
? i18n.t('Search commands')
: currentView === 'shortcuts'
? i18n.t('Search shortcuts')
: null
: i18n.t('Search apps, shortcuts, commands')
}
/>
<div className="headerbar-menu-content">
{currentView !== 'home' && !filter ? (
<BackButton onClickHandler={goToDefaultView} />
) : null}
{/* switch views */}
{currentView === 'apps' && (
<BrowseApps
{currentView === 'home' && (
<HomeView
apps={filteredApps}
filter={filter}
commands={filteredCommands}
shortcuts={filteredShortcuts}
/>
)}
{currentView === 'apps' && (
<BrowseApps apps={filteredApps} />
)}
{currentView === 'commands' && (
<BrowseCommands
commands={filteredCommands}
filter={filter}
type={'commands'}
/>
<BrowseCommands commands={filteredCommands} />
)}
{currentView === 'shortcuts' && (
<BrowseShortcuts
shortcuts={filteredShortcuts}
filter={filter}
/>
)}
{currentView === 'home' && (
<HomeView
apps={filteredApps}
commands={filteredCommands}
shortcuts={filteredShortcuts}
filter={filter}
/>
)}
{/* actions sections */}
{showActions && (
<ActionsMenu
showAppsList={apps?.length > MIN_APPS_NUM}
showCommandsList={commands?.length > 0}
showShortcutsList={shortcuts?.length > 0}
setCurrentView={setCurrentView}
/>
)}
</div>
</div>
</ModalContainer>
) : null}

<style jsx>{`
button {
display: block;
Expand Down Expand Up @@ -173,9 +152,8 @@ const CommandPalette = ({ apps, commands, shortcuts }) => {
height: 100%;
}
.headerbar-menu-content {
display: flex;
flex-direction: column;
overflow-y: auto;
max-height: calc(544px - 50px);
}
`}</style>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import PropTypes from 'prop-types'
import React, { createContext, useContext, useState } from 'react'

const commandPaletteContext = createContext()

export const CommandPaletteContextProvider = ({ children }) => {
const [filter, setFilter] = useState('')
const [highlightedIndex, setHighlightedIndex] = useState(0)
const [currentView, setCurrentView] = useState('home')
// home view sections
const [activeSection, setActiveSection] = useState('grid')

return (
<commandPaletteContext.Provider
value={{
filter,
setFilter,
highlightedIndex,
setHighlightedIndex,
currentView,
setCurrentView,
activeSection,
setActiveSection,
}}
>
{children}
</commandPaletteContext.Provider>
)
}
CommandPaletteContextProvider.propTypes = {
children: PropTypes.node,
}

export const useCommandPaletteContext = () => useContext(commandPaletteContext)
30 changes: 30 additions & 0 deletions components/header-bar/src/command-palette/hooks/use-filter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useMemo } from 'react'
import { useCommandPaletteContext } from '../context/command-palette-context.js'
import { filterItemsArray } from '../utils/filterItemsArray.js'

export const useFilter = ({ apps, commands, shortcuts }) => {
const { filter, currentView } = useCommandPaletteContext()

const filteredApps = filterItemsArray(apps, filter)
const filteredCommands = filterItemsArray(commands, filter)
const filteredShortcuts = filterItemsArray(shortcuts, filter)

const currentViewItemsArray = useMemo(() => {
if (currentView === 'apps') {
return filteredApps
} else if (currentView === 'commands') {
return filteredCommands
} else if (currentView === 'shortcuts') {
return filteredShortcuts
} else {
return filteredApps.concat(filteredCommands, filteredShortcuts)
}
}, [currentView, filteredApps, filteredCommands, filteredShortcuts])

return {
filteredApps,
filteredCommands,
filteredShortcuts,
currentViewItemsArray,
}
}
Loading

0 comments on commit 9d6d6ca

Please sign in to comment.