Skip to content

Changelog ​

Versjonshistorikk for @navikt/lumi-survey.

Endringsloggen vedlikeholdes på engelsk i henhold til npm-konvensjoner.

All notable changes to @navikt/lumi-survey will be documented in this file.

This project follows SemVer.

[2.5.0] - 2026-10-21 ​

Added ​

  • transport accepts { endpoint: "/api/lumi/feedback" }. The widget then sends the answers itself: it POSTs submission.transportPayload as JSON to the endpoint with only Content-Type: application/json, on the page's own origin (credentials and mode are same-origin), and passes the abort signal on. Only a 2xx answer counts as success. A redirect, for example to a login page, and a web page answered with 200 count as failures, so the receipt never shows while the answers are lost. Error answers throw LumiSurveyHttpError, so the time limit and the automatic retry from 2.4.0 apply. The widget does not follow redirects, not even one that only adds a trailing slash, so endpoint must be the exact path of the route. Each error answer logs one [Lumi] line without the payload. LumiSurveyEndpoint is the type of this form. A transport with a submit function always wins over endpoint.
  • @navikt/lumi-survey/client is an entry point with "use client" for React Server Components, such as the Next.js App Router. A server component can render LumiSurveyDock from it with transport=, data and JSX. Functions such as events, a transport with submit and labels.textTooLong still need the app's own client component. The main entry stays without the directive. TypeScript resolves the types of the new entry with moduleResolution bundler or node16.

Changed ​

  • The type of transport is now LumiSurveyTransport | LumiSurveyEndpoint. Code that reads props.transport.submit must check for submit first. Wrappers that extend LumiSurveyDockProps can still narrow transport.
  • Without a usable transport, for example when transport is a string or endpoint is empty, relative to the page or on another origin, the dock renders nothing, counts no view and logs one [Lumi] error. Previously it rendered and failed when the user submitted.

[2.4.0] - 2026-10-21 ​

Added ​

  • LumiSurveyHttpError lets a transport report the HTTP status from the app's backend: throw LumiSurveyHttpError.fromResponse(response). It keeps status and the seconds from a valid Retry-After header.
  • transport.submit gets a second, optional argument with an AbortSignal: submit(submission, options?). Pass options.signal on to fetch, so the request is cancelled when the widget stops waiting. A function that already takes another optional second parameter now receives this object, and TypeScript rejects assigning it as submit.
  • style.zIndex sets the stacking order of the dock. The default is still 1000. Lower it to place the dock under the app's own fixed elements.
  • behavior.hideMinimizedButton shows nothing while the dock is minimized, instead of the «Gi tilbakemelding» button. Closing the survey then returns focus to the page. Users the dock cannot store a dismissal for, for example without consent, start minimized and never see the survey.
  • LumiApiErrorType.DATABASE_UNAVAILABLE, which Lumi API returns with 503 when the database does not answer in time. Code with an exhaustive switch or a Record over LumiApiErrorType must handle it.

Changed ​

  • The widget waits at most 10 seconds for each attempt. It then aborts the signal and shows the error message. onSubmitError receives a DOMException named TimeoutError. Previously the widget waited for as long as the browser kept the request open.

  • The widget tries once more with the same submission and deduplication key when the transport throws an error with a numeric status:

    • 503: after Retry-After, or after 0.5–1.5 seconds without it. The spread keeps widgets that failed together from retrying in the same instant.
    • 429: only when Retry-After asks for at most 5 seconds. Lumi API's rate limits last a minute, so a retry without it would be rejected again.

    When Retry-After asks for more than 5 seconds, the error is shown at once. Lumi API stores a repeated deduplication key only once. onSubmitStart and onSubmitError are still called once per submit. Transports that throw a plain Error behave as before. Transports whose own error type has a numeric status now get the retry too.

  • A submit that settles after reset(), for example after the user closed the dock, no longer sets the status or error of the reset survey. The events still fire.

  • Returning focus to the page after the dock hides no longer scrolls the page to the element that had focus before.

[2.3.0] - 2026-10-15 ​

Version 2.2.4 was prepared in the repository but never published. Its changes are included in 2.3.0.

