From 0ff3ca88287421686f2fa3afa992c26f0756a710 Mon Sep 17 00:00:00 2001 From: Owen McGirr Date: Mon, 28 Sep 2026 09:53:55 +0100 Subject: [PATCH 1/2] Add after-selection options to scanning settings After a selection and Start from appear under More options, shared and for each scanner. A scanner shows what it will do, so its usual behaviour stands in where nothing has been chosen. They replace the keyboard's After typing, whose saved value remains the keyboard's usual behaviour. Closes #933 Co-Authored-By: Claude Fable 5.1 --- src/PointScan.test.tsx | 42 ++++++++++++++++++++++++++--- src/scanning/preferences.ts | 16 +++++++++-- src/settings/ScannerPreferences.tsx | 22 +++++++++++---- 3 files changed, 69 insertions(+), 11 deletions(-) diff --git a/src/PointScan.test.tsx b/src/PointScan.test.tsx index 57daaac6..0ad3990d 100644 --- a/src/PointScan.test.tsx +++ b/src/PointScan.test.tsx @@ -11,6 +11,7 @@ import { defaultPointScanConfig, type PointScanState, } from "./scanning/useScanning"; +import { defaultScanPreferences } from "./scanning/preferences"; import { ScanningSection } from "./settings/ScanningSection"; function more() { const button = screen.getByRole("button", { name: "More options" }); @@ -391,16 +392,49 @@ it("saves the shared panel pointer-avoidance choice from the mouse and keyboard expect(screen.getByRole("checkbox", { name: "Move away from the pointer" })).toBeChecked(); }); -it("saves the keyboard after-typing choice and preserves it while manual", async () => { +it("shows what each scanner does after a selection and saves a choice for one", async () => { mocks.invoke.mockImplementation(async (command, args) => command === "get_point_scan" ? initial : { ...initial, config: args.config }); render(); await screen.findByText(initial.message); - expect(screen.queryByText("After typing")).not.toBeInTheDocument(); + const pressed = (name: string) => screen.getByRole("button", { name }).getAttribute("aria-pressed") === "true"; + more(); + expect(screen.getAllByRole("button", { name: "Usual for each scanner" }).map(button => button.getAttribute("aria-pressed"))).toEqual(["true", "true"]); + fireEvent.click(screen.getByRole("button", { name: "Customise menus" })); + more(); + expect(screen.queryByRole("button", { name: "Usual for each scanner" })).not.toBeInTheDocument(); + expect([pressed("Start scanning"), pressed("Where I selected")]).toEqual([true, true]); + fireEvent.click(screen.getByRole("button", { name: /Back to scanning settings/ })); + fireEvent.click(screen.getByRole("button", { name: "Customise point scanning" })); + more(); + expect(pressed("Wait for Select")).toBe(true); + expect(screen.queryByText("Start from")).not.toBeInTheDocument(); + fireEvent.click(screen.getByRole("button", { name: "Start scanning" })); + await waitFor(() => expect(mocks.invoke).toHaveBeenLastCalledWith("configure_point_scan", { config: { ...defaultPointScanConfig, scanPreferences: { ...defaultScanPreferences, point: { nextScan: "automatic" } } } })); + fireEvent.click(screen.getByRole("button", { name: "Use default for after a selection" })); + await waitFor(() => expect(pressed("Wait for Select")).toBe(true)); + fireEvent.click(screen.getByRole("button", { name: /Back to scanning settings/ })); + fireEvent.click(screen.getByRole("button", { name: "Customise mouse scanning" })); + more(); + expect([pressed("Start scanning"), pressed("The beginning")]).toEqual([true, true]); + fireEvent.click(screen.getByRole("button", { name: "Where I selected" })); + await waitFor(() => expect(mocks.invoke.mock.lastCall?.[1].config.scanPreferences.mouse).toEqual({ startFrom: "selection" })); +}); + +it("keeps a saved wait after typing as the keyboard's usual behaviour", async () => { + const saved = { ...initial, config: { ...initial.config, keyboardWaitAfterTyping: true } }; + mocks.invoke.mockImplementation(async (command, args) => command === "get_point_scan" ? saved : { ...saved, config: args.config }); + render(); + await screen.findByText(initial.message); more(); fireEvent.click(screen.getByRole("button", { name: "Customise keyboard" })); - expect(screen.getByRole("button", { name: "Continue scanning" })).toHaveAttribute("aria-pressed", "true"); + more(); + expect(screen.queryByText("After typing")).not.toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Wait for Select" })).toHaveAttribute("aria-pressed", "true"); + fireEvent.click(screen.getByRole("button", { name: "Start scanning" })); + await waitFor(() => expect(mocks.invoke.mock.lastCall?.[1].config.scanPreferences.keyboard).toEqual({ nextScan: "automatic" })); + expect(screen.getByRole("button", { name: "Start scanning" })).toHaveAttribute("aria-pressed", "true"); fireEvent.click(screen.getByRole("button", { name: "Wait for Select" })); - await waitFor(() => expect(mocks.invoke).toHaveBeenLastCalledWith("configure_point_scan", { config: { ...defaultPointScanConfig, keyboardWaitAfterTyping: true } })); + await waitFor(() => expect(mocks.invoke.mock.lastCall?.[1].config.scanPreferences.keyboard).toEqual({ nextScan: "wait" })); fireEvent.click(screen.getByRole("checkbox", { name: "Automatic scanning" })); await waitFor(() => expect(screen.getByRole("button", { name: "Wait for Select" })).toBeDisabled()); expect(screen.getByRole("button", { name: "Wait for Select" })).toHaveAttribute("aria-pressed", "true"); diff --git a/src/scanning/preferences.ts b/src/scanning/preferences.ts index 5212a596..f990d8b0 100644 --- a/src/scanning/preferences.ts +++ b/src/scanning/preferences.ts @@ -8,16 +8,28 @@ export type ScanOptions = { pattern: 'grouped' | 'linear'; color: ScannerColor; thickness: 'thin' | 'standard' | 'thick'; + /** `standard` is what each scanner has always done. */ + nextScan: 'standard' | 'automatic' | 'wait'; + startFrom: 'standard' | 'beginning' | 'selection'; }; -export type ScanPreferences = Pick & Record>; +export type ScanPreferences = Pick + & Partial> & Record>; export const defaultScanPreferences: ScanPreferences = { direction: 'forward', passLimit: 3, pattern: 'grouped', thickness: 'standard', point: {}, menu: {}, keyboard: {}, mouse: {}, }; +/** What a scanner does after a selection when nothing has been chosen for it. */ +export function usualAfterSelection(config: PointScanConfig, area: ScanArea): Pick { + return { + nextScan: area === 'point' || (area === 'keyboard' && config.keyboardWaitAfterTyping) ? 'wait' : 'automatic', + startFrom: area === 'menu' ? 'selection' : 'beginning', + }; +} export function sharedOptions(config: PointScanConfig): ScanOptions { const settings = config.scanPreferences ?? defaultScanPreferences; return { automatic: config.automatic, intervalMs: config.blockIntervalMs, color: config.scannerColor, - direction: settings.direction, passLimit: settings.passLimit, pattern: settings.pattern, thickness: settings.thickness }; + direction: settings.direction, passLimit: settings.passLimit, pattern: settings.pattern, thickness: settings.thickness, + nextScan: settings.nextScan ?? 'standard', startFrom: settings.startFrom ?? 'standard' }; } export function areaOptions(config: PointScanConfig, area: ScanArea): ScanOptions { const shared = sharedOptions(config); diff --git a/src/settings/ScannerPreferences.tsx b/src/settings/ScannerPreferences.tsx index 671fa77b..f393c2c6 100644 --- a/src/settings/ScannerPreferences.tsx +++ b/src/settings/ScannerPreferences.tsx @@ -2,7 +2,7 @@ import { Button, Input, Select, MoreOptions } from "../ui/controls"; import { useLayoutEffect, useRef, useState, type ReactNode } from 'react'; import { ChevronRight } from 'lucide-react'; import type { ScanningController, ScannerColor, PointScanConfig } from '../scanning/useScanning'; -import { areaOptions, sharedOptions, defaultScanPreferences, type ScanArea, type ScanOptions } from '../scanning/preferences'; +import { areaOptions, sharedOptions, defaultScanPreferences, usualAfterSelection, type ScanArea, type ScanOptions } from '../scanning/preferences'; import { SettingGroup, Toggle, OptionGroup } from './controls'; export function ScannerPreferences({ controller }: { controller: ScanningController }) { @@ -45,6 +45,11 @@ export function ScannerPreferences({ controller }: { controller: ScanningControl const settings = config.scanPreferences ?? defaultScanPreferences; const shared = sharedOptions(config); const effective = area === 'shared' ? shared : areaOptions(config, area); + // A scanner shows what it will do, so its usual behaviour stands in for "standard". + const usual = area === 'shared' ? null : usualAfterSelection(config, area); + const nextScan = effective.nextScan === 'standard' && usual ? usual.nextScan : effective.nextScan; + const startFrom = effective.startFrom === 'standard' && usual ? usual.startFrom : effective.startFrom; + const usualOption = area === 'shared' ? [{ value: 'standard' as const, label: 'Usual for each scanner' }] : []; const change = (key: K, value: ScanOptions[K] | undefined) => { if (area !== 'shared') { const overrides = { ...(settings[area] ?? {}), [key]: value }; @@ -141,10 +146,6 @@ export function ScannerPreferences({ controller }: { controller: ScanningControl } {field('automatic', 'Automatic scanning', locked => change('automatic', value)} />)} - {area === 'keyboard' && legend="After typing" disabled={disabled || !effective.automatic} value={config.keyboardWaitAfterTyping ? 'wait' : 'continue'} - options={[{ value: 'continue', label: 'Continue scanning' }, { value: 'wait', label: 'Wait for Select' }]} - onChange={value => update('keyboardWaitAfterTyping', value === 'wait')} - note={{ summary: effective.automatic ? 'After typing a key or suggestion, wait for Select before scanning again.' : 'Used in automatic keyboard scanning. Your choice is kept while scanning manually.' }} />} {field('intervalMs', 'Auto scan rate', locked => <>
Auto scan rate
@@ -155,6 +156,17 @@ export function ScannerPreferences({ controller }: { controller: ScanningControl )} + {field('nextScan', 'After a selection', locked => legend="After a selection" columns={area === 'shared' ? 'three' : undefined} disabled={locked || (area !== 'point' && !effective.automatic)} value={nextScan} + options={[...usualOption, { value: 'automatic', label: 'Start scanning' }, { value: 'wait', label: 'Wait for Select' }]} onChange={value => change('nextScan', value)} + note={{ summary: area === 'point' ? 'After a click, the next point scan starts by itself or waits for Select. The pass limit still stops it.' + : area !== 'shared' && !effective.automatic ? 'Used in automatic scanning. Your choice is kept while scanning manually.' + : area === 'shared' ? 'After a selection that does something, such as typing a key or clicking, scanning starts again by itself or waits for Select. Usually, scanning waits after a click and starts again after anything else.' + : 'After a selection that does something, such as typing a key or scrolling, scanning starts again by itself or waits for Select.' }} />)} + {area !== 'point' && field('startFrom', 'Start from', locked => legend="Start from" columns={area === 'shared' ? 'three' : undefined} disabled={locked} value={startFrom} + options={[...usualOption, { value: 'beginning', label: 'The beginning' }, { value: 'selection', label: 'Where I selected' }]} onChange={value => change('startFrom', value)} + note={{ summary: area === 'shared' ? 'Where scanning starts after a selection that does something. Usually, menus stay where you selected, and the keyboard and mouse panel return to the beginning. Point scanning always starts from the beginning.' + : area === 'keyboard' ? 'Where scanning starts after typing a key. After a suggestion, or when changing page, it starts from the beginning.' + : 'Where scanning starts after a selection that does something. Opening another page or menu starts from its beginning.' }} />)} {field('direction', 'Initial direction', locked => legend="Initial direction" disabled={locked} value={effective.direction} options={[{ value: 'forward', label: 'Forward' }, { value: 'reverse', label: 'Reverse' }]} onChange={value => change('direction', value)} />)} {field('passLimit', 'Pass limit', locked => legend="Pass limit" disabled={locked} value={effective.passLimit} options={[1, 2, 3, 5, 0].map(value => ({ value, label: value ? `${value} ${value === 1 ? 'pass' : 'passes'}` : 'Unlimited' }))} onChange={value => change('passLimit', value)} note={{ summary: 'After this many automatic passes, scanning waits for Select. Unlimited keeps scanning until you pause or stop.' }} />)} From 6d967ec4be8e14c8704da41aab5855f646c6a705 Mon Sep 17 00:00:00 2001 From: Owen McGirr Date: Mon, 28 Sep 2026 09:56:42 +0100 Subject: [PATCH 2/2] Show a saved wait after typing for what it is Review follow-up: the shared After a selection stays available while shared automatic scanning is off, since point scanning and scanners with their own setting use it. The notes say when the keyboard waits because of a setting saved earlier. Labels read Keep scanning and Start again from. Co-Authored-By: Claude Fable 5.1 --- src/PointScan.test.tsx | 53 +++++++++++++++++++++++++---- src/scanning/preferences.ts | 7 +++- src/settings/ScannerPreferences.tsx | 14 ++++---- 3 files changed, 59 insertions(+), 15 deletions(-) diff --git a/src/PointScan.test.tsx b/src/PointScan.test.tsx index 0ad3990d..b0a68ff7 100644 --- a/src/PointScan.test.tsx +++ b/src/PointScan.test.tsx @@ -402,24 +402,62 @@ it("shows what each scanner does after a selection and saves a choice for one", fireEvent.click(screen.getByRole("button", { name: "Customise menus" })); more(); expect(screen.queryByRole("button", { name: "Usual for each scanner" })).not.toBeInTheDocument(); - expect([pressed("Start scanning"), pressed("Where I selected")]).toEqual([true, true]); + expect([pressed("Keep scanning"), pressed("Where I selected")]).toEqual([true, true]); fireEvent.click(screen.getByRole("button", { name: /Back to scanning settings/ })); fireEvent.click(screen.getByRole("button", { name: "Customise point scanning" })); more(); expect(pressed("Wait for Select")).toBe(true); - expect(screen.queryByText("Start from")).not.toBeInTheDocument(); - fireEvent.click(screen.getByRole("button", { name: "Start scanning" })); + expect(screen.queryByText("Start again from")).not.toBeInTheDocument(); + fireEvent.click(screen.getByRole("button", { name: "Keep scanning" })); await waitFor(() => expect(mocks.invoke).toHaveBeenLastCalledWith("configure_point_scan", { config: { ...defaultPointScanConfig, scanPreferences: { ...defaultScanPreferences, point: { nextScan: "automatic" } } } })); fireEvent.click(screen.getByRole("button", { name: "Use default for after a selection" })); await waitFor(() => expect(pressed("Wait for Select")).toBe(true)); fireEvent.click(screen.getByRole("button", { name: /Back to scanning settings/ })); fireEvent.click(screen.getByRole("button", { name: "Customise mouse scanning" })); more(); - expect([pressed("Start scanning"), pressed("The beginning")]).toEqual([true, true]); + expect([pressed("Keep scanning"), pressed("The beginning")]).toEqual([true, true]); fireEvent.click(screen.getByRole("button", { name: "Where I selected" })); await waitFor(() => expect(mocks.invoke.mock.lastCall?.[1].config.scanPreferences.mouse).toEqual({ startFrom: "selection" })); }); +it("saves a choice for every scanner and shows it in each", async () => { + mocks.invoke.mockImplementation(async (command, args) => command === "get_point_scan" + ? { ...initial, config: { ...initial.config, automatic: false, keyboardWaitAfterTyping: true } } : { ...initial, config: args.config }); + render(); + await screen.findByText(initial.message); + more(); + // Point scanning and scanners with their own setting still use it. + expect(screen.getByRole("button", { name: "Keep scanning" })).toBeEnabled(); + expect(screen.getByText(/The keyboard waits, as saved earlier/)).toBeInTheDocument(); + fireEvent.click(screen.getByRole("button", { name: "Keep scanning" })); + await waitFor(() => expect(mocks.invoke.mock.lastCall?.[1].config.scanPreferences.nextScan).toBe("automatic")); + expect(screen.queryByText(/The keyboard waits, as saved earlier/)).not.toBeInTheDocument(); + fireEvent.click(screen.getByRole("button", { name: "Where I selected" })); + await waitFor(() => expect(mocks.invoke.mock.lastCall?.[1].config.scanPreferences.startFrom).toBe("selection")); + fireEvent.click(screen.getByRole("button", { name: "Customise keyboard" })); + more(); + expect(screen.getByRole("button", { name: "Keep scanning" })).toHaveAttribute("aria-pressed", "true"); + expect(screen.getByRole("button", { name: "Keep scanning" })).toBeDisabled(); + expect(screen.getByRole("button", { name: "Where I selected" })).toHaveAttribute("aria-pressed", "true"); + expect(screen.getByRole("button", { name: "Where I selected" })).toBeEnabled(); + fireEvent.click(screen.getByRole("button", { name: "The beginning" })); + await waitFor(() => expect(mocks.invoke.mock.lastCall?.[1].config.scanPreferences.keyboard).toEqual({ startFrom: "beginning" })); + fireEvent.click(screen.getByRole("button", { name: "Reset scanning overrides" })); + await waitFor(() => expect(mocks.invoke.mock.lastCall?.[1].config.scanPreferences.keyboard).toEqual({})); + expect(screen.getByRole("button", { name: "Where I selected" })).toHaveAttribute("aria-pressed", "true"); +}); + +it("shows a keyboard set back to standard as it will behave", async () => { + const saved = { ...initial, config: { ...initial.config, keyboardWaitAfterTyping: true, scanPreferences: { ...defaultScanPreferences, keyboard: { nextScan: "standard" as const } } } }; + mocks.invoke.mockImplementation(async (command, args) => command === "get_point_scan" ? saved : { ...saved, config: args.config }); + render(); + await screen.findByText(initial.message); + more(); + fireEvent.click(screen.getByRole("button", { name: "Customise keyboard" })); + more(); + expect(screen.getByRole("button", { name: "Keep scanning" })).toHaveAttribute("aria-pressed", "true"); +}); + it("keeps a saved wait after typing as the keyboard's usual behaviour", async () => { const saved = { ...initial, config: { ...initial.config, keyboardWaitAfterTyping: true } }; mocks.invoke.mockImplementation(async (command, args) => command === "get_point_scan" ? saved : { ...saved, config: args.config }); @@ -428,11 +466,12 @@ it("keeps a saved wait after typing as the keyboard's usual behaviour", async () more(); fireEvent.click(screen.getByRole("button", { name: "Customise keyboard" })); more(); - expect(screen.queryByText("After typing")).not.toBeInTheDocument(); + expect(screen.queryByRole("group", { name: "After typing" })).not.toBeInTheDocument(); + expect(screen.getByText(/saved earlier as After typing/)).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Wait for Select" })).toHaveAttribute("aria-pressed", "true"); - fireEvent.click(screen.getByRole("button", { name: "Start scanning" })); + fireEvent.click(screen.getByRole("button", { name: "Keep scanning" })); await waitFor(() => expect(mocks.invoke.mock.lastCall?.[1].config.scanPreferences.keyboard).toEqual({ nextScan: "automatic" })); - expect(screen.getByRole("button", { name: "Start scanning" })).toHaveAttribute("aria-pressed", "true"); + expect(screen.getByRole("button", { name: "Keep scanning" })).toHaveAttribute("aria-pressed", "true"); fireEvent.click(screen.getByRole("button", { name: "Wait for Select" })); await waitFor(() => expect(mocks.invoke.mock.lastCall?.[1].config.scanPreferences.keyboard).toEqual({ nextScan: "wait" })); fireEvent.click(screen.getByRole("checkbox", { name: "Automatic scanning" })); diff --git a/src/scanning/preferences.ts b/src/scanning/preferences.ts index f990d8b0..c1bcd958 100644 --- a/src/scanning/preferences.ts +++ b/src/scanning/preferences.ts @@ -18,10 +18,15 @@ export const defaultScanPreferences: ScanPreferences = { direction: 'forward', passLimit: 3, pattern: 'grouped', thickness: 'standard', point: {}, menu: {}, keyboard: {}, mouse: {}, }; +/** Wait after typing, saved before the choice existed for every scanner, is still in effect. */ +export function savedKeyboardWait(config: PointScanConfig) { + const settings = config.scanPreferences ?? defaultScanPreferences; + return config.keyboardWaitAfterTyping && settings.keyboard?.nextScan == null && (settings.nextScan ?? 'standard') === 'standard'; +} /** What a scanner does after a selection when nothing has been chosen for it. */ export function usualAfterSelection(config: PointScanConfig, area: ScanArea): Pick { return { - nextScan: area === 'point' || (area === 'keyboard' && config.keyboardWaitAfterTyping) ? 'wait' : 'automatic', + nextScan: area === 'point' || (area === 'keyboard' && savedKeyboardWait(config)) ? 'wait' : 'automatic', startFrom: area === 'menu' ? 'selection' : 'beginning', }; } diff --git a/src/settings/ScannerPreferences.tsx b/src/settings/ScannerPreferences.tsx index f393c2c6..91bf5372 100644 --- a/src/settings/ScannerPreferences.tsx +++ b/src/settings/ScannerPreferences.tsx @@ -2,7 +2,7 @@ import { Button, Input, Select, MoreOptions } from "../ui/controls"; import { useLayoutEffect, useRef, useState, type ReactNode } from 'react'; import { ChevronRight } from 'lucide-react'; import type { ScanningController, ScannerColor, PointScanConfig } from '../scanning/useScanning'; -import { areaOptions, sharedOptions, defaultScanPreferences, usualAfterSelection, type ScanArea, type ScanOptions } from '../scanning/preferences'; +import { areaOptions, sharedOptions, defaultScanPreferences, usualAfterSelection, savedKeyboardWait, type ScanArea, type ScanOptions } from '../scanning/preferences'; import { SettingGroup, Toggle, OptionGroup } from './controls'; export function ScannerPreferences({ controller }: { controller: ScanningController }) { @@ -156,13 +156,13 @@ export function ScannerPreferences({ controller }: { controller: ScanningControl )} - {field('nextScan', 'After a selection', locked => legend="After a selection" columns={area === 'shared' ? 'three' : undefined} disabled={locked || (area !== 'point' && !effective.automatic)} value={nextScan} - options={[...usualOption, { value: 'automatic', label: 'Start scanning' }, { value: 'wait', label: 'Wait for Select' }]} onChange={value => change('nextScan', value)} - note={{ summary: area === 'point' ? 'After a click, the next point scan starts by itself or waits for Select. The pass limit still stops it.' + {field('nextScan', 'After a selection', locked => legend="After a selection" columns={area === 'shared' ? 'three' : undefined} disabled={locked || (area !== 'shared' && area !== 'point' && !effective.automatic)} value={nextScan} + options={[...usualOption, { value: 'automatic', label: 'Keep scanning' }, { value: 'wait', label: 'Wait for Select' }]} onChange={value => change('nextScan', value)} + note={{ summary: area === 'point' ? 'After a click, the next point scan starts by itself or waits for Select. Scanning still stops at the pass limit.' : area !== 'shared' && !effective.automatic ? 'Used in automatic scanning. Your choice is kept while scanning manually.' - : area === 'shared' ? 'After a selection that does something, such as typing a key or clicking, scanning starts again by itself or waits for Select. Usually, scanning waits after a click and starts again after anything else.' - : 'After a selection that does something, such as typing a key or scrolling, scanning starts again by itself or waits for Select.' }} />)} - {area !== 'point' && field('startFrom', 'Start from', locked => legend="Start from" columns={area === 'shared' ? 'three' : undefined} disabled={locked} value={startFrom} + : area === 'shared' ? 'After a selection that does something, such as typing a key or clicking, scanning starts again by itself or waits for Select. Usually, scanning waits after a click and starts again after anything else.' + (savedKeyboardWait(config) ? ' The keyboard waits, as saved earlier.' : '') + ' Waiting applies to scanners that scan automatically.' + : 'After a selection that does something, such as typing a key or scrolling, scanning starts again by itself or waits for Select.' + (area === 'keyboard' && savedKeyboardWait(config) ? ' Waiting is the default here because it was saved earlier as After typing.' : '') }} />)} + {area !== 'point' && field('startFrom', 'Start again from', locked => legend="Start again from" columns={area === 'shared' ? 'three' : undefined} disabled={locked} value={startFrom} options={[...usualOption, { value: 'beginning', label: 'The beginning' }, { value: 'selection', label: 'Where I selected' }]} onChange={value => change('startFrom', value)} note={{ summary: area === 'shared' ? 'Where scanning starts after a selection that does something. Usually, menus stay where you selected, and the keyboard and mouse panel return to the beginning. Point scanning always starts from the beginning.' : area === 'keyboard' ? 'Where scanning starts after typing a key. After a suggestion, or when changing page, it starts from the beginning.'