Skip to content

Commit

Permalink
feat: adjust grid layout and navigation with screen size and availabe…
Browse files Browse the repository at this point in the history
… apps

- Add tests for navigation of different grid layouts
- Add GridNavigation class for grid helper methods
- Modify grid layout hook
  • Loading branch information
d-rita committed Mar 4, 2025
1 parent 7987f7a commit 5c13448
Show file tree
Hide file tree
Showing 12 changed files with 408 additions and 189 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'
import React from 'react'
import CommandPalette from '../command-palette.js'
import { CommandPaletteContextProvider } from '../context/command-palette-context.js'
import { MIN_APPS_NUM } from '../utils/constants.js'
import { MIN_APPS_NUM, APP, COMMAND, SHORTCUT } from '../utils/constants.js'

const CommandPaletteProviderWrapper = ({ children }) => {
return (
Expand All @@ -28,27 +28,30 @@ export const minAppsNum = MIN_APPS_NUM // 8
export const testApps = new Array(minAppsNum + 1)
.fill(null)
.map((_, index) => ({
type: APP,
name: `Test App ${index + 1}`,
displayName: `Test App ${index + 1}`,
icon: '',
defaultAction: '',
action: '',
}))

export const testCommands = [
{
type: COMMAND,
name: 'Test Command 1',
displayName: 'Test Command 1',
icon: '',
defaultAction: '',
action: '',
},
]

export const testShortcuts = [
{
type: SHORTCUT,
name: 'Test Shortcut 1',
displayName: 'Test Shortcut 1',
icon: '',
defaultAction: '',
action: '',
},
]

Expand Down Expand Up @@ -83,7 +86,7 @@ describe('Command Palette Component', () => {

// Actions menu
expect(queryByTestId('headerbar-actions-menu')).toBeInTheDocument()
// since apps < MIN_APPS_NUM (8)
// since apps < minAppsNum (8)
expect(queryByTestId('headerbar-browse-apps')).not.toBeInTheDocument()
// since commands < 1
expect(
Expand Down
142 changes: 140 additions & 2 deletions components/header-bar/src/command-palette/__tests__/home-view.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ describe('Command Palette - Home View', () => {
expect(getAllByText(/Test App/)).toHaveLength(8)

// Actions menu
// since apps > MIN_APPS_NUM(8)
// since apps > minAppsNum(8)
expect(queryByTestId('headerbar-browse-apps')).toBeInTheDocument()
// since commands > 1
expect(queryByTestId('headerbar-browse-commands')).toBeInTheDocument()
Expand Down Expand Up @@ -220,9 +220,10 @@ describe('Command Palette - Home View', () => {

await user.keyboard('{ArrowDown}')
expect(rowOneFirstApp).not.toHaveClass('highlighted')

expect(rowTwoFirstApp).toHaveClass('highlighted')
expect(rowTwoFirstApp.querySelector('span')).toHaveTextContent(
'Test App '
'Test App 5'
)

// actions menu
Expand Down Expand Up @@ -305,4 +306,141 @@ describe('Command Palette - Home View', () => {
await user.keyboard('{ArrowUp}')
expect(rowOneFirstApp).toHaveClass('highlighted')
})

it('handles home view navigation for a grid with 2 apps', async () => {
const user = userEvent.setup()
const { container, getByTestId, queryByTestId } = render(
<CommandPalette
apps={testApps.slice(0, 2)}
shortcuts={[]}
commands={[]}
/>
)

// open modal with (Ctrl + k) keys
fireEvent.keyDown(container, { key: 'k', ctrlKey: true })

// topApps
const appsGrid = getByTestId('headerbar-top-apps-list')
const topApps = appsGrid.querySelectorAll('a')

expect(topApps.length).toBe(2)
expect(topApps.length).toBeLessThan(minAppsNum)
expect(queryByTestId('headerbar-browse-apps')).not.toBeInTheDocument()
expect(
queryByTestId('headerbar-browse-commands')
).not.toBeInTheDocument()
expect(
queryByTestId('headerbar-browse-shortcuts')
).not.toBeInTheDocument()

const firstApp = topApps[0]
const secondApp = topApps[1]

expect(firstApp.querySelector('span')).toHaveTextContent('Test App 1')
expect(secondApp.querySelector('span')).toHaveTextContent('Test App 2')

// first highlighted item
expect(firstApp).toHaveClass('highlighted')

// right arrow navigation
await user.keyboard('{ArrowRight}')
expect(secondApp).toHaveClass('highlighted')

await user.keyboard('{ArrowRight}')
expect(firstApp).toHaveClass('highlighted') // loop back to first

// left arrow navigation
await user.keyboard('{ArrowLeft}')
expect(secondApp).toHaveClass('highlighted')

await user.keyboard('{ArrowLeft}')
expect(firstApp).toHaveClass('highlighted') // loop back to first

// down arrow navigation
await user.keyboard('{ArrowDown}')
expect(queryByTestId('headerbar-logout')).toHaveClass('highlighted')

await user.keyboard('{ArrowDown}')
expect(firstApp).toHaveClass('highlighted')

// up arrow navigation
await user.keyboard('{ArrowUp}')
expect(queryByTestId('headerbar-logout')).toHaveClass('highlighted')

await user.keyboard('{ArrowUp}')
expect(firstApp).toHaveClass('highlighted')
})

it('handles home view navigation for a grid with 6 apps', async () => {
const user = userEvent.setup()
const { container, getByTestId, queryByTestId } = render(
<CommandPalette
apps={testApps.slice(0, 6)}
shortcuts={[]}
commands={[]}
/>
)

// open modal with (Ctrl + k) keys
fireEvent.keyDown(container, { key: 'k', ctrlKey: true })

// topApps
const appsGrid = getByTestId('headerbar-top-apps-list')
const topApps = appsGrid.querySelectorAll('a')

expect(topApps.length).toBe(6)
expect(topApps.length).toBeLessThan(minAppsNum)
expect(queryByTestId('headerbar-browse-apps')).not.toBeInTheDocument()
expect(
queryByTestId('headerbar-browse-commands')
).not.toBeInTheDocument()
expect(
queryByTestId('headerbar-browse-shortcuts')
).not.toBeInTheDocument()

const firstApp = topApps[0]
const thirdApp = topApps[2]
const fifthApp = topApps[4]
const sixthApp = topApps[5]

expect(firstApp.querySelector('span')).toHaveTextContent('Test App 1')
expect(thirdApp.querySelector('span')).toHaveTextContent('Test App 3')
expect(fifthApp.querySelector('span')).toHaveTextContent('Test App 5')
expect(sixthApp.querySelector('span')).toHaveTextContent('Test App 6')

// first highlighted item
expect(firstApp).toHaveClass('highlighted')

// right arrow navigation - top Row
await user.keyboard('{ArrowRight}')
await user.keyboard('{ArrowRight}')
expect(thirdApp).toHaveClass('highlighted')

// down navigation - to actions
await user.keyboard('{ArrowDown}')
expect(queryByTestId('headerbar-logout')).toHaveClass('highlighted')

// up navigation - to bottom row
await user.keyboard('{ArrowUp}')
expect(fifthApp).toHaveClass('highlighted')

// left arrow navigation - row 2
await user.keyboard('{ArrowLeft}')
expect(sixthApp).toHaveClass('highlighted')

await user.keyboard('{ArrowLeft}')
expect(fifthApp).toHaveClass('highlighted')

//right arrow navigation - row 2
await user.keyboard('{ArrowRight}')
expect(sixthApp).toHaveClass('highlighted')

await user.keyboard('{ArrowRight}')
expect(fifthApp).toHaveClass('highlighted')

// up to first row
await user.keyboard('{ArrowUp}')
expect(firstApp).toHaveClass('highlighted')
})
})
23 changes: 16 additions & 7 deletions components/header-bar/src/command-palette/command-palette.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,25 @@ import React, { useCallback, useRef, useEffect } from 'react'
import { useCommandPaletteContext } from './context/command-palette-context.js'
import { useAvailableActions } from './hooks/use-actions.js'
import { useFilter } from './hooks/use-filter.js'
import { useGridLayout } from './hooks/use-grid-layout.js'
import { useNavigation } from './hooks/use-navigation.js'
import ModalContainer from './sections/modal-container.js'
import NavigationKeysLegend from './sections/navigation-keys-legend.js'
import SearchFilter from './sections/search-field.js'
import { FILTERABLE_ACTION, HOME_VIEW } from './utils/constants.js'
import {
FILTERABLE_ACTION,
GRID_COLUMNS_DESKTOP,
GRID_COLUMNS_MOBILE,
HOME_VIEW,
} from './utils/constants.js'
import HomeView from './views/home-view.js'
import ListView from './views/list-view.js'

const CommandPalette = ({ apps, commands, shortcuts }) => {
const containerEl = useRef(null)
const { currentView, goToDefaultView, setShowGrid } =
useCommandPaletteContext()
const { currentView, goToDefaultView } = useCommandPaletteContext()

const { isMobile, gridLayout } = useGridLayout(apps)

const actionsArray = useAvailableActions({ apps, shortcuts, commands })
const searchableActions = actionsArray.filter(
Expand All @@ -30,13 +37,10 @@ const CommandPalette = ({ apps, commands, shortcuts }) => {
actions: searchableActions,
})

useEffect(() => {
setShowGrid(apps?.length > 0)
}, [apps, setShowGrid])

const { handleKeyDown, modalRef, setModalOpen, showModal } = useNavigation({
itemsArray: currentViewItemsArray,
actionsArray,
gridLayout,
})

const handleVisibilityToggle = useCallback(() => {
Expand Down Expand Up @@ -91,6 +95,11 @@ const CommandPalette = ({ apps, commands, shortcuts }) => {
actions={actionsArray}
filteredItems={currentViewItemsArray}
apps={filteredApps}
gridColumns={
isMobile
? GRID_COLUMNS_MOBILE
: GRID_COLUMNS_DESKTOP
}
/>
) : (
<ListView
Expand Down
61 changes: 61 additions & 0 deletions components/header-bar/src/command-palette/hooks/use-grid-layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useEffect, useMemo, useState } from 'react'
import { useCommandPaletteContext } from '../context/command-palette-context.js'
import {
GRID_COLUMNS_DESKTOP,
GRID_COLUMNS_MOBILE,
GRID_ROWS_DESKTOP,
GRID_ROWS_MOBILE,
} from '../utils/constants.js'

function getGridLayout(apps, isMobile) {
let columns = isMobile ? GRID_COLUMNS_MOBILE : GRID_COLUMNS_DESKTOP
let rows = isMobile ? GRID_ROWS_MOBILE : GRID_ROWS_DESKTOP

const expectedGridSize = rows * columns
const availableApps =
apps?.length >= expectedGridSize ? expectedGridSize : apps.length

if (availableApps && availableApps < expectedGridSize) {
if (availableApps / columns < 1) {
columns = availableApps
rows = 1
} else if (availableApps % columns === 0) {
rows = availableApps / columns
} else {
rows = Math.trunc(availableApps / columns + 1)
}
}

return { rows, columns, size: availableApps }
}

export const useGridLayout = (apps) => {
const { setShowGrid } = useCommandPaletteContext()

const [isMobile, setIsMobile] = useState(window.innerWidth <= 480)

useEffect(() => {
setShowGrid(apps?.length > 0)
}, [apps, setShowGrid])

useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth <= 480)
}

handleResize()
window.addEventListener('resize', handleResize)

return () => window.removeEventListener('resize', handleResize)
}, [])

const gridLayout = useMemo(
() => getGridLayout(apps, isMobile),
[apps, isMobile]
)

return {
gridLayout,
isMobile,
}
}
26 changes: 8 additions & 18 deletions components/header-bar/src/command-palette/hooks/use-modal.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,17 @@
import { useCallback, useState, useEffect } from 'react'
import { useState, useEffect } from 'react'

const useModal = (modalRef) => {
const [modalOpen, setModalOpen] = useState(false)

const handleOpenModal = useCallback(() => {
if (modalRef.current) {
modalRef.current.showModal()
}
}, [modalRef])

const handleCloseModal = useCallback(() => {
if (modalRef.current) {
modalRef.current.close()
}
}, [modalRef])

useEffect(() => {
if (modalOpen) {
handleOpenModal()
} else {
handleCloseModal()
if (modalRef.current) {
if (modalOpen) {
modalRef.current.showModal()
} else {
modalRef.current.close()
}
}
}, [modalOpen, handleCloseModal, handleOpenModal])
}, [modalOpen, modalRef])

return {
modalOpen,
Expand Down
Loading

0 comments on commit 5c13448

Please sign in to comment.