@qaiddev/quests-embed 1.1.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -246,6 +246,27 @@ We offer a Free Plan that hosts both the endpoint and a dashboard for managing y
246
246
  | `saveDebounceMs` | `number` | `500` | Debounce in ms for autosave on text/currency/range |
247
247
  | `autoFocus` | `boolean` | `true` | Auto-focus the input on each step. Set `false` in preview/embedded contexts that shouldn't steal focus |
248
248
 
249
+ ### Host integration
250
+
251
+ For programmatic embedding — e.g. launching a quest from another widget — these hooks let the host pass correlation data in and react to the quest's lifecycle. They're only useful when you construct `QaidQuests` yourself (not via the auto-init `<script>` tag).
252
+
253
+ | Option | Type | Default | Description |
254
+ |--------|------|---------|-------------|
255
+ | `metadata` | `Record<string, unknown>` | — | Extra key/value pairs sent verbatim in the create-response `POST` body. The server decides which keys it persists; unknown keys are ignored. Used, for example, by `@qaiddev/thumbs-embed` to pass `{ feedbackId }` so the QAid backend joins the quest answers to the feedback record. |
256
+ | `onComplete` | `(answers) => void` | — | Called once when the quest is completed and submitted (just after the thank-you screen renders), with a copy of the collected answers. Fires in both modal and inline modes. |
257
+ | `onClose` | `() => void` | — | Called once when the embed is torn down — visitor close, host `destroy()`, or teardown after completion. Lets a host that launched the quest drop its reference. |
258
+
259
+ ```typescript
260
+ const quest = new QaidQuests({
261
+ endpoint: 'https://qaid.dev/api/quests/responses',
262
+ configUrl: 'https://qaid.dev/api/quests/<questId>/definition',
263
+ apiKey: 'YOUR_API_KEY',
264
+ metadata: { feedbackId: 'clx…' }, // correlate this response server-side
265
+ onComplete: (answers) => console.log('done', answers),
266
+ onClose: () => { /* drop your reference */ },
267
+ });
268
+ ```
269
+
249
270
  ### Appearance
250
271
 
251
272
  | Option | Type | Default | Description |