Changed ​

  • Apps without the Nav decorator that do not set storageStrategy now start minimized. The default consent strategy cannot store anything there, so users see the Gi tilbakemelding button and open the survey themselves. Set behavior.storageStrategy: "localStorage" to have the survey open by itself and remember that it was closed. The widget never falls back to localStorage on its own, since that would bypass consent.
  • The dock now starts minimized whenever it cannot store a dismissal, whatever initialOpen says: without survey consent, when the decorator does not allow the storage key, without the decorator, and when the browser blocks localStorage. These users previously saw the survey open by itself on every page load. storageStrategy: "none" still follows initialOpen.
  • While the consent lookup is pending after the 300 ms grace period, the dock shows the minimized button. It opens later only when consent turns out to be given and nothing is stored, and only if the user has not interacted with the dock. Previously it opened after 300 ms and could minimize or disappear when a late lookup found a stored dismissal.
  • A successful submit is now stored right away, with the same hideAfterSubmit and dismissCooldownDays handling as closing the receipt. Users who leave the receipt without pressing close no longer get the survey again on the next page load. A failed write after the submit calls onDismissalPersistFailed. Closing the receipt afterwards does not write the same dismissal again.
  • Reopening the minimized dock clears storage only when the read found a stored value or this session has written one, also when the user opens the dock before the consent lookup has answered. Users without consent therefore no longer trigger onDismissalPersistFailed just by opening the survey.
  • The widget writes one console.warn per page load, also in production, when the consent setup is wrong. Without the Nav decorator it advises storageStrategy: "localStorage". When the decorator refuses the storage key although the user has consented, it advises asking in #lumi. Both link to the storage guide. A user who declines consent causes no warning.
  • onViewDock now fires once per actual opening of the panel, whether it is shown open from the start or opened from the minimized button. It previously fired on mount, before the stored state was read, also for users who only ever saw the minimized button or nothing. Users on nav.no without survey consent, and all users in apps without the Nav decorator that do not set storageStrategy: "localStorage", no longer count until they open the survey themselves. Teams that compare responses with views get a break in that series at 2.3.0.

Added ​

  • lang on LumiSurveyDock sets the language of the text that comes from the survey: titles, questions, options, descriptions, the intro and receipt text and authored NPS end labels. Without lang that text inherits the page language, as in 2.2.3. Set for example lang="nb" for a Bokmål survey on an English page. Option descriptions, combobox options and rating labels are passed to Aksel or read through aria-label as plain strings and cannot carry their own lang.

Fixed ​

  • Built-in copy shown as text is now marked lang="nb", so screen readers read it in Norwegian Bokmål on pages in other languages. This covers for example "Neste", "Tilbake", "(valgfritt)", "Steg X av Y", the default receipt title, the default buttons, the default NPS end labels and the default error and privacy messages. Copy replaced through labels, success.primaryLabel or behavior.personalDataNotice gets no lang and inherits the page language. The dock itself sets no lang. Attribute values such as aria-label cannot be marked and inherit the page language as before.
  • When closing the receipt hides the dock through hideAfterSubmit, also after success.autoClose, focus returns to the element that had it before the dock. If that element is gone or can no longer take focus, focus moves to #maincontent, or to <body> as a last resort, with a temporary tabindex="-1". A tabindex the page has set itself is kept. Focus was previously lost.
  • Emoji and NPS rating buttons now show focus in forced colors mode, such as Windows high contrast. A transparent outline accompanies the focus shadow that forced colors remove.
  • The NPS scale wraps into rows of 0–5 and 6–10 when it is narrower than 26rem, and into rows of four below 11rem. A wrapped last row ends at the right edge, so 10 stays above the high label. The buttons keep room for their numbers on small screens and with enlarged text, and the scale no longer clips their focus ring. The layout needs no container query, so RatingQuestionField also works in shrink-to-fit layouts.
  • The default panelMaxHeight is now calc(100dvh - 2rem), with calc(100vh - 2rem) in browsers without dvh, so mobile browser toolbars cannot push the close button off screen.
  • The @default values in the LumiSurveyLabels JSDoc now match the copy the dock shows, for example "Send" and "Kunne ikke sende tilbakemeldingen. Prøv igjen senere.".

