Skip to content

Commit

Permalink
feat: able to toggle all options
Browse files Browse the repository at this point in the history
  • Loading branch information
jeffwcx committed May 13, 2024
1 parent 4cb8fa1 commit 31f8168
Show file tree
Hide file tree
Showing 12 changed files with 145 additions and 7 deletions.
5 changes: 5 additions & 0 deletions .changeset/lemon-kings-nail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'inquirer-select-pro': minor
---

Able to toggle all options
1 change: 1 addition & 0 deletions .changeset/pre.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"cuddly-baboons-cross",
"dry-spies-decide",
"green-cups-wait",
"lemon-kings-nail",
"tame-ducks-type"
]
}
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# inquirer-select-pro

## 1.0.0-alpha.4

### Minor Changes

- Able to toggle all options

## 1.0.0-alpha.3

### Patch Changes
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ declare function useSelect<Value, Multiple extends boolean>(

#### Returns

[**_UseSelectReturnValue_**](./src/types.ts#L142)<!-- -->**_\<Value>_**
[**_UseSelectReturnValue_**](./src/types.ts#L149)<!-- -->**_\<Value>_**

### Theming

Expand Down Expand Up @@ -199,6 +199,7 @@ pnpm test
> Running `pnpm dev` actually allows you to specify the demo directly.
Here is a list of available demos:

- local
- remote
- filter-remote
Expand Down
1 change: 1 addition & 0 deletions examples/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const availableOptions = [
'required',
'loop',
'multiple',
'canToggleAll',
];

let whichDemo: Demos | null;
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "inquirer-select-pro",
"version": "1.0.0-alpha.3",
"version": "1.0.0-alpha.4",
"description": "An inquirer select that supports multiple selections and filtering.",
"keywords": [
"inquirer",
Expand Down
16 changes: 14 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,15 @@ function renderPage<Value>({
}

function renderHelpTip<Value>(context: SelectContext<Value>) {
const { theme, instructions, displayItems, pageSize, behaviors, multiple } =
context;
const {
theme,
instructions,
displayItems,
pageSize,
behaviors,
multiple,
canToggleAll,
} = context;
let helpTipTop = '';
let helpTipBottom = '';
if (
Expand All @@ -102,6 +109,11 @@ function renderHelpTip<Value>(context: SelectContext<Value>) {
if (multiple) {
keys.push(`${theme.style.key('tab')} to select/deselect`);
}
if (canToggleAll) {
keys.push(
`${theme.style.key('ctrl')} + ${theme.style.key('a')} to toggle all`,
);
}
keys.push(`${theme.style.key('enter')} to proceed`);
}

Expand Down
9 changes: 9 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ export interface UseSelectOptions<Value, Multiple extends boolean = true> {
* `false`
*/
clearInputWhenSelected?: boolean;
/**
* Enable toggle all options
*
* @defaultValue
* `false`
*/
canToggleAll?: boolean;

/**
* The user's input is debounced, and the default debounce delay is 200ms.
Expand Down Expand Up @@ -149,6 +156,8 @@ export interface UseSelectReturnValue<Value> {
loop: boolean;
multiple: boolean;
enableFilter: boolean;
canToggleAll: boolean;
required: boolean;
behaviors: SelectBehaviors;
}

Expand Down
39 changes: 37 additions & 2 deletions src/useSelect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import {
useState,
} from '@inquirer/core';
import {
check,
isDownKey,
isSelectAllKey,
isSelectable,
isTabKey,
isUpKey,
Expand Down Expand Up @@ -88,6 +90,7 @@ export function useSelect<Value, Multiple extends boolean>(
required = false,
defaultValue,
clearInputWhenSelected = false,
canToggleAll = false,
inputDelay = 200,
validate = () => true,
equals = (a, b) => a === b,
Expand Down Expand Up @@ -127,6 +130,7 @@ export function useSelect<Value, Multiple extends boolean>(
});

function setBehavior(key: keyof SelectBehaviors, value: boolean) {
if (behaviors[key] === value) return;
setBehaviors({
...behaviors,
[key]: value,
Expand All @@ -144,7 +148,7 @@ export function useSelect<Value, Multiple extends boolean>(
rl.write(filterInput);
}

// <tab/space/enter> selects or deselects an option
// <tab> selects or deselects an option
function handleSelect(
rl: InquirerReadline,
clearInput = clearInputWhenSelected,
Expand Down Expand Up @@ -180,6 +184,33 @@ export function useSelect<Value, Multiple extends boolean>(
);
}

// <ctrl+a> toggle all options
function toggleAll(rl: InquirerReadline) {
if (cursor < 0 || displayItems.length <= 0) {
if (enableFilter) {
keepFilterInput(rl);
}
return;
}
const hasSelectAll = !displayItems.find(
(item) => isSelectable(item) && !item.checked,
);
if (hasSelectAll) {
selections.current = [];
setBehavior('deselect', true);
setDisplayItems(displayItems.map((item) => check(item, false)));
} else {
selections.current = displayItems.reduce((ss, item) => {
if (isSelectable(item)) {
ss.push({ ...item });
}
return ss;
}, [] as SelectOption<Value>[]);
setBehavior('select', true);
setDisplayItems(displayItems.map((item) => check(item, true)));
}
}

// <backspace> Remove the last selected option when filterInput is empty
function removeLastSection() {
if (selections.current.length <= 0) return;
Expand Down Expand Up @@ -245,7 +276,9 @@ export function useSelect<Value, Multiple extends boolean>(
setBehavior('setCursor', true);
setCursor(next);
}
} else if (isTabKey(key) && multiple) {
} else if (canToggleAll && multiple && isSelectAllKey(key)) {
toggleAll(rl);
} else if (multiple && isTabKey(key)) {
handleSelect(rl);
} else {
if (!enableFilter || status === SelectStatus.UNLOADED) return;
Expand Down Expand Up @@ -325,6 +358,8 @@ export function useSelect<Value, Multiple extends boolean>(
loop,
multiple,
enableFilter,
canToggleAll,
required,
behaviors,
};
}
13 changes: 13 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export function isTabKey(key: KeypressEvent) {
return key.name === 'tab';
}

export function isSelectAllKey(key: KeypressEvent) {
return key.name === 'a' && key.ctrl;
}

export function isSelectable<Value>(
item: InternalSelectItem<Value>,
): item is SelectOption<Value> {
Expand All @@ -27,6 +31,15 @@ export function toggle<Value>(
return isSelectable(item) ? { ...item, checked: !item.checked } : item;
}

export function check<Value>(
item: InternalSelectItem<Value>,
checked = true,
): InternalSelectItem<Value> {
return isSelectable(item) && item.checked !== checked
? { ...item, checked }
: item;
}

export function useDebounce<F extends () => void>(func: F, wait: number) {
const ref = useRef<F>();
ref.current = func;
Expand Down
18 changes: 18 additions & 0 deletions tests/__snapshots__/index.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,24 @@ exports[`inquirer-select-pro > interactions > should clear input after the optio
(Use arrow keys to reveal more options)"
`;
exports[`inquirer-select-pro > interactions > should toggle all options when press <ctrl> + <a> 1`] = `
"? Choose movie: (Press <backspace> to remove option)
>> a, b, c, d
>[✔] a
[✔] b
[✔] c
(Use arrow keys to reveal more options)"
`;
exports[`inquirer-select-pro > interactions > should toggle all options when press <ctrl> + <a> 2`] = `
"? Choose movie: (Press <backspace> to remove option)
>> Type to search
>[ ] a
[ ] b
[ ] c
(Use arrow keys to reveal more options)"
`;
exports[`inquirer-select-pro > interactions > should work when options is a function and filter disabled 1`] = `
"? Choose movie: (Press <tab> to select/deselect, <enter> to proceed)
>[ ] The Shawshank Redemption (1994)
Expand Down
39 changes: 38 additions & 1 deletion tests/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,43 @@ describe('inquirer-select-pro', () => {
events.keypress('enter');
expect(getScreen()).toBe(origin);
});

it('should toggle all options when press <ctrl> + <a>', async () => {
const options = [
{ name: 'a', value: 1 },
{ name: 'b', value: 2 },
{ name: 'c', value: 3 },
{ name: 'd', value: 4 },
];
await renderPrompt({
message,
options: (input) =>
options.filter(({ name }) => !input || name === input),
pageSize: 3,
inputDelay: 20,
canToggleAll: true,
required: true,
});
await waitForInteraction();
expect(getScreen().includes('<ctrl> + <a>')).toBe(true);
events.keypress({ name: 'a', ctrl: true });
expect(getScreen()).toMatchSnapshot();
events.keypress({ name: 'a', ctrl: true });
expect(getScreen()).toMatchSnapshot();
events.type('ab');
await wait(25);
await waitForInteraction();
events.keypress({ name: 'a', ctrl: true });
keyseq('backspace', 2);
await wait(25);
await waitForInteraction();
events.keypress({ name: 'a', ctrl: true });
events.keypress('down');
events.keypress('tab');
events.keypress({ name: 'a', ctrl: true });
events.keypress('enter');
await expect(answer).resolves.toHaveLength(4);
});
});

describe('appearance', () => {
Expand Down Expand Up @@ -347,7 +384,7 @@ describe('inquirer-select-pro', () => {
expect(isLoading()).toBe(true);
await waitForInteraction();
events.type('any keys i want');
await wait(10);
await wait(20);
await waitForInteraction();
expect(getScreen().includes(emptyText)).toBe(true);
});
Expand Down

0 comments on commit 31f8168

Please sign in to comment.