From 5c1344872f6d3f8dcecaa594f60e7b36feefa5e3 Mon Sep 17 00:00:00 2001 From: Diana Nanyanzi Date: Wed, 19 Feb 2025 00:10:28 +0300 Subject: [PATCH] feat: adjust grid layout and navigation with screen size and availabe apps - Add tests for navigation of different grid layouts - Add GridNavigation class for grid helper methods - Modify grid layout hook --- .../__tests__/command-palette.test.js | 13 +- .../__tests__/home-view.test.js | 142 +++++++++++++++++- .../src/command-palette/command-palette.js | 23 ++- .../command-palette/hooks/use-grid-layout.js | 61 ++++++++ .../src/command-palette/hooks/use-modal.js | 26 +--- .../command-palette/hooks/use-navigation.js | 17 +-- .../sections/modal-container.js | 2 +- .../src/command-palette/utils/constants.js | 11 +- .../command-palette/utils/grid-functions.js | 99 ------------ .../command-palette/utils/grid-navigation.js | 136 +++++++++++++++++ .../command-palette/utils/home-navigation.js | 58 ++----- .../src/command-palette/views/home-view.js | 9 +- 12 files changed, 408 insertions(+), 189 deletions(-) create mode 100644 components/header-bar/src/command-palette/hooks/use-grid-layout.js delete mode 100644 components/header-bar/src/command-palette/utils/grid-functions.js create mode 100644 components/header-bar/src/command-palette/utils/grid-navigation.js diff --git a/components/header-bar/src/command-palette/__tests__/command-palette.test.js b/components/header-bar/src/command-palette/__tests__/command-palette.test.js index f938d9003..aaaf473bf 100644 --- a/components/header-bar/src/command-palette/__tests__/command-palette.test.js +++ b/components/header-bar/src/command-palette/__tests__/command-palette.test.js @@ -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 ( @@ -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: '', }, ] @@ -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( diff --git a/components/header-bar/src/command-palette/__tests__/home-view.test.js b/components/header-bar/src/command-palette/__tests__/home-view.test.js index 2d86e922e..02f36b0d4 100644 --- a/components/header-bar/src/command-palette/__tests__/home-view.test.js +++ b/components/header-bar/src/command-palette/__tests__/home-view.test.js @@ -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() @@ -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 @@ -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( + + ) + + // 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( + + ) + + // 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') + }) }) diff --git a/components/header-bar/src/command-palette/command-palette.js b/components/header-bar/src/command-palette/command-palette.js index 0cf2eb833..ce7fe9923 100755 --- a/components/header-bar/src/command-palette/command-palette.js +++ b/components/header-bar/src/command-palette/command-palette.js @@ -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( @@ -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(() => { @@ -91,6 +95,11 @@ const CommandPalette = ({ apps, commands, shortcuts }) => { actions={actionsArray} filteredItems={currentViewItemsArray} apps={filteredApps} + gridColumns={ + isMobile + ? GRID_COLUMNS_MOBILE + : GRID_COLUMNS_DESKTOP + } /> ) : ( = 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, + } +} diff --git a/components/header-bar/src/command-palette/hooks/use-modal.js b/components/header-bar/src/command-palette/hooks/use-modal.js index cc80c718a..da3d2e3cc 100644 --- a/components/header-bar/src/command-palette/hooks/use-modal.js +++ b/components/header-bar/src/command-palette/hooks/use-modal.js @@ -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, diff --git a/components/header-bar/src/command-palette/hooks/use-navigation.js b/components/header-bar/src/command-palette/hooks/use-navigation.js index 1b67e5fae..14230e6e9 100644 --- a/components/header-bar/src/command-palette/hooks/use-navigation.js +++ b/components/header-bar/src/command-palette/hooks/use-navigation.js @@ -1,16 +1,11 @@ import { useCallback, useMemo, useRef } from 'react' import { useCommandPaletteContext } from '../context/command-palette-context.js' -import { - GRID_COLUMNS, - GRID_ROWS, - GRID_SECTION, - HOME_VIEW, -} from '../utils/constants.js' +import { GRID_SECTION, HOME_VIEW } from '../utils/constants.js' import { handleHomeNavigation } from '../utils/home-navigation.js' import { handleListNavigation } from '../utils/list-navigation.js' import useModal from './use-modal.js' -export const useNavigation = ({ itemsArray, actionsArray }) => { +export const useNavigation = ({ itemsArray, actionsArray, gridLayout }) => { const modalRef = useRef(null) const { @@ -64,13 +59,16 @@ export const useNavigation = ({ itemsArray, actionsArray }) => { const actionsListLength = actionsArray.length if (showGrid) { + const { columns, rows, size } = gridLayout + const { section, index } = handleHomeNavigation({ event, activeSection, - rows: GRID_ROWS, - columns: GRID_COLUMNS, + rows, + columns, highlightedIndex, actionsListLength, + gridSize: size, }) setActiveSection(section) setHighlightedIndex(index) @@ -90,6 +88,7 @@ export const useNavigation = ({ itemsArray, actionsArray }) => { showGrid, setActiveSection, setHighlightedIndex, + gridLayout, ] ) diff --git a/components/header-bar/src/command-palette/sections/modal-container.js b/components/header-bar/src/command-palette/sections/modal-container.js index dcfb54e26..2f40484a6 100644 --- a/components/header-bar/src/command-palette/sections/modal-container.js +++ b/components/header-bar/src/command-palette/sections/modal-container.js @@ -37,7 +37,6 @@ const ModalContainer = forwardRef(function ModalContainer( modal.addEventListener('click', onClick) modal.addEventListener('focus', handleFocus) modal.addEventListener('keydown', onKeyDown) - return () => { modal.removeEventListener('click', onClick) modal.removeEventListener('focus', handleFocus) @@ -50,6 +49,7 @@ const ModalContainer = forwardRef(function ModalContainer( {children} @@ -115,6 +119,7 @@ HomeView.propTypes = { actions: PropTypes.array, apps: PropTypes.array, filteredItems: PropTypes.array, + gridColumns: PropTypes.number, } export default HomeView