Documentation ​

  • The package README uses clearer wording and current names: Nais, Nais Console and Entra ID.

[2.2.3] - 2026-09-13 ​

Documentation ​

  • Start with Surveyverksted to create and share a survey, then integrate the exported document in your app. The package README now follows this flow through backend setup and checking responses in Lumi.
  • Add documentation, issue tracker and search keywords to the package metadata.

[2.2.2] - 2026-09-10 ​

Distribution ​

  • The package is now published to npmjs as the primary, anonymously readable registry and to GitHub Packages as a compatibility mirror. Consumers no longer need registry configuration or a GitHub token to install it.
  • Published metadata now declares the MIT license and the package's source directory in navikt/lumi.

[2.2.1] - 2026-09-07 ​

Fixed ​

  • V1 survey documents now ignore answers from hidden source questions when deciding which follow-ups to show, validate and submit. Closing a branch hides all dependent follow-ups while retaining their answers locally for returning to that branch. Alternative any conditions, metadata conditions and the existing behavior of negative conditions on visible, unanswered questions are preserved. Legacy flat survey visibility is unchanged.
  • Conditional page flows show the current step without an estimated percentage or fixed total. Linear flows retain their exact progress indicator.

[2.2.0] - 2026-08-31 ​

Version 2.1.1 was prepared in the repository but never published. Its changes are included in 2.2.0 together with the later additions since 2.1.0.

Added ​

  • Schema V2 submissions now include a versioned, canonical visibleIf flow contract. Surveys that still use deprecated imperative logic remain submission-compatible but deliberately omit the flow contract so analytics treats those rows as unpinned. Legacy visibleIf shapes that cannot be represented exactly by visible-if-v1 likewise keep submitting without a flow contract instead of failing or recording false provenance.

Fixed ​

  • validateSurveyDocumentV1 and buildCanonicalSurvey now reject visibleIf operators that cannot work against the referenced question's type — for example EQ against a multiChoice question, whose array answers never strictly equal a single value. The error names the owning question, the referenced question and its type, and the operators that are allowed. The runtime-compatible set per type is exported as allowedVisibleIfOperators. CONTAINS remains supported for string-valued single-choice answers so existing code-authored V1 documents keep working; the workshop continues to offer exact equality for that type. METADATA conditions and legacy flat surveys are unaffected.

  • Focus now follows dock transitions: opening targets the active heading, closing targets the minimized trigger, and successful submission targets the receipt heading. The minimized trigger no longer references an unmounted panel through disclosure attributes.

  • Consent storage no longer leaves the widget blank for five seconds when the Nav decorator is unavailable. The initial view is released after a 300 ms grace period while a late persisted dismissal can still be applied.

  • Rating questions now expose exactly one named group to assistive technology. The prompt is the fieldset legend (a level 3 heading when visible), the fieldset itself carries role="radiogroup", and the visually hidden legend copy no longer duplicates an external prompt heading. Screen readers previously announced the question up to three times per rating group.

  • The published stylesheet now contains only lumi--namespaced selectors. Unused CSS Module output previously leaked generic selectors such as .container, .header, .panel and .active into consumer applications.

  • Star-rating icons now keep a stable pointer target while hover and focus feedback changes. In Chromium, swapping the icon element during a real pointer interaction could leave the star focused but unchecked, so a required star question could not be submitted.

  • Text answers that exceed the configured limit, or the API maximum of 2000 characters, are now blocked in the widget with a field-level validation message instead of failing permanently as a generic transport error. Consumers can customize the new validation summary and length message with labels.validationSummary and labels.textTooLong.

  • Inline events objects no longer count parent re-renders as new dock views or restart the success auto-close timer.

  • Dismissal storage failures now invoke onDismissalPersistFailed with their cause, while intentional none storage and server rendering remain silent.

Internal ​

  • The local full-chain verification now exercises every stable survey and field scenario from the test bench and records per-scenario receipts in the release evidence.

[2.1.0] - 2026-08-20 ​

Added ​

  • createRatingSurveyDocument and DEFAULT_RATING_SURVEY_DOCUMENT make the page-based model the recommended starting point for rating surveys too.
  • createDiscoverySurveyDocument, createTopTasksSurveyDocument and createTaskPrioritySurveyDocument provide verified, page-based SurveyDocumentV1 templates for Lumi's specialized analytics.
  • Specialized survey contracts are checked by the widget before transport so invalid field IDs or answer shapes cannot silently produce empty analytics.

