diff --git a/.changeset/tall-doors-smile.md b/.changeset/tall-doors-smile.md new file mode 100644 index 00000000..f0f014a5 --- /dev/null +++ b/.changeset/tall-doors-smile.md @@ -0,0 +1,5 @@ +--- +'@clack/core': minor +--- + +Add `isAccessible()` and an `accessible` setting as the foundation for accessible mode. Resolution order: per-call option > `updateSettings({ accessible })` > the `ACCESSIBLE` environment variable (any non-empty value enables it, except `0` and `false`). diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 4383a3df..be3c9fdb 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -23,6 +23,6 @@ export { default as TextPrompt } from './prompts/text.js'; export type { ClackState as State } from './types.js'; export { block, getColumns, getRows, isCancel, wrapTextWithPrefix } from './utils/index.js'; export type { ClackSettings } from './utils/settings.js'; -export { settings, updateSettings } from './utils/settings.js'; +export { isAccessible, settings, updateSettings } from './utils/settings.js'; export type { Validate } from './utils/validation.js'; export { runValidation } from './utils/validation.js'; diff --git a/packages/core/src/utils/settings.ts b/packages/core/src/utils/settings.ts index 7b35959e..b3d008cf 100644 --- a/packages/core/src/utils/settings.ts +++ b/packages/core/src/utils/settings.ts @@ -25,6 +25,7 @@ interface InternalClackSettings { error: string; }; withGuide: boolean; + accessible: boolean | undefined; date: { monthNames: string[]; messages: { @@ -54,6 +55,7 @@ export const settings: InternalClackSettings = { error: 'Something went wrong', }, withGuide: true, + accessible: undefined, date: { monthNames: [...DEFAULT_MONTH_NAMES], messages: { @@ -94,6 +96,12 @@ export interface ClackSettings { withGuide?: boolean; + /** + * Force accessible (static, screen-reader friendly) output on or off. + * Overrides the `ACCESSIBLE` environment variable. + */ + accessible?: boolean; + /** * Date prompt localization */ @@ -145,6 +153,10 @@ export function updateSettings(updates: ClackSettings) { settings.withGuide = updates.withGuide !== false; } + if (updates.accessible !== undefined) { + settings.accessible = updates.accessible === true; + } + if (updates.date !== undefined) { const date = updates.date; if (date.monthNames !== undefined) { @@ -170,6 +182,21 @@ export function updateSettings(updates: ClackSettings) { } } +/** + * Resolves whether accessible (static, screen-reader friendly) output is enabled. + * + * Resolution order: per-call option > global `accessible` setting > `ACCESSIBLE` + * environment variable (any non-empty value enables it, except `0` and `false`). + * + * @param accessible - Per-call override; takes precedence when defined + */ +export function isAccessible(accessible?: boolean): boolean { + if (accessible !== undefined) return accessible; + if (settings.accessible !== undefined) return settings.accessible; + const value = process.env.ACCESSIBLE; + return value !== undefined && value !== '' && value !== '0' && value !== 'false'; +} + /** * Check if a key is an alias for a default action * @param key - The raw key which might match to an action diff --git a/packages/core/test/utils/settings.test.ts b/packages/core/test/utils/settings.test.ts new file mode 100644 index 00000000..bd427d72 --- /dev/null +++ b/packages/core/test/utils/settings.test.ts @@ -0,0 +1,63 @@ +import { afterEach, describe, expect, test } from 'vitest'; +import { isAccessible, settings, updateSettings } from '../../src/utils/settings.js'; + +describe('isAccessible', () => { + const originalEnv = process.env.ACCESSIBLE; + + afterEach(() => { + if (originalEnv === undefined) { + delete process.env.ACCESSIBLE; + } else { + process.env.ACCESSIBLE = originalEnv; + } + settings.accessible = undefined; + }); + + describe('ACCESSIBLE env var', () => { + test('disabled when unset', () => { + delete process.env.ACCESSIBLE; + expect(isAccessible()).toBe(false); + }); + + test.each(['', '0', 'false'])('disabled when set to %j', (value) => { + process.env.ACCESSIBLE = value; + expect(isAccessible()).toBe(false); + }); + + test.each(['1', 'true', 'yes'])('enabled when set to %j', (value) => { + process.env.ACCESSIBLE = value; + expect(isAccessible()).toBe(true); + }); + }); + + describe('global setting', () => { + test('updateSettings({ accessible: true }) enables it', () => { + delete process.env.ACCESSIBLE; + updateSettings({ accessible: true }); + expect(isAccessible()).toBe(true); + }); + + test('updateSettings({ accessible: false }) overrides the env var', () => { + process.env.ACCESSIBLE = '1'; + updateSettings({ accessible: false }); + expect(isAccessible()).toBe(false); + }); + }); + + describe('per-call option', () => { + test('true overrides a false global setting', () => { + updateSettings({ accessible: false }); + expect(isAccessible(true)).toBe(true); + }); + + test('false overrides the env var', () => { + process.env.ACCESSIBLE = '1'; + expect(isAccessible(false)).toBe(false); + }); + + test('undefined falls through to env var', () => { + process.env.ACCESSIBLE = '1'; + expect(isAccessible(undefined)).toBe(true); + }); + }); +});