Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions e2e/command-palette-basic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,27 @@ 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('o');

await expect(page.locator('.command-palette-portal [role="combobox"]')).toBeVisible();
});

test('should not show child 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 show child actions at root with option', async ({ page }) => {
await page.goto('/demo');
await triggerCommandPaletteOpen(page);

await expect(page.locator('text=Configure Personal profile')).toBeVisible();
});
});
2 changes: 2 additions & 0 deletions src/app/views/demo/NestedActionDemo/nestedActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const setProfileAction = defineAction({
id: 'set-profile',
title: 'Set profile',
subtitle: 'Select this and then choose one of the options',
shortcut: 'p o',
});

const setToPersonalProfileAction = defineAction({
Expand Down Expand Up @@ -34,6 +35,7 @@ const configureProfileAction = defineAction({
id: 'configure-profile',
title: 'Configure profile',
subtitle: 'Select this to try 2 levels of nested actions',
isolateChildren: false,
});

const configurePersonalProfileAction = defineAction({
Expand Down
2 changes: 1 addition & 1 deletion src/lib/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const CommandPaletteInternal: Component<CommandPaletteProps> = (p) => {
let lastFocusedElem: null | HTMLElement;

function triggerRun(action: WrappedAction) {
runAction(action, state.actionsContext, storeMethods);
runAction(action, state.actionsContext, storeMethods, 'palette');
}

function activatePrevItem() {
Expand Down
2 changes: 2 additions & 0 deletions src/lib/actionUtils/actionUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -103,6 +104,7 @@ describe('Test Action Utils', () => {
const baseStoreMethods = {
selectParentAction: selectParentActionMock,
closePalette: closePaletteMock,
openPalette: openPaletteMock,
};

afterEach(() => {
Expand Down
16 changes: 13 additions & 3 deletions src/lib/actionUtils/actionUtils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { KeyBindingMap } from 'tinykeys';
import { rootParentActionId } from '../constants';
import { ActionId, ActionsContext, StoreMethods, WrappedAction, WrappedActionList } from '../types';
import { ActionId, Actions, ActionsContext, StoreMethods, WrappedAction, WrappedActionList } from '../types';
import { DeepReadonly } from 'solid-js/store';

type RunStoreMethods = {
selectParentAction: StoreMethods['selectParentAction'];
closePalette: StoreMethods['closePalette'];
openPalette: StoreMethods['openPalette'];
};

function getActionContext(action: WrappedAction, actionsContext: ActionsContext) {
Expand All @@ -31,12 +33,16 @@ export function checkActionAllowed(action: WrappedAction, actionsContext: Action
export function runAction(
action: WrappedAction,
actionsContext: ActionsContext,
storeMethods: RunStoreMethods
storeMethods: RunStoreMethods,
invokedBy: 'shortcut' | 'palette'
) {
const { id, run } = action;

if (!run) {
storeMethods.selectParentAction(id);
if (invokedBy === 'shortcut') {
storeMethods.openPalette();
}
return;
}

Expand Down Expand Up @@ -68,7 +74,7 @@ export function getShortcutHandlersMap(
}

event.preventDefault();
runAction(action, actionsContext, storeMethods);
runAction(action, actionsContext, storeMethods, 'shortcut');
};

const shortcut = action.shortcut;
Expand All @@ -82,6 +88,10 @@ export function getShortcutHandlersMap(

type ActiveParentActionIdListArg = Readonly<Array<ActionId>>;

export function getParentAction(action: WrappedAction, actions: DeepReadonly<Actions>) {
return Object.values(actions).filter(({id}) => id === action.parentActionId)[0]
}

export function getActiveParentAction(activeParentActionIdList: ActiveParentActionIdListArg) {
const activeId = activeParentActionIdList.at(-1) || rootParentActionId;
const isRoot = activeId === rootParentActionId;
Expand Down
8 changes: 6 additions & 2 deletions src/lib/createActionList.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createMemo, createEffect } from 'solid-js';
import Fuse from 'fuse.js';
import { useStore } from './StoreContext';
import { checkActionAllowed, getActiveParentAction } from './actionUtils/actionUtils';
import { checkActionAllowed, getParentAction, getActiveParentAction } from './actionUtils/actionUtils';
import { WrappedAction } from './types';

export function createActionList() {
Expand All @@ -19,9 +19,13 @@ export function createNestedActionList() {
const [state] = useStore();

function nestedActionFilter(action: WrappedAction) {
const parent = getParentAction(action, state.actions);
const { activeId, isRoot } = getActiveParentAction(state.activeParentActionIdList);

const isAllowed = isRoot || action.parentActionId === activeId;
const showAtRoot = isRoot && !parent?.isolateChildren;
const isActiveChild = action.parentActionId === activeId;
const isAllowed = showAtRoot || isActiveChild;

return isAllowed;
}

Expand Down
4 changes: 3 additions & 1 deletion src/lib/defineAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const defineAction = (partialAction: PartialAction): Action => {
const keywords = partialAction.keywords || [];
const shortcut = partialAction.shortcut || null;
const run = partialAction.run;
const isolateChildren = partialAction.isolateChildren ?? true;

const normalizedAction = {
id,
Expand All @@ -18,7 +19,8 @@ export const defineAction = (partialAction: PartialAction): Action => {
shortcut,
cond: partialAction.cond,
run,
};
isolateChildren,
} as Action;

return normalizedAction;
};
21 changes: 20 additions & 1 deletion src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface RunArgs {
dynamicContext: ActionContext;
}

export interface Action {
export interface BaseAction {
id: ActionId;
parentActionId: ParentActionId;
title: string;
Expand All @@ -35,8 +35,27 @@ export interface Action {
*/
cond?: (args: RunArgs) => boolean;
run?: (args: RunArgs) => void;
/**
* Prevent children from being displayed at the root level of the palette.
*
* Default: `true`
*/
isolateChildren?: boolean;
}

export interface ChildAction {
isolateChildren?: never;
}

export interface ParentAction {
parentActionId: never;
run?: never;
}

export type Action =
| (Omit<BaseAction, keyof ChildAction> & ChildAction)
| (Omit<BaseAction, keyof ParentAction> & ParentAction)

export type PartialAction = Partial<Action> & {
id: ActionId;
title: Action['title'];
Expand Down