Fixed ​

  • Specialized fields must now be required and always visible, Task Priority enforces a usable selection limit, optional answers are validated when supplied, and checkbox choices enforce maxSelections in the UI as well as at submission.
  • Submission setup errors now settle the widget in its error state and call onSubmitError instead of leaving the UI in a permanent submitting state.
  • The new Discovery, Top Tasks and Task Priority document builders use one canonical field vocabulary: task, success, blocker and priority. The widget and API continue to accept the field IDs emitted by the deprecated 2.0.1 builders. Specialized analytics require an explicit type, so ordinary surveys with similarly named fields are never misclassified.
  • Multi-choice maxSelections is now part of the V2 definition contract and is enforced by the API. Existing definitions without the field are enriched once on their first compatible submission; later limit changes are rejected as structural changes.

Documentation ​

  • The legacy flat LumiSurveyConfig, rating presets and builders are marked deprecated in TypeScript while remaining runtime-compatible in 2.x.
  • The package README now teaches SurveyDocumentV1 as the only model for new surveys and links to the page, visibility and migration guides. Legacy flat configs, presets, builders and logic remain supported in 2.x but are documented only as compatibility APIs.
  • The LumiSurveyDock example now uses a consumer-defined survey variable instead of the nonexistent NAV_STANDARD_RATING symbol.

[2.0.1] - 2026-08-20 ​

Fixed ​

  • Rating questions now render validation messages with Aksel's ErrorMessage in a permanently mounted, polite live region. This gives rating, text and choice fields the same visual treatment and announcement priority. Rating errors previously used a plain BodyShort with an assertive alert and a package-owned colour override.
  • Rating choices now form one keyboard tab stop. Arrow keys move and select within emoji, thumbs, stars and NPS controls, including wrapping from the first choice to the last and back.

[2.0.0] - 2026-08-19 ​

This release adds the page-based authoring format (SurveyDocumentV1) — the serializable document Surveyverksted produces. A consumer on 1.0.0 cannot type-check or render an authored export at all, because the format did not exist there.

