diff --git a/.changeset/happy-parents-explain.md b/.changeset/happy-parents-explain.md new file mode 100644 index 00000000..16eb459d --- /dev/null +++ b/.changeset/happy-parents-explain.md @@ -0,0 +1,5 @@ +--- +"@clack/prompts": minor +--- + +Adds a new `groupSpacing` option to grouped multi-select prompts. If set to an integer greater than 0, it will add that number of new lines between each group. diff --git a/packages/prompts/src/__snapshots__/index.test.ts.snap b/packages/prompts/src/__snapshots__/index.test.ts.snap index d38c1f8e..1850dfb8 100644 --- a/packages/prompts/src/__snapshots__/index.test.ts.snap +++ b/packages/prompts/src/__snapshots__/index.test.ts.snap @@ -380,6 +380,94 @@ exports[`prompts (isCI = false) > groupMultiselect > cursorAt sets initial selec ] `; +exports[`prompts (isCI = false) > groupMultiselect > groupSpacing > negative spacing is ignored 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ group1 +│ └ ◻ group1value0 +│ ◻ group2 +│ └ ◻ group2value0 +└ +", + "", + "", + "", + "│ ◻ group1 +│ └ ◻ group1value0 +│ ◻ group2 +│ └ ◻ group2value0 +└ +", + "", + "", + "", + "│ ◼ group1 +│ └ ◼ group1value0 +│ ◻ group2 +│ └ ◻ group2value0 +└ +", + "", + "", + "", + "◇ foo +│ group1value0", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = false) > groupMultiselect > groupSpacing > renders spaced groups 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ +│ +│ ◻ group1 +│ └ ◻ group1value0 +│ +│ +│ ◻ group2 +│ └ ◻ group2value0 +└ +", + "", + "", + "", + "│ ◻ group1 +│ └ ◻ group1value0 +│ +│ +│ ◻ group2 +│ └ ◻ group2value0 +└ +", + "", + "", + "", + "│ ◼ group1 +│ └ ◼ group1value0 +│ +│ +│ ◻ group2 +│ └ ◻ group2value0 +└ +", + "", + "", + "", + "◇ foo +│ group1value0", + " +", + "[?25h", +] +`; + exports[`prompts (isCI = false) > groupMultiselect > initial values can be set 1`] = ` [ "[?25l", @@ -2154,6 +2242,94 @@ exports[`prompts (isCI = true) > groupMultiselect > cursorAt sets initial select ] `; +exports[`prompts (isCI = true) > groupMultiselect > groupSpacing > negative spacing is ignored 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ ◻ group1 +│ └ ◻ group1value0 +│ ◻ group2 +│ └ ◻ group2value0 +└ +", + "", + "", + "", + "│ ◻ group1 +│ └ ◻ group1value0 +│ ◻ group2 +│ └ ◻ group2value0 +└ +", + "", + "", + "", + "│ ◼ group1 +│ └ ◼ group1value0 +│ ◻ group2 +│ └ ◻ group2value0 +└ +", + "", + "", + "", + "◇ foo +│ group1value0", + " +", + "[?25h", +] +`; + +exports[`prompts (isCI = true) > groupMultiselect > groupSpacing > renders spaced groups 1`] = ` +[ + "[?25l", + "│ +◆ foo +│ +│ +│ ◻ group1 +│ └ ◻ group1value0 +│ +│ +│ ◻ group2 +│ └ ◻ group2value0 +└ +", + "", + "", + "", + "│ ◻ group1 +│ └ ◻ group1value0 +│ +│ +│ ◻ group2 +│ └ ◻ group2value0 +└ +", + "", + "", + "", + "│ ◼ group1 +│ └ ◼ group1value0 +│ +│ +│ ◻ group2 +│ └ ◻ group2value0 +└ +", + "", + "", + "", + "◇ foo +│ group1value0", + " +", + "[?25h", +] +`; + exports[`prompts (isCI = true) > groupMultiselect > initial values can be set 1`] = ` [ "[?25l", diff --git a/packages/prompts/src/index.test.ts b/packages/prompts/src/index.test.ts index 216dfa7f..7d55856c 100644 --- a/packages/prompts/src/index.test.ts +++ b/packages/prompts/src/index.test.ts @@ -1204,5 +1204,49 @@ describe.each(['true', 'false'])('prompts (isCI = %s)', (isCI) => { expect(value).toEqual([value0]); expect(output.buffer).toMatchSnapshot(); }); + + describe('groupSpacing', () => { + test('renders spaced groups', async () => { + const result = prompts.groupMultiselect({ + message: 'foo', + input, + output, + options: { + group1: [{ value: 'group1value0' }], + group2: [{ value: 'group2value0' }], + }, + groupSpacing: 2, + }); + + input.emit('keypress', '', { name: 'down' }); + input.emit('keypress', '', { name: 'space' }); + input.emit('keypress', '', { name: 'return' }); + + await result; + + expect(output.buffer).toMatchSnapshot(); + }); + + test('negative spacing is ignored', async () => { + const result = prompts.groupMultiselect({ + message: 'foo', + input, + output, + options: { + group1: [{ value: 'group1value0' }], + group2: [{ value: 'group2value0' }], + }, + groupSpacing: -2, + }); + + input.emit('keypress', '', { name: 'down' }); + input.emit('keypress', '', { name: 'space' }); + input.emit('keypress', '', { name: 'return' }); + + await result; + + expect(output.buffer).toMatchSnapshot(); + }); + }); }); }); diff --git a/packages/prompts/src/index.ts b/packages/prompts/src/index.ts index 996a2794..1fa1c670 100644 --- a/packages/prompts/src/index.ts +++ b/packages/prompts/src/index.ts @@ -489,9 +489,10 @@ export interface GroupMultiSelectOptions extends CommonOptions { required?: boolean; cursorAt?: Value; selectableGroups?: boolean; + groupSpacing?: number; } export const groupMultiselect = (opts: GroupMultiSelectOptions) => { - const { selectableGroups = true } = opts; + const { selectableGroups = true, groupSpacing = 0 } = opts; const opt = ( option: Option, state: @@ -510,21 +511,23 @@ export const groupMultiselect = (opts: GroupMultiSelectOptions) => const next = isItem && (options[options.indexOf(option) + 1] ?? { group: true }); const isLast = isItem && (next as any).group === true; const prefix = isItem ? (selectableGroups ? `${isLast ? S_BAR_END : S_BAR} ` : ' ') : ''; + const spacingPrefix = + groupSpacing > 0 && !isItem ? `\n${color.cyan(S_BAR)} `.repeat(groupSpacing) : ''; if (state === 'active') { - return `${color.dim(prefix)}${color.cyan(S_CHECKBOX_ACTIVE)} ${label} ${ + return `${spacingPrefix}${color.dim(prefix)}${color.cyan(S_CHECKBOX_ACTIVE)} ${label} ${ option.hint ? color.dim(`(${option.hint})`) : '' }`; } if (state === 'group-active') { - return `${prefix}${color.cyan(S_CHECKBOX_ACTIVE)} ${color.dim(label)}`; + return `${spacingPrefix}${prefix}${color.cyan(S_CHECKBOX_ACTIVE)} ${color.dim(label)}`; } if (state === 'group-active-selected') { - return `${prefix}${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}`; + return `${spacingPrefix}${prefix}${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}`; } if (state === 'selected') { const selectedCheckbox = isItem || selectableGroups ? color.green(S_CHECKBOX_SELECTED) : ''; - return `${color.dim(prefix)}${selectedCheckbox} ${color.dim(label)} ${ + return `${spacingPrefix}${color.dim(prefix)}${selectedCheckbox} ${color.dim(label)} ${ option.hint ? color.dim(`(${option.hint})`) : '' }`; } @@ -532,7 +535,7 @@ export const groupMultiselect = (opts: GroupMultiSelectOptions) => return `${color.strikethrough(color.dim(label))}`; } if (state === 'active-selected') { - return `${color.dim(prefix)}${color.green(S_CHECKBOX_SELECTED)} ${label} ${ + return `${spacingPrefix}${color.dim(prefix)}${color.green(S_CHECKBOX_SELECTED)} ${label} ${ option.hint ? color.dim(`(${option.hint})`) : '' }`; } @@ -540,7 +543,7 @@ export const groupMultiselect = (opts: GroupMultiSelectOptions) => return `${color.dim(label)}`; } const unselectedCheckbox = isItem || selectableGroups ? color.dim(S_CHECKBOX_INACTIVE) : ''; - return `${color.dim(prefix)}${unselectedCheckbox} ${color.dim(label)}`; + return `${spacingPrefix}${color.dim(prefix)}${unselectedCheckbox} ${color.dim(label)}`; }; return new GroupMultiSelectPrompt({