/**
* Questionnaire — one question at a time, with progress, validation and a way
* back.
*
* Unlike `Steps`, which reflects a flow the app owns, Questionnaire *owns* the
* flow: it holds the answers, decides which question is current, gates the way
* forward on the current one being answered, and reports the whole set back
* when it is done. The caller supplies the questions and does something with
* the answers; everything between those two points belongs here.
*
* ```tsx
* save(answers)}>
* Project setup
*
*
* What should we build next?
* Choose one, or write your own.
*
*
*
*
*
*
*
*
*
*
*
*
*
* ```
*
* ## Why it draws its own Frame
*
* A survey is a widget, not a paragraph: it wants a boundary, a title strip and
* a footer that stays put while the middle changes. That is exactly `Frame`, so
* the root renders one rather than leaving every caller to assemble the same
* shell. `Frame.Panel`'s `overflow-hidden` also does the clipping the sliding
* question needs, for free. Pass `frame={false}` to drop it — for a
* questionnaire inside a `BottomSheet` or a card that already draws a border.
*
* It is the `inset` variant, so the panel the question is written on floats in
* a recessed band rather than sitting flush in a tray, and the actions go in
* the band rather than in a section under the question. That separation is the
* point: the question changes and the row under it does not, and a row drawn
* on the band is visibly not part of the card that keeps being replaced.
*
* The band shapes its actions into equal pills, which is why
* `Questionnaire.Spacer` is dropped on the way in — see `bandActions`.
*
* ## Why the root reads its children instead of collecting registrations
*
* Only the active question is mounted, so an unmounted one cannot report that
* it exists — and without knowing the full set there is no total to count
* against, no "is this the last one", and no way to disable a question the
* user has not reached. So the root inspects its children once per render and
* reads `name`, `required`, `multiple` and `disabled` straight off the
* elements. React elements carry their props before anything renders them,
* which makes the whole set knowable without mounting any of it.
*
* That same pass sorts the parts into the shell: the title and progress go to
* the header strip above the panel, the footer's actions to the band around
* it, and everything else is a question.
*
* ## Answers are one record, the way a form would submit them
*
* `answers[name]` is a string for a single-answer question and an array for a
* `multiple` one. A freeform answer lands under the same name — it is another
* answer to the same question, not a separate field — which is why the text
* input shows whatever value does not match one of the question's own choices.
* Picking a choice and typing therefore replace each other, without either one
* having to know the other exists.
*
* ## What blocks the way forward
*
* A required question blocks until it has an answer. An optional one never
* blocks. `Questionnaire.Skip` does not unblock anything, then — it *records*
* that the question was deliberately left out, moving its status from
* `unanswered` to `skipped` so the app can tell the two apart. Making an
* optional question demand an explicit skip would trap anyone who did not
* render the Skip button, and a question that cannot be ignored is not
* optional.
*/
import { type ReactNode } from 'react';
import { View, type TextInput, type ViewProps } from 'react-native';
import { type ButtonProps } from '../button/index.js';
import { type InputProps } from '../input/index.js';
/** Where a question stands: never touched, answered, or deliberately left out. */
export type QuestionnaireItemStatus = 'unanswered' | 'answered' | 'skipped';
/** Which key each answer is badged with. */
export type QuestionnaireShortcutMode = 'letters' | 'numbers';
/** Every answer given so far, keyed by question name. */
export type QuestionnaireAnswers = Record;
/**
* A question, described rather than rendered. Pass these as `items` so the
* questionnaire knows its full set before any of it mounts — which is what
* makes a conditional question countable and a total meaningful.
*/
export interface QuestionnaireItemDefinition {
/** Unique name — the key this question's answer is stored under. */
name: string;
/** Blocks the way forward until it has an answer. */
required?: boolean;
/** Accepts more than one answer, so its answer is an array. */
multiple?: boolean;
/** Left out of the count and never navigated to. */
disabled?: boolean;
}
export interface QuestionnaireProps extends Omit {
className?: string;
/**
* The full set of questions, in order. Optional: without it the order and
* the totals come from the `Questionnaire.Item` children instead. Pass it
* when a question is conditional, since a question the user has not reached
* still has to be counted — or not counted, if it no longer applies.
*/
items?: readonly QuestionnaireItemDefinition[];
/** Controlled active question, by name. */
item?: string;
/** Which question to open on. Defaults to the first enabled one. */
defaultItem?: string;
/** Called with the name of the question being moved to. */
onItemChange?: (name: string) => void;
/** Controlled answers. */
answers?: QuestionnaireAnswers;
/** Answers to start with — for resuming a part-finished questionnaire. */
defaultAnswers?: QuestionnaireAnswers;
/** Called with the whole set every time any answer changes. */
onAnswersChange?: (answers: QuestionnaireAnswers) => void;
/** Called with every answer once the last question validates. */
onSubmit?: (answers: QuestionnaireAnswers) => void;
/**
* Badge every answer with a letter (`A`, `B`, `C`) or a number (`1`, `2`,
* `3`). Disabled answers are skipped rather than taking a badge with them.
*
* The badge is an affordance, not a binding: React Native surfaces hardware
* key events only to a focused text field, so nothing here can listen for
* the key itself.
*/
shortcuts?: QuestionnaireShortcutMode;
/**
* Let a horizontal drag move between questions. Going forward is gated on
* the same answer the button is, so a swipe off an unanswered required
* question springs back and shows its error.
*/
swipeable?: boolean;
/**
* Draw the surrounding `Frame`. Turn it off to place the questionnaire in a
* sheet, a dialog or a card that already draws its own boundary.
*/
frame?: boolean;
children?: ReactNode;
}
declare function QuestionnaireRoot({ className, items, item: itemProp, defaultItem, onItemChange, answers: answersProp, defaultAnswers, onAnswersChange, onSubmit, shortcuts, swipeable, frame, children, ...props }: QuestionnaireProps): import("react").JSX.Element;
declare namespace QuestionnaireRoot {
var displayName: string;
}
export interface QuestionnaireTitleProps extends ViewProps {
className?: string;
children?: ReactNode;
}
/**
* Names the questionnaire as a whole, in the frame's header strip. The
* current question's own prompt is `Questionnaire.Question`.
*/
declare function QuestionnaireTitle({ className, children, ...props }: QuestionnaireTitleProps): import("react").JSX.Element;
declare namespace QuestionnaireTitle {
var displayName: string;
}
/** What a custom progress indicator is told about where the reader is. */
export interface QuestionnaireProgressState {
/** One-based position of the active question. */
current: number;
/** How many questions are enabled. */
total: number;
first: boolean;
last: boolean;
}
/** How the position is drawn. */
export type QuestionnaireProgressVariant = 'ring' | 'pips' | 'numbers' | 'count';
export interface QuestionnaireProgressProps {
className?: string;
/**
* `ring` is an arc that sweeps round as the reader advances — how far
* through the set they are, without saying how many questions there are.
* It is the only one that holds its size and its meaning at any length,
* which is why it is the default.
*
* `pips` is a bar per question, filled up to the one being asked and widened
* on it. `numbers` counts them out instead, which is what you want when the
* reader will be sent back to a particular question. `count` is the plain
* `Question 2 of 5`.
*
* `pips` and `numbers` fall back to `count` past eight questions, where
* neither is countable at a glance any more. `ring` never does.
*/
variant?: QuestionnaireProgressVariant;
/**
* Replace the indicator entirely. Given a function, it is called with the
* position — for a bar, a row of dots, or a percentage.
*/
children?: ReactNode | ((state: QuestionnaireProgressState) => ReactNode);
}
/** Where the reader is in the set, announced as a progress bar. */
declare function QuestionnaireProgress({ className, variant, children, }: QuestionnaireProgressProps): import("react").JSX.Element;
declare namespace QuestionnaireProgress {
var displayName: string;
}
export interface QuestionnaireItemProps extends Omit {
className?: string;
/** Unique name — the key this question's answer is stored under. */
name: string;
/** Blocks the way forward until it has an answer. */
required?: boolean;
/** Accepts more than one answer, so its answer is an array. */
multiple?: boolean;
/** Left out of the count and never navigated to. */
disabled?: boolean;
/** Mark the question at fault from a validator of your own. */
invalid?: boolean;
/** Called whenever this question moves between unanswered, answered and skipped. */
onStatusChange?: (status: QuestionnaireItemStatus) => void;
children?: ReactNode;
}
/**
* One question. Only the active one is mounted, so anything it holds is built
* when it is reached and thrown away when it is left.
*/
declare function QuestionnaireItem({ className, name, required, multiple, invalid: invalidProp, children, disabled: _disabled, onStatusChange: _onStatusChange, ...props }: QuestionnaireItemProps): import("react").JSX.Element;
declare namespace QuestionnaireItem {
var displayName: string;
}
export interface QuestionnaireQuestionProps extends ViewProps {
className?: string;
children?: ReactNode;
}
/** The question being asked. */
declare function QuestionnaireQuestion({ className, children, ...props }: QuestionnaireQuestionProps): import("react").JSX.Element | null;
declare namespace QuestionnaireQuestion {
var displayName: string;
}
export interface QuestionnaireDescriptionProps extends ViewProps {
className?: string;
children?: ReactNode;
}
/** A line under the question — what to consider, or that it can be skipped. */
declare function QuestionnaireDescription({ className, children, ...props }: QuestionnaireDescriptionProps): import("react").JSX.Element;
declare namespace QuestionnaireDescription {
var displayName: string;
}
export interface QuestionnaireChoicesProps extends Omit {
className?: string;
children?: ReactNode;
}
/**
* The answers to a question. It hands each choice its shortcut badge, counting
* only the ones that can be picked so a disabled answer does not take a letter
* out of the sequence with it.
*/
declare function QuestionnaireChoices({ className, children, ...props }: QuestionnaireChoicesProps): import("react").JSX.Element;
declare namespace QuestionnaireChoices {
var displayName: string;
}
export interface QuestionnaireChoiceProps {
className?: string;
/** The value recorded when this answer is picked. */
value: string;
/** The answer itself. */
label?: string;
/** A line under the label, for an answer that needs explaining. */
description?: string;
disabled?: boolean;
children?: ReactNode;
}
export interface QuestionnaireInputProps extends Omit {
className?: string;
}
export interface QuestionnaireErrorProps extends ViewProps {
className?: string;
/** Replace the default message. */
children?: ReactNode;
}
/** Why the way forward is closed. Nothing until the question fails to pass. */
declare function QuestionnaireError({ className, children, ...props }: QuestionnaireErrorProps): import("react").JSX.Element | null;
declare namespace QuestionnaireError {
var displayName: string;
}
export interface QuestionnaireFooterProps extends ViewProps {
className?: string;
children?: ReactNode;
}
/**
* The action row, in its own section at the foot of the panel. It stays put
* while the question above it changes, which is what keeps the button under
* the thumb where it was.
*/
declare function QuestionnaireFooter({ className, children, ...props }: QuestionnaireFooterProps): import("react").JSX.Element;
declare namespace QuestionnaireFooter {
var displayName: string;
}
/** What a navigation button is told about the question it is acting on. */
export interface QuestionnaireActionState {
/** Whether the action applies to the active question at all. */
visible: boolean;
/** Where the active question stands. */
status: QuestionnaireItemStatus;
}
export interface QuestionnaireActionProps extends Omit {
/** Replace the label. Given a function, it is called with the question's state. */
children?: ReactNode | ((state: QuestionnaireActionState) => ReactNode);
}
/**
* A flexible gap for the footer, so the trailing buttons sit against the
* trailing edge whether or not `Questionnaire.Back` is showing.
*/
declare function QuestionnaireSpacer({ className, ...props }: ViewProps): import("react").JSX.Element;
declare namespace QuestionnaireSpacer {
var displayName: string;
}
export declare const Questionnaire: typeof QuestionnaireRoot & {
Title: typeof QuestionnaireTitle;
Progress: typeof QuestionnaireProgress;
Item: typeof QuestionnaireItem;
Question: typeof QuestionnaireQuestion;
Description: typeof QuestionnaireDescription;
Choices: typeof QuestionnaireChoices;
Choice: import("react").ForwardRefExoticComponent>;
Input: import("react").ForwardRefExoticComponent>;
Error: typeof QuestionnaireError;
Footer: typeof QuestionnaireFooter;
Spacer: typeof QuestionnaireSpacer;
Back: {
({ children, variant, className, ...props }: QuestionnaireActionProps): import("react").JSX.Element | null;
displayName: string;
};
Skip: {
({ children, variant, className, ...props }: QuestionnaireActionProps): import("react").JSX.Element | null;
displayName: string;
};
Next: {
({ children, variant, className, ...props }: QuestionnaireActionProps): import("react").JSX.Element | null;
displayName: string;
};
Submit: {
({ children, variant, className, ...props }: QuestionnaireActionProps): import("react").JSX.Element | null;
displayName: string;
};
};
export {};
//# sourceMappingURL=index.d.ts.map