feat(js,js-components): survey support, Survey Popover component#629
feat(js,js-components): survey support, Survey Popover component#629VojtechVidra wants to merge 13 commits intomainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds cross-SDK survey support (JS + JS Components) and introduces a Survey Popover UI component, while centralizing shared survey constants/helpers and expanding E2E coverage beyond React.
Changes:
- Introduce shared survey utilities/constants (
sessionStoragehelpers, defaults, animations, answered-check helper) and export them from@flows/shared. - Add JS SDK survey runtime (trigger detection, running survey state, API submit plumbing) and wire it into block rendering/click handling.
- Add
@flows/js-componentsSurvey Popover (Lit) and update setup/exports so survey components can be registered and rendered; expand E2E survey/page-targeting coverage to JS.
Reviewed changes
Copilot reviewed 56 out of 57 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| workspaces/shared/src/survey.ts | Adds shared survey constants + storage helpers + answered-check helper. |
| workspaces/shared/src/index.ts | Re-exports shared survey helpers/constants. |
| workspaces/react/src/lib/active-block.ts | Adds shared item→active-block mapping supporting survey blocks. |
| workspaces/react/src/index.ts | Reorganizes exported types (grouping/comments). |
| workspaces/react/src/hooks/use-running-surveys.ts | Switches to shared sessionStorage helpers for running surveys. |
| workspaces/react/src/hooks/use-current-blocks.ts | Refactors to use itemToActiveBlock mapping. |
| workspaces/react-components/src/survey-components/survey-popover/use-survey-popover.ts | Replaces local timing constants with shared constants. |
| workspaces/react-components/src/survey-components/survey-popover/survey-popover.tsx | Uses shared default labels/position; adjusts question context reset. |
| workspaces/react-components/src/survey-components/survey-popover/survey-next-button.tsx | Uses shared isSurveyQuestionAnswered. |
| workspaces/react-components/src/survey-components/survey-popover/single-choice-input.tsx | Extracts click handler; updates Other option usage. |
| workspaces/react-components/src/survey-components/survey-popover/rating-input.tsx | Uses shared emojis; switches hover handlers to pointer events. |
| workspaces/react-components/src/survey-components/survey-popover/other-option.tsx | Improves focus behavior; uses shared “Other” label; pointer events. |
| workspaces/react-components/src/survey-components/survey-popover/multiple-choice-input.tsx | Extracts click handler; updates Other option usage. |
| workspaces/react-components/src/survey-components/survey-popover/freeform-input.tsx | Uses shared default placeholder constant. |
| workspaces/react-components/src/survey-components/survey-popover/end-screen.tsx | Uses shared auto-close timeout constant; minor href cleanup. |
| workspaces/react-components/src/internal-components/text.tsx | Narrows as to `"legend" |
| workspaces/react-components/src/internal-components/input.tsx | Attempts to add ref support to Input component. |
| workspaces/js/src/survey.ts | Adds survey trigger/run logic using signals + MutationObserver + click handler. |
| workspaces/js/src/store.ts | Adds runningSurveyIds signal initialized from session storage. |
| workspaces/js/src/methods.ts | Refactors slot block mapping via itemToActiveBlock. |
| workspaces/js/src/lib/click.ts | Wires survey click handler into document click handling. |
| workspaces/js/src/lib/api.ts | Adds postSurvey API method for submitting survey answers. |
| workspaces/js/src/lib/active-block.ts | Adds survey active-block conversion + refactors setStateMemory. |
| workspaces/js/src/index.ts | Exports survey-related types from shared for JS SDK consumers. |
| workspaces/js/src/debug/panels/settings-panel.ts | Adds CSS class to version label. |
| workspaces/js/src/computed.ts | Filters visible survey blocks by runningSurveyIds; refactors floating items mapping. |
| workspaces/js-components/tsup.config.ts | Adds survey-components entry point to build. |
| workspaces/js-components/src/types/components.ts | Adds SurveyComponent/SurveyComponents types. |
| workspaces/js-components/src/survey-components/survey-popover/survey-popover.ts | New Lit Survey Popover implementation. |
| workspaces/js-components/src/survey-components/survey-popover/survey-next-button.ts | New Survey Next button (Lit) using shared answered-check helper. |
| workspaces/js-components/src/survey-components/survey-popover/single-choice-input.ts | New Lit single-choice input. |
| workspaces/js-components/src/survey-components/survey-popover/rating-input.ts | New Lit rating input (stars/numbers/emojis). |
| workspaces/js-components/src/survey-components/survey-popover/question-context.ts | New global signals-based question “context”. |
| workspaces/js-components/src/survey-components/survey-popover/other-option.ts | New Lit “Other” option rendering + focus management. |
| workspaces/js-components/src/survey-components/survey-popover/multiple-choice-input.ts | New Lit multiple-choice input. |
| workspaces/js-components/src/survey-components/survey-popover/index.ts | Exports the survey-popover component. |
| workspaces/js-components/src/survey-components/survey-popover/freeform-input.ts | New Lit freeform input (textarea). |
| workspaces/js-components/src/survey-components/survey-popover/end-screen.ts | New Lit end screen rendering. |
| workspaces/js-components/src/survey-components/index.ts | Re-exports survey components. |
| workspaces/js-components/src/survey-components.ts | Adds top-level survey-components barrel export. |
| workspaces/js-components/src/render.ts | Extends setup options + registration to include survey components. |
| workspaces/js-components/src/internal-components/text.ts | Adds as/id support using lit/static-html. |
| workspaces/js-components/src/internal-components/input.ts | Adds internal Input helper (Lit) used by survey components. |
| workspaces/js-components/src/internal-components/button.ts | Adds disabled support + refactors class building/closing tag. |
| workspaces/js-components/src/icons/star-filled-16.ts | Adds star-filled icon (Lit) for rating display. |
| workspaces/js-components/src/icons/star-empty-16.ts | Adds star-empty icon (Lit) for rating display. |
| workspaces/js-components/src/components-store.ts | Adds surveyComponents registry. |
| workspaces/js-components/src/block.ts | Adds survey component resolution for rendering active blocks. |
| workspaces/js-components/package.json | Adds survey-components export + new dependencies. |
| workspaces/e2e/tests/tour-trigger.spec.ts | Removes React-only survey skip to enable multi-SDK coverage. |
| workspaces/e2e/tests/survey.spec.ts | Enables running survey tests for JS; adjusts URL expectations. |
| workspaces/e2e/tests/page-targeting.spec.ts | Removes React-only survey skip; minor formatting. |
| workspaces/e2e/pages/js.ts | Registers surveyComponents in JS E2E page setup. |
| pnpm-lock.yaml | Updates lockfile for new deps + tool version bumps. |
| package.json | Bumps oxlint/oxfmt tool versions. |
| cspell.json | Adds “describedby”/“labelledby” words. |
| .oxlintrc.json | Enables type-aware linting. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export const currentQuestionValue = signal<SurveyQuestion>(); | ||
|
|
||
| export const questionToContextValue = (question: SurveyQuestion): ContextData => ({ | ||
| value: "getValue" in question ? question.getValue() : undefined, | ||
| optionIds: "getSelectedOptionIds" in question ? question.getSelectedOptionIds() : [], | ||
| otherSelected: "getOtherSelected" in question ? question.getOtherSelected() : false, | ||
| }); | ||
|
|
||
| export const getDefaultQuestionState = (): ContextData => ({ | ||
| value: "", | ||
| optionIds: [], | ||
| otherSelected: false, | ||
| }); | ||
|
|
||
| export const questionState = signal<ContextData>(getDefaultQuestionState()); | ||
|
|
||
| const refresh = (): void => { | ||
| const currentQuestion = currentQuestionValue.value; | ||
| if (currentQuestion) { | ||
| questionState.value = questionToContextValue(currentQuestion); | ||
| } else { | ||
| questionState.value = getDefaultQuestionState(); | ||
| } |
There was a problem hiding this comment.
The question context signals (currentQuestionValue, questionState) are module-level singletons, so multiple SurveyPopover instances rendered at the same time will overwrite each other’s state (answers/selection can bleed between surveys). Make the context state instance-scoped (e.g., stored on the SurveyPopover element and passed down) rather than shared globals.
| connectedCallback(): void { | ||
| super.connectedCallback(); | ||
|
|
||
| this._questionIndex = this.survey.getCurrentQuestionIndex(); | ||
| this.popoverElement?.addEventListener("transitionend", this.handleTransitionEnd.bind(this)); | ||
| } |
There was a problem hiding this comment.
popoverElement is queried from the rendered DOM, so it won’t be available in connectedCallback; the transitionend listener likely never attaches. Attach the listener in firstUpdated/updated once popoverElement exists, and remove the same bound handler in disconnectedCallback to avoid leaks.
| const inputRef = createRef(); | ||
|
|
||
| export const OtherOption = ({ question }: Props) => { | ||
| const { value, otherSelected, refresh } = useQuestionContext(); | ||
| const type = question.type === "single-choice" ? "radio" : "checkbox"; |
There was a problem hiding this comment.
inputRef is declared at module scope, so all OtherOption instances share the same ref and can focus the wrong input when multiple surveys/questions render. Create the ref inside OtherOption so each instance has its own reference.
| const inputRef = createRef(); | |
| export const OtherOption = ({ question }: Props) => { | |
| const { value, otherSelected, refresh } = useQuestionContext(); | |
| const type = question.type === "single-choice" ? "radio" : "checkbox"; | |
| export const OtherOption = ({ question }: Props) => { | |
| const { value, otherSelected, refresh } = useQuestionContext(); | |
| const type = question.type === "single-choice" ? "radio" : "checkbox"; | |
| const inputRef = createRef<HTMLInputElement>(); |
| const hoverIndexSignal = signal<number | null>(null); | ||
|
|
||
| export const RatingInput = ({ | ||
| descriptionId, | ||
| legendId, | ||
| onAnswer, | ||
| question, | ||
| }: Props): TemplateResult => { | ||
| const { value, refresh } = useQuestionContext(); | ||
| const hoverIndex = hoverIndexSignal.value; | ||
|
|
There was a problem hiding this comment.
hoverIndexSignal is module-scoped, so hover state will be shared across all RatingInput instances (and across multiple concurrent surveys). Make this hover state instance-scoped (e.g., per SurveyPopover / per RatingInput invocation) to prevent cross-popover interference.
| export const Button = ({ | ||
| className, | ||
| className: classNameProp, | ||
| variant, | ||
| size = "default", | ||
| children, | ||
| onClick, | ||
| href, | ||
| target, | ||
| disabled, | ||
| }: Props): TemplateResult => { | ||
| const tag = href ? aLiteral : buttonLiteral; | ||
|
|
||
| const className = clsx( | ||
| "flows_basicsV2_button", | ||
| `flows_basicsV2_button_${variant}`, | ||
| `flows_basicsV2_button_size_${size}`, | ||
| disabled && "flows_basicsV2_button_disabled", | ||
| classNameProp, | ||
| ); | ||
|
|
||
| return html` | ||
| <${tag} | ||
| type=${tag === buttonLiteral ? "button" : undefined} | ||
| class=${clsx( | ||
| "flows_basicsV2_button", | ||
| `flows_basicsV2_button_${variant}`, | ||
| `flows_basicsV2_button_size_${size}`, | ||
| className, | ||
| )} | ||
| class=${className} | ||
| @click=${onClick} | ||
| target=${target} | ||
| href=${href} | ||
| ?disabled=${disabled} | ||
| > |
There was a problem hiding this comment.
disabled doesn’t behave on <a> elements, so when href is set this won’t prevent navigation or clicks (it only adds a class/attribute). If disabled is true, consider rendering a <button> (no href) or suppressing the click/default action and removing href/target.
Pull request checklist