BREAKING ​

  • question.visibleIf is now typed VisibleIfCondition (leaf | group) so it can carry any/all groups. Code that reads visibleIf.operator directly must narrow with isConditionGroup/isLeafCondition first. Authoring a condition is unchanged — only reading one back is affected. LogicCondition stays leaf-only, so LogicRule consumers are untouched. (#333)

The dock title renders at the same size as in 1.0.0. Its line height tightens, and the description below it grows from 14px to 16px. See Changed.

Added ​

  • SurveyDocumentV1 gains optional survey-level screens: intro (title, body?, startLabel?) and success (title, body?) as plain strings. LumiSurveyDock derives its intro/success screens from the document, with explicit embed props as field-level overrides — a partial success= keeps the authored title and body. Blank titles read as absent (lenient drafts never render an empty screen), and blank startLabel falls back to "Start" so the intro button always has an accessible name. validateSurveyDocumentV1 shape-checks the new fields. (#441)

  • behavior.initialPageId, behavior.simulatedViewport and style.panelMaxHeight add an embedding seam for authoring previews: start the flow on a specific authored page, size and classify (viewport/deviceType) from a simulated viewport, and constrain the open panel's height. Production behavior is unchanged when the props are absent. (#338)

  • validateSurveyDocumentV1 exposes the widget's runtime document validation as a pure public seam for authoring tools before preview or export. (#338)

  • SurveyDocumentV1, SurveyPageV1 and SurveyQuestionV1 add a serializable page-based authoring format. A page can contain multiple questions that render and validate together; singlePage preserves page headings while step layout navigates pages. The new format uses question-level visibleIf and deliberately rejects legacy logic. Existing flat LumiSurveyConfig inputs remain supported unchanged. (#336)

  • visibleIf now supports any (OR) and all (AND) to combine multiple conditions. The wider condition type is exported as VisibleIfCondition (with isConditionGroup/getLeafConditions/isLeafCondition helpers); LogicCondition stays leaf-only so LogicRule consumers are unaffected. Note: question.visibleIf is now typed VisibleIfCondition (leaf | group), so code reading visibleIf.operator directly must first narrow with isConditionGroup/isLeafCondition. (#333)

Changed ​

  • behavior.initialPageId now also suppresses the intro screen when the requested page exists in the document — an explicit start page (authoring previews, deep links) outranks the intro. A typo'd or unknown id keeps the intro and falls back to normal navigation. (#441)

  • createTopTasksSurvey now expresses its flow with visibleIf instead of logic. Answer-based value conditions (EQ/NEQ/GT/LT/CONTAINS) automatically enable step mode under questionLayout: "auto", while EXISTS-only progressive disclosure remains single-page. (#359)

  • behavior.showProgress: true now shows progress from the first question in step mode. Intro and success screens are not counted, and single-step surveys omit the indicator. Branching exposes only the known step number to assistive technology. (#334)

  • Progress indicators now include visible step text: Steg X av N for linear surveys and Steg X when branching makes the total uncertain. (#418)

  • Dock header typography now comes from Aksel Heading/BodyShort props instead of font-size overrides in the package stylesheet. Upgrading from 1.0.0 does not change the rendered header size: 1.0.0 asked for Heading medium but the package's own unlayered rule overrode it to 1.25rem, so the header rendered at 20px/600 then and renders at 20px/600 now. Two smaller things do change: the heading's line height tightens from 32px to 28px, and the panel description grows from 14px to 16px, because it too came from a stylesheet override (--ax-font-size-small) and now uses BodyShort size="small" — which is 16px in Aksel's scale, not 14px. The header is the panel's title block and keeps one scale whatever fills it — an authored page title, the first question standing in for one, intro or success. (#416, #447)

  • An authored page title now renders at the title scale with a visible group boundary above its questions. It previously shared the field scale with the question headings and labels below it (Aksel Heading xsmall and .aksel-label are both 1.125rem/bold), so a title, the question under it and the next field label were three identical lines. Applies both in the panel header and inline in singlePage layout. New in this release — the page format itself did not exist in 1.0.0. (#447)

Fixed ​

  • Step navigation now moves focus to the newly rendered question heading after both Next and Back, without stealing focus when an answer changes. (#417)
  • logic conditions that reference another question via condition.questionId are now evaluated against that question's answer instead of the current question's. Cross-question branching (e.g. routing on an earlier answer) now works the same way visibleIf already did. (#332)
  • Submissions now omit answers from questions hidden by visibleIf at submit time while retaining the complete survey definition. Hidden answers remain in local state so they are restored if the user reopens the branch. (#357)
  • Reachable-step estimates now count overlapping unresolved visibleIf branches (such as multiple NEQ/CONTAINS conditions or overlapping numeric ranges) together while still counting mutually exclusive branches only once. (#358)
  • METADATA conditions now receive auto-collected context fields (deviceType, viewport, screenResolution, userAgent) and opt-in location fields in addition to flattened context.tags. Submission-time visibility uses the same metadata map; context.debug remains excluded. (#144)

[1.0.0] - 2026-06-24 ​

This release switches the widget to the v2 submission schema. The widget now always emits schemaVersion: 2; the v1 wire format is no longer sent. Requires a backend that accepts schema v2 (@navikt/lumi-api with schema v2 support, shipped in #297).

Changed ​

  • BREAKING: Submissions are now sent with schemaVersion: 2 — the widget no longer emits the v1 payload shape (#297)
  • Each submission now carries a self-describing definition block (SubmissionDefinition with typed fields: RATING, TEXT, SINGLE_CHOICE, MULTI_CHOICE, DATE), so the survey structure travels with the data

Added ​

  • deduplicationKey on every submission for idempotent delivery: generated client-side, exposed via getDeduplicationKey from useLumiSurvey, and rotated after a successful submit and on reset
  • New public types re-exported from the package: LumiApiFeedbackSubmission (the V1 | V2 union), LumiApiFeedbackSubmissionV2, LumiApiSubmissionDefinition, and LumiApiSubmissionFieldDefinition

Internal ​

  • Migrated the workspace from npm to pnpm (#210) and upgraded to TypeScript 6 (#238)
  • Removed unused close-button CSS from the dock fallback styles (#269)

[0.4.0] - 2026-03-25 ​

Added ​

  • Multi-signal device detection: uses UA Client Hints, UA string parsing, and iPadOS 13+ heuristic (maxTouchPoints) before falling back to viewport width (Closes #168)
  • screenResolution field in submission context — captures actual screen dimensions via window.screen

Changed ​

  • deviceType classification is now based on actual device signals instead of viewport width alone, fixing misclassification when browser DevTools is open

[0.3.0] - 2026-03-20 ​

Added ​

  • hasIntro prop on ProgressProps — shows progress bar from step 0 when survey has intro page

Fixed ​

  • Progress bar no longer jumps/oscillates with chained visibleIf conditions (Closes #163)
  • Progress bar now reaches 100% on the last step in branching surveys
  • Progress bar only updates on navigation events, preventing visual jitter from checkbox interactions

Changed ​

  • Replaced high-water mark mechanism with direct reachability estimation via computeReachableSteps algorithm

[0.2.0] - 2026-03-17 ​

Added ​

  • Added optional intro support to LumiSurveyDock, so surveys can start with an intro screen before the first question.
  • Added behavior.showProgress and events.onStepChange for step-based surveys, including progress feedback based on visible steps.
  • Added DEFAULT_SURVEY_THUMBS, DEFAULT_SURVEY_STARS, and DEFAULT_SURVEY_NPS exports for ready-made rating surveys.

Changed ​

  • useLumiSurvey().validate() and submit() now accept an optional question subset, so step-based and branched flows can validate only the questions in the active path.

Fixed ​

  • Step navigation now respects visibleIf conditions when moving through branched surveys.
  • Removed the horizontal scrollbar on emoji rating rows and improved smiley alignment.

[0.1.1] - 2026-03-05 ​

Changed ​

  • localStorage key prefix changed from flexjar-* to lumi-*. Previously dismissed surveys will reappear once as the old keys are no longer read. Requires lumi-* to be allowlisted in the NAV consent API.

Removed ​

  • Removed all legacy flexjar references from source, mocks, and documentation.
  • Removed duplicate test file FlexJarDock.test.tsx (covered by LumiSurveyDock.test.tsx).

[0.1.0] - 2026-02-26 ​

Changed ​

  • Removed @navikt/nav-dekoratoren-moduler dependency. The consent storage strategy now reads directly from the NAV consent API window globals (window.__DECORATOR_DATA__ and window.webStorageController). No extra npm package needed — behavior is identical.
  • Simplified consent API polling logic (setInterval instead of recursive setTimeout).
  • Cleaned up Storybook config (removed module aliasing and viteFinal override).

[0.0.5] - 2026-01-21 ​

Changed ​

  • Tooling: upgraded to Vitest v4 and added V8 coverage provider support (@vitest/coverage-v8).

[0.0.4] - 2026-01-21 ​

Fixed ​

  • Next.js/SSR safety: LumiSurveyDock is a client component and avoids window is not defined during server rendering.

[0.0.3] - 2026-01-21 ​

Changed ​

  • Rating surveys: the first rating question defaults to required: true when omitted.
  • Submit UX: button stays hidden until the user has interacted (validation happens on submit).
  • Optional labeling: “(valgfritt)” is derived from required consistently; presets no longer hardcode it in prompt strings.

[0.0.2] - 2026-01-21 ​

Added ​

  • Runtime validation for invalid question references (visibility/branching).
  • NPS hover/focus styling (moved to dedicated NPS styles).

Changed ​

  • Privacy-safe defaults: url is never auto-collected; pathname is only auto-collected when behavior.collectLocation is enabled.
  • Personal data notice is only shown when a text question is actually visible.
  • Submit UX: submit button is hidden until submission is possible (no disabled “send” button).
  • Logic conditions: field defaults to "ANSWER" when omitted.

Removed ​

  • Deprecated createLumiApiTransport() export.

[0.0.1] - 2026-01-19 ​

Added ​

  • First installable edition

Changed ​

  • Made @navikt/lumi-survey self-contained for external publishing.

Utviklet og driftet av Team eSyfo i Nav