diff --git a/.gitignore b/.gitignore index b948412..7be701f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ coverage .DS_Store test-results/ playwright-report/ +.vscode diff --git a/e2e/command-palette-basic.spec.ts b/e2e/command-palette-basic.spec.ts index f0e681e..a9475dc 100644 --- a/e2e/command-palette-basic.spec.ts +++ b/e2e/command-palette-basic.spec.ts @@ -1,15 +1,5 @@ -import { test, expect, Page } from '@playwright/test'; -import { checkMac } from './testUtils/checkMac'; - -async function triggerCommandPaletteOpen(page: Page) { - const isMac = await checkMac(page); - - if (isMac) { - await page.keyboard.press('Meta+k'); - } else { - await page.keyboard.press('Control+k'); - } -} +import { test, expect } from '@playwright/test'; +import { triggerCommandPaletteOpen } from './testUtils/triggerCommandPaletteOpen'; test.describe('Test basic interactions of Command Palette', () => { test('should be able to open command palette & run first action', async ({ page }) => { @@ -102,4 +92,13 @@ test.describe('Test basic interactions of Command Palette', () => { await expect(profileStatusLocator).toBeVisible(); }); + + test('should be able to open nested actions using keyboard', async ({ page }) => { + await page.goto('/demo'); + + await page.keyboard.press('p'); + await page.keyboard.press('s'); + + await expect(page.locator('.command-palette-portal [role="combobox"]')).toBeVisible(); + }); }); diff --git a/e2e/command-palette-root-initial-visible-actions.spec.ts b/e2e/command-palette-root-initial-visible-actions.spec.ts new file mode 100644 index 0000000..ea20aaa --- /dev/null +++ b/e2e/command-palette-root-initial-visible-actions.spec.ts @@ -0,0 +1,25 @@ +import { test, expect } from '@playwright/test'; +import { triggerCommandPaletteOpen } from './testUtils/triggerCommandPaletteOpen'; + +test.describe('Test `initialVisibleActions` prop of `Root` component', () => { + test('should not show nested actions at root by default', async ({ page }) => { + await page.goto('/demo'); + await triggerCommandPaletteOpen(page); + + await expect(page.locator('text=Set to Personal profile')).not.toBeVisible(); + }); + + test('should not show nested actions at root when set to `root`', async ({ page }) => { + await page.goto('/demo/InitialVisibleActions/Root'); + await triggerCommandPaletteOpen(page); + + await expect(page.locator('text=Configure Personal profile')).not.toBeVisible(); + }); + + test('should show nested actions at root when set to `all`', async ({ page }) => { + await page.goto('/demo/InitialVisibleActions/All'); + await triggerCommandPaletteOpen(page); + + await expect(page.locator('text=Configure Personal profile')).toBeVisible(); + }); +}); diff --git a/e2e/testUtils/triggerCommandPaletteOpen.ts b/e2e/testUtils/triggerCommandPaletteOpen.ts new file mode 100644 index 0000000..d97ac6e --- /dev/null +++ b/e2e/testUtils/triggerCommandPaletteOpen.ts @@ -0,0 +1,12 @@ +import { Page } from '@playwright/test'; +import { checkMac } from './checkMac'; + +export async function triggerCommandPaletteOpen(page: Page) { + const isMac = await checkMac(page); + + if (isMac) { + await page.keyboard.press('Meta+k'); + } else { + await page.keyboard.press('Control+k'); + } +} diff --git a/src/app/App.tsx b/src/app/App.tsx index f29ec6e..d2af248 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -46,7 +46,15 @@ const routes: Array = [ }, { path: '/demo', - component: lazy(() => import('./views/demo/Demo.view')), + component: lazy(() => import('./views/demo/Default.view')), + }, + { + path: '/demo/InitialVisibleActions/All', + component: lazy(() => import('./views/demo/InitialVisibleActions-All.view')), + }, + { + path: '/demo/InitialVisibleActions/Root', + component: lazy(() => import('./views/demo/InitialVisibleActions-Root.view')), }, { path: '/', diff --git a/src/app/views/demo/Default.view.tsx b/src/app/views/demo/Default.view.tsx new file mode 100644 index 0000000..b917583 --- /dev/null +++ b/src/app/views/demo/Default.view.tsx @@ -0,0 +1,8 @@ +import { Component } from 'solid-js'; +import BaseDemoView from './shared/Demo.view'; + +const DemoView: Component = () => { + return ; +} + +export default DemoView; diff --git a/src/app/views/demo/InitialVisibleActions-All.view.tsx b/src/app/views/demo/InitialVisibleActions-All.view.tsx new file mode 100644 index 0000000..5ef2223 --- /dev/null +++ b/src/app/views/demo/InitialVisibleActions-All.view.tsx @@ -0,0 +1,12 @@ +import { Component } from 'solid-js'; +import BaseDemoView from './shared/Demo.view'; + +const DemoView: Component = () => { + return ( + + ); +} + +export default DemoView; diff --git a/src/app/views/demo/InitialVisibleActions-Root.view.tsx b/src/app/views/demo/InitialVisibleActions-Root.view.tsx new file mode 100644 index 0000000..a241a43 --- /dev/null +++ b/src/app/views/demo/InitialVisibleActions-Root.view.tsx @@ -0,0 +1,12 @@ +import { Component } from 'solid-js'; +import BaseDemoView from './shared/Demo.view'; + +const DemoView: Component = () => { + return ( + + ); +} + +export default DemoView; diff --git a/src/app/views/demo/CustomComponentsDemo/CustomComponentsDemo.module.css b/src/app/views/demo/shared/CustomComponentsDemo/CustomComponentsDemo.module.css similarity index 100% rename from src/app/views/demo/CustomComponentsDemo/CustomComponentsDemo.module.css rename to src/app/views/demo/shared/CustomComponentsDemo/CustomComponentsDemo.module.css diff --git a/src/app/views/demo/CustomComponentsDemo/CustomComponentsDemo.tsx b/src/app/views/demo/shared/CustomComponentsDemo/CustomComponentsDemo.tsx similarity index 86% rename from src/app/views/demo/CustomComponentsDemo/CustomComponentsDemo.tsx rename to src/app/views/demo/shared/CustomComponentsDemo/CustomComponentsDemo.tsx index 285493f..64eb706 100644 --- a/src/app/views/demo/CustomComponentsDemo/CustomComponentsDemo.tsx +++ b/src/app/views/demo/shared/CustomComponentsDemo/CustomComponentsDemo.tsx @@ -1,6 +1,6 @@ import { Component, Show } from 'solid-js'; -import { KbdShortcut, ResultContentProps } from '../../../../lib'; -import utilStyles from '../../../utils.module.css'; +import { KbdShortcut, ResultContentProps } from '../../../../../lib'; +import utilStyles from '../../../../utils.module.css'; import styles from './CustomComponentsDemo.module.css'; export const DemoResultContent: Component = (p) => { diff --git a/src/app/views/demo/CustomComponentsDemo/components.ts b/src/app/views/demo/shared/CustomComponentsDemo/components.ts similarity index 100% rename from src/app/views/demo/CustomComponentsDemo/components.ts rename to src/app/views/demo/shared/CustomComponentsDemo/components.ts diff --git a/src/app/views/demo/Demo.module.css b/src/app/views/demo/shared/Demo.module.css similarity index 100% rename from src/app/views/demo/Demo.module.css rename to src/app/views/demo/shared/Demo.module.css diff --git a/src/app/views/demo/Demo.view.tsx b/src/app/views/demo/shared/Demo.view.tsx similarity index 93% rename from src/app/views/demo/Demo.view.tsx rename to src/app/views/demo/shared/Demo.view.tsx index bee3948..76e009b 100644 --- a/src/app/views/demo/Demo.view.tsx +++ b/src/app/views/demo/shared/Demo.view.tsx @@ -1,16 +1,19 @@ import { Component, createSignal, Show } from 'solid-js'; import { useSearchParams } from 'solid-app-router'; -import { Root, CommandPalette, KbdShortcut } from '../../../lib'; +import { Root, CommandPalette, KbdShortcut } from '../../../../lib'; +import { RootProps } from '../../../../lib/types'; import { actions } from './actions'; import { NestedActionDemo } from './NestedActionDemo/NestedActionDemo'; import { DynamicActionContextDemo } from './DynamicActionContextDemo/DynamicActionContextDemo'; import { components } from './CustomComponentsDemo/components'; import { Profile } from './types'; -import utilStyles from '../../utils.module.css'; +import utilStyles from '../../../utils.module.css'; import demoStyles from './demoUtils.module.css'; import styles from './Demo.module.css'; -const DemoView: Component = () => { +type DemoProps = Pick; + +const DemoView: Component = (p) => { const [count, setCount] = createSignal(0); const [muted, setMuted] = createSignal(false); const [profile, setProfile] = createSignal('personal'); @@ -66,6 +69,7 @@ const DemoView: Component = () => { actions={actions} actionsContext={actionsContext} components={customProps.components} + initialVisibleActions={p.initialVisibleActions} >
diff --git a/src/app/views/demo/DynamicActionContextDemo/DynamicActionContextDemo.module.css b/src/app/views/demo/shared/DynamicActionContextDemo/DynamicActionContextDemo.module.css similarity index 100% rename from src/app/views/demo/DynamicActionContextDemo/DynamicActionContextDemo.module.css rename to src/app/views/demo/shared/DynamicActionContextDemo/DynamicActionContextDemo.module.css diff --git a/src/app/views/demo/DynamicActionContextDemo/DynamicActionContextDemo.tsx b/src/app/views/demo/shared/DynamicActionContextDemo/DynamicActionContextDemo.tsx similarity index 98% rename from src/app/views/demo/DynamicActionContextDemo/DynamicActionContextDemo.tsx rename to src/app/views/demo/shared/DynamicActionContextDemo/DynamicActionContextDemo.tsx index 629b864..21e477c 100644 --- a/src/app/views/demo/DynamicActionContextDemo/DynamicActionContextDemo.tsx +++ b/src/app/views/demo/shared/DynamicActionContextDemo/DynamicActionContextDemo.tsx @@ -1,9 +1,9 @@ import { Component, createMemo, createSignal, createUniqueId, For, Show } from 'solid-js'; -import { KbdShortcut, createSyncActionsContext } from '../../../../lib'; +import { KbdShortcut, createSyncActionsContext } from '../../../../../lib'; import { ownContactId, contacts, contactActionId } from './data'; import { InputEventHandler, ContactItemProps, ReceiverContactDetailsProps } from './types'; import demoStyles from '../demoUtils.module.css'; -import utilStyles from '../../../utils.module.css'; +import utilStyles from '../../../../utils.module.css'; import styles from './DynamicActionContextDemo.module.css'; const ContactItem: Component = (p) => { diff --git a/src/app/views/demo/DynamicActionContextDemo/data.ts b/src/app/views/demo/shared/DynamicActionContextDemo/data.ts similarity index 100% rename from src/app/views/demo/DynamicActionContextDemo/data.ts rename to src/app/views/demo/shared/DynamicActionContextDemo/data.ts diff --git a/src/app/views/demo/DynamicActionContextDemo/dynamicContextActions.ts b/src/app/views/demo/shared/DynamicActionContextDemo/dynamicContextActions.ts similarity index 93% rename from src/app/views/demo/DynamicActionContextDemo/dynamicContextActions.ts rename to src/app/views/demo/shared/DynamicActionContextDemo/dynamicContextActions.ts index 70a1694..5e6a775 100644 --- a/src/app/views/demo/DynamicActionContextDemo/dynamicContextActions.ts +++ b/src/app/views/demo/shared/DynamicActionContextDemo/dynamicContextActions.ts @@ -1,4 +1,4 @@ -import { defineAction } from '../../../../lib'; +import { defineAction } from '../../../../../lib'; import { contactActionId, contacts } from './data'; export const contactAction = defineAction({ diff --git a/src/app/views/demo/DynamicActionContextDemo/types.ts b/src/app/views/demo/shared/DynamicActionContextDemo/types.ts similarity index 100% rename from src/app/views/demo/DynamicActionContextDemo/types.ts rename to src/app/views/demo/shared/DynamicActionContextDemo/types.ts diff --git a/src/app/views/demo/NestedActionDemo/NestedActionDemo.module.css b/src/app/views/demo/shared/NestedActionDemo/NestedActionDemo.module.css similarity index 100% rename from src/app/views/demo/NestedActionDemo/NestedActionDemo.module.css rename to src/app/views/demo/shared/NestedActionDemo/NestedActionDemo.module.css diff --git a/src/app/views/demo/NestedActionDemo/NestedActionDemo.tsx b/src/app/views/demo/shared/NestedActionDemo/NestedActionDemo.tsx similarity index 92% rename from src/app/views/demo/NestedActionDemo/NestedActionDemo.tsx rename to src/app/views/demo/shared/NestedActionDemo/NestedActionDemo.tsx index 81f8b32..1ecaa69 100644 --- a/src/app/views/demo/NestedActionDemo/NestedActionDemo.tsx +++ b/src/app/views/demo/shared/NestedActionDemo/NestedActionDemo.tsx @@ -1,8 +1,8 @@ import { Component } from 'solid-js'; -import { KbdShortcut } from '../../../../lib'; +import { KbdShortcut } from '../../../../../lib'; import { Profile } from '../types'; import demoStyles from '../demoUtils.module.css'; -import utilStyles from '../../../utils.module.css'; +import utilStyles from '../../../../utils.module.css'; import styles from './NestedActionDemo.module.css'; export interface Props { diff --git a/src/app/views/demo/NestedActionDemo/nestedActions.ts b/src/app/views/demo/shared/NestedActionDemo/nestedActions.ts similarity index 97% rename from src/app/views/demo/NestedActionDemo/nestedActions.ts rename to src/app/views/demo/shared/NestedActionDemo/nestedActions.ts index 3c38d7f..3a23866 100644 --- a/src/app/views/demo/NestedActionDemo/nestedActions.ts +++ b/src/app/views/demo/shared/NestedActionDemo/nestedActions.ts @@ -1,9 +1,10 @@ -import { defineAction } from '../../../../lib'; +import { defineAction } from '../../../../../lib'; const setProfileAction = defineAction({ id: 'set-profile', title: 'Set profile', subtitle: 'Select this and then choose one of the options', + shortcut: 'p s', }); const setToPersonalProfileAction = defineAction({ diff --git a/src/app/views/demo/actions.ts b/src/app/views/demo/shared/actions.ts similarity index 97% rename from src/app/views/demo/actions.ts rename to src/app/views/demo/shared/actions.ts index eb9ea78..f8e9537 100644 --- a/src/app/views/demo/actions.ts +++ b/src/app/views/demo/shared/actions.ts @@ -1,4 +1,4 @@ -import { defineAction } from '../../../lib'; +import { defineAction } from '../../../../lib'; import { contactAction } from './DynamicActionContextDemo/dynamicContextActions'; import { nestedActionsConfig } from './NestedActionDemo/nestedActions'; diff --git a/src/app/views/demo/demoUtils.module.css b/src/app/views/demo/shared/demoUtils.module.css similarity index 100% rename from src/app/views/demo/demoUtils.module.css rename to src/app/views/demo/shared/demoUtils.module.css diff --git a/src/app/views/demo/types.ts b/src/app/views/demo/shared/types.ts similarity index 100% rename from src/app/views/demo/types.ts rename to src/app/views/demo/shared/types.ts diff --git a/src/lib/CommandPalette.tsx b/src/lib/CommandPalette.tsx index 25135fb..d923448 100644 --- a/src/lib/CommandPalette.tsx +++ b/src/lib/CommandPalette.tsx @@ -51,7 +51,7 @@ const CommandPaletteInternal: Component = (p) => { let lastFocusedElem: null | HTMLElement; function triggerRun(action: WrappedAction) { - runAction(action, state.actionsContext, storeMethods); + runAction(action, state.actionsContext, storeMethods, 'palette'); } function activatePrevItem() { diff --git a/src/lib/Root.tsx b/src/lib/Root.tsx index 2c5878c..6702a4f 100644 --- a/src/lib/Root.tsx +++ b/src/lib/Root.tsx @@ -15,6 +15,7 @@ const RootInternal: Component = () => { export const Root: Component = (p) => { const initialActions = p.actions || {}; const initialActionsContext = p.actionsContext || {}; + const initialVisibleActions = p.initialVisibleActions || 'root'; const [state, setState] = createStore({ visibility: 'closed', @@ -26,6 +27,7 @@ export const Root: Component = (p) => { dynamic: {}, }, components: p.components, + initialVisibleActions: initialVisibleActions, }); const storeMethods: StoreMethods = { diff --git a/src/lib/actionUtils/actionUtils.test.ts b/src/lib/actionUtils/actionUtils.test.ts index e5aa356..23ddef3 100644 --- a/src/lib/actionUtils/actionUtils.test.ts +++ b/src/lib/actionUtils/actionUtils.test.ts @@ -93,6 +93,7 @@ describe('Test Action Utils', () => { const runMock = vi.fn(); const selectParentActionMock = vi.fn(); const closePaletteMock = vi.fn(); + const openPaletteMock = vi.fn(); const baseAction = { id: 'test-action', @@ -103,6 +104,7 @@ describe('Test Action Utils', () => { const baseStoreMethods = { selectParentAction: selectParentActionMock, closePalette: closePaletteMock, + openPalette: openPaletteMock, }; afterEach(() => { diff --git a/src/lib/actionUtils/actionUtils.ts b/src/lib/actionUtils/actionUtils.ts index c710a92..6c909fc 100644 --- a/src/lib/actionUtils/actionUtils.ts +++ b/src/lib/actionUtils/actionUtils.ts @@ -1,10 +1,11 @@ import { KeyBindingMap } from 'tinykeys'; import { rootParentActionId } from '../constants'; -import { ActionId, ActionsContext, StoreMethods, WrappedAction, WrappedActionList } from '../types'; +import { ActionId, ActionsContext, InvokeBy, StoreMethods, WrappedAction, WrappedActionList } from '../types'; type RunStoreMethods = { selectParentAction: StoreMethods['selectParentAction']; closePalette: StoreMethods['closePalette']; + openPalette: StoreMethods['openPalette']; }; function getActionContext(action: WrappedAction, actionsContext: ActionsContext) { @@ -31,12 +32,16 @@ export function checkActionAllowed(action: WrappedAction, actionsContext: Action export function runAction( action: WrappedAction, actionsContext: ActionsContext, - storeMethods: RunStoreMethods + storeMethods: RunStoreMethods, + invokedBy: InvokeBy ) { const { id, run } = action; if (!run) { storeMethods.selectParentAction(id); + if (invokedBy === 'shortcut') { + storeMethods.openPalette(); + } return; } @@ -68,7 +73,7 @@ export function getShortcutHandlersMap( } event.preventDefault(); - runAction(action, actionsContext, storeMethods); + runAction(action, actionsContext, storeMethods, 'shortcut'); }; const shortcut = action.shortcut; diff --git a/src/lib/createActionList.ts b/src/lib/createActionList.ts index 7c2ee76..074e8b5 100644 --- a/src/lib/createActionList.ts +++ b/src/lib/createActionList.ts @@ -21,7 +21,14 @@ export function createNestedActionList() { function nestedActionFilter(action: WrappedAction) { const { activeId, isRoot } = getActiveParentAction(state.activeParentActionIdList); - const isAllowed = isRoot || action.parentActionId === activeId; + const isRootAction = !action.parentActionId; + const isActiveChild = action.parentActionId === activeId; + + const isAllowed = + (isRoot && isRootAction) + || isActiveChild + || state.initialVisibleActions === 'all' + return isAllowed; } diff --git a/src/lib/defineAction.ts b/src/lib/defineAction.ts index 2a47ad2..20a3903 100644 --- a/src/lib/defineAction.ts +++ b/src/lib/defineAction.ts @@ -18,7 +18,7 @@ export const defineAction = (partialAction: PartialAction): Action => { shortcut, cond: partialAction.cond, run, - }; + } as Action; return normalizedAction; }; diff --git a/src/lib/types.ts b/src/lib/types.ts index 480375f..0b7118e 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -60,6 +60,7 @@ export interface RootProps { actions: Actions; actionsContext: ActionContext; components?: Components; + initialVisibleActions?: InitialVisibleActions; } export interface StoreState { @@ -69,6 +70,12 @@ export interface StoreState { actions: Actions; actionsContext: ActionsContext; components?: Components; + /** + * `root`: nested children are hidden from the root-level palette. (*default*) + * + * `all`: nested children are shown in the root-level palette. + */ + initialVisibleActions?: InitialVisibleActions; } export type StoreStateWrapped = Store; @@ -93,3 +100,7 @@ export type CreateSyncActionsContext = ( actionId: ActionId, callback: CreateSyncActionsContextCallback ) => void; + +export type InitialVisibleActions = 'root' | 'all' + +export type InvokeBy = 'shortcut' | 'palette'