package/dist/a11y.d.ts ADDED
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Shared accessibility primitives for the qaid embeds.
3
+ *
4
+ * These helpers are intentionally framework-free and self-contained (no CSS
5
+ * dependency) so they can be dropped into any open shadow root. They cover the
6
+ * cross-cutting WCAG 2.2 AA infrastructure called for in the embed
7
+ * accessibility plan: live-region announcements, focus management (trap,
8
+ * save/restore), dialog semantics, and background isolation.
9
+ *
10
+ * The API is deliberately identical to @qaiddev/thumbs-embed's a11y module so
11
+ * both embeds consume one shared surface. In quests-embed specifically:
12
+ * - {@link announce} is a GENERAL-PURPOSE announcer for step-change,
13
+ * loading, and thank-you messages. It is separate from the pre-existing
14
+ * per-step `.qaid-q-error` region (aria-live="polite") in embed.ts — that
15
+ * region stays the owner of field-validation errors. Callers should route
16
+ * step/status text through announce()'s polite region and only use the
17
+ * assertive region for load failures, so the two never clobber each other.
18
+ */
19
+ /** Host for the live regions: either a shadow root or a plain element. */
20
+ export type AnnounceRoot = ShadowRoot | HTMLElement;
21
+ /** Options accepted by {@link announce}. */
22
+ export interface AnnounceOptions {
23
+ /** Route the message through the assertive (role="alert") region. */
24
+ assertive?: boolean;
25
+ }
26
+ /** Options accepted by {@link applyDialog}. */
27
+ export interface ApplyDialogOptions {
28
+ /** id of the element that labels the dialog (wired via aria-labelledby). */
29
+ labelledbyId?: string;
30
+ /** id of the element that describes the dialog (wired via aria-describedby). */
31
+ describedbyId?: string;
32
+ /** Fallback accessible name when no labelling element exists. */
33
+ label?: string;
34
+ }
35
+ /** Handle returned by {@link createFocusTrap}. */
36
+ export interface FocusTrap {
37
+ /** Remove the trap's key handler and clean up any tabindex it added. */
38
+ release(): void;
39
+ }
40
+ /**
41
+ * Announce a message to assistive technology via a visually-hidden live region
42
+ * mounted inside `root`. The two regions (polite role="status" and assertive
43
+ * role="alert") are created lazily on first use and reused thereafter.
44
+ *
45
+ * The target region is cleared before the new text is written so that repeated
46
+ * identical messages are re-announced rather than coalesced.
47
+ */
48
+ export declare function announce(root: AnnounceRoot, message: string, opts?: AnnounceOptions): void;
49
+ /**
50
+ * Collect the focusable descendants of `container` in DOM order, excluding
51
+ * disabled controls, elements opted out with tabindex="-1", and elements that
52
+ * are hidden (via the hidden attribute or an inline display/visibility rule on
53
+ * themselves or an ancestor up to the document root).
54
+ */
55
+ export declare function getFocusable(container: HTMLElement): HTMLElement[];
56
+ /**
57
+ * Trap keyboard focus inside `container`: Tab / Shift+Tab cycle between the
58
+ * first and last focusable descendants and can never leave the container. On
59
+ * creation focus moves to the first focusable element, or to the container
60
+ * itself (made programmatically focusable) when it has no focusable children.
61
+ *
62
+ * Call {@link FocusTrap.release} to remove the handler; pair it with
63
+ * {@link restoreFocus} to return focus to the invoking control.
64
+ */
65
+ export declare function createFocusTrap(container: HTMLElement): FocusTrap;
66
+ /**
67
+ * Capture the currently focused element (drilling through shadow roots) so it
68
+ * can be restored later with {@link restoreFocus}. Returns null when focus is
69
+ * on nothing focusable.
70
+ */
71
+ export declare function saveFocus(): HTMLElement | null;
72
+ /**
73
+ * Restore focus to a previously saved element. Safe to call with null or an
74
+ * element that has since been detached (a missing/throwing focus is ignored).
75
+ */
76
+ export declare function restoreFocus(el: HTMLElement | null): void;
77
+ /**
78
+ * Apply dialog semantics to `el`: role="dialog", aria-modal="true", and the
79
+ * naming/description wiring described by `opts`. Prefer aria-labelledby /
80
+ * aria-describedby pointing at visible title/subtitle elements; fall back to
81
+ * aria-label when no visible label element exists.
82
+ */
83
+ export declare function applyDialog(el: HTMLElement, opts?: ApplyDialogOptions): void;
84
+ /**
85
+ * Isolate the background from assistive technology while a dialog is open by
86
+ * marking every top-level sibling of the dialog inert (with an aria-hidden
87
+ * fallback). `except` is the open dialog (or any element inside it); the
88
+ * top-level element that contains it is left interactive.
89
+ *
90
+ * Returns a restore function that reverts every attribute this call changed,
91
+ * leaving elements that were already inert/hidden untouched.
92
+ */
93
+ export declare function setBackgroundInert(except: HTMLElement): () => void;
package/dist/embed.d.ts CHANGED
@@ -31,12 +31,19 @@ export declare class QaidQuests {
31
31
  private backdropEl;
32
32
  private currentInput;
33
33
  private boundKeyDown;
34
+ private savedOpener;
35
+ private focusTrap;
36
+ private inertRestore;
34
37
  private cssVars;
35
38
  private hostThemeOverrides;
36
39
  private hostInlineVars;
37
40
  private themeUrl;
38
41
  private themeDocument;
39
42
  private hostCss;
43
+ private metadata;
44
+ private onCompleteCb;
45
+ private onCloseCb;
46
+ private closed;
40
47
  constructor(config: QuestsConfig);
41
48
  private init;
42
49
  private loadTheme;
@@ -73,6 +80,13 @@ export declare class QaidQuests {
73
80
  private jsonHeaders;
74
81
  private showSaving;
75
82
  private handleKeyDown;
83
+ /**
84
+ * Modal mode only: trap Tab within the card and mark the rest of the
85
+ * page inert while the dialog is open. Runs once (guarded) after the
86
+ * first step renders. Inline mode keeps normal page tab flow and is
87
+ * never trapped or isolated.
88
+ */
89
+ private setupModalA11y;
76
90
  private close;
77
91
  /** Destroy the embed and clean up all resources */
78
92
  destroy(): void;
package/dist/inputs.d.ts CHANGED
@@ -15,6 +15,15 @@ export interface QuestionInput {
15
15
  focus(): void;
16
16
  getValue(): AnswerValue;
17
17
  isValid(): boolean;
18
+ /**
19
+ * Mark the control invalid: sets aria-invalid="true" and links the
20
+ * error text (identified by `errorId`) via aria-describedby so a
21
+ * screen reader reads the error as the field's description. Do not
22
+ * rely on colour alone (WCAG 3.3.1 / 1.3.1 / 4.1.2).
23
+ */
24
+ setInvalid(errorId: string): void;
25
+ /** Clear aria-invalid and unlink the error text once the field is corrected. */
26
+ clearInvalid(): void;
18
27
  }
19
28
  export interface QuestionInputOptions {
20
29
  question: Question;