@shift-software/survey-web-component 0.0.0-stage → 0.1.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/dist/ShiftSurveyElement.d.ts +48 -0
- package/dist/ShiftSurveyElement.d.ts.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26854 -0
- package/dist/index.js.map +1 -0
- package/dist/library-fonts.d.ts +1 -0
- package/dist/library-fonts.d.ts.map +1 -0
- package/package.json +47 -4
- package/src/ShiftSurveyElement.tsx +273 -0
- package/src/index.ts +19 -0
- package/src/library-fonts.ts +7 -0
- package/src/styles.d.ts +6 -0
- package/README.md +0 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=library-fonts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"library-fonts.d.ts","sourceRoot":"","sources":["../src/library-fonts.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shift-software/survey-web-component",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Custom-element wrapper around @shift-software/survey-renderer. Exposes <shift-survey> so non-React hosts (Blazor live-preview, plain HTML embeds, Stencil apps) can mount a survey without pulling React into the host. Ships React + renderer inside the bundle.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"src",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"license": "Apache-2.0",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/ShiftSoftware/ADP",
|
|
23
|
+
"directory": "ADP.Surveys/renderer/packages/survey-web-component"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "vite build && tsc -p tsconfig.build.json --emitDeclarationOnly",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"test.watch": "vitest"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@shift-software/survey-renderer": "^0.1.0",
|
|
36
|
+
"@shift-software/survey-sdk": "^0.1.0",
|
|
37
|
+
"react": "^19.0.0",
|
|
38
|
+
"react-dom": "^19.0.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@testing-library/dom": "^10.4.0",
|
|
42
|
+
"@types/react": "^19.0.0",
|
|
43
|
+
"@types/react-dom": "^19.0.0",
|
|
44
|
+
"jsdom": "^25.0.1",
|
|
45
|
+
"typescript": "^5.6.3",
|
|
46
|
+
"vite": "^6.0.3",
|
|
47
|
+
"vitest": "^2.1.8"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { createElement } from 'react';
|
|
2
|
+
import { createRoot, type Root } from 'react-dom/client';
|
|
3
|
+
import { SurveyRenderer } from '@shift-software/survey-renderer';
|
|
4
|
+
// Relative path on purpose — the renderer's `exports` map is strict and doesn't
|
|
5
|
+
// whitelist `?inline`; reaching through the workspace path is the most robust
|
|
6
|
+
// way to inline the stylesheet at build time.
|
|
7
|
+
import rendererStyles from '../../survey-renderer/src/styles.css?inline';
|
|
8
|
+
import {
|
|
9
|
+
SurveyClient,
|
|
10
|
+
type Survey,
|
|
11
|
+
type SurveySubmission,
|
|
12
|
+
} from '@shift-software/survey-sdk';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* `<shift-survey>` — custom-element wrapper around `<SurveyRenderer>`.
|
|
16
|
+
*
|
|
17
|
+
* Two usage modes, pick whichever fits the host:
|
|
18
|
+
*
|
|
19
|
+
* 1. **API mode** (attribute-driven): the element fetches schema + submits
|
|
20
|
+
* responses against the Surveys API on its own.
|
|
21
|
+
* ```html
|
|
22
|
+
* <shift-survey
|
|
23
|
+
* instance-id="abc-123-guid"
|
|
24
|
+
* api-base="https://api.example.com/api/Surveys"
|
|
25
|
+
* locale="ar"></shift-survey>
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* 2. **Schema mode** (property-driven): the host provides the schema + onSubmit
|
|
29
|
+
* directly as DOM properties. Used by the Blazor builder's live-preview
|
|
30
|
+
* (Phase 4) — the host pushes draft schemas via JS interop as the author
|
|
31
|
+
* types.
|
|
32
|
+
* ```ts
|
|
33
|
+
* const el = document.querySelector('shift-survey');
|
|
34
|
+
* el.schema = draftSchema;
|
|
35
|
+
* el.onSubmit = (submission) => { ... };
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* Lifecycle events bubble up as CustomEvents on the element (`survey:loaded`,
|
|
39
|
+
* `survey:screen-changed`, `survey:completed`, `survey:error`) — same vocabulary
|
|
40
|
+
* as the postMessage protocol, but dispatched as DOM events for page-level hosts.
|
|
41
|
+
* If the element also sits inside an iframe, postMessage emission still fires
|
|
42
|
+
* for the iframe parent; the two channels coexist.
|
|
43
|
+
*/
|
|
44
|
+
export class ShiftSurveyElement extends HTMLElement {
|
|
45
|
+
static get observedAttributes(): string[] {
|
|
46
|
+
return ['instance-id', 'api-base', 'locale', 'mode', 'active-screen-id', 'locale-picker'];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Schema-mode setter. Assigning this swaps the element into schema mode and
|
|
50
|
+
* re-renders with the new schema immediately. */
|
|
51
|
+
#schema: Survey | null = null;
|
|
52
|
+
/** Schema-mode submit handler. In API mode the element manages this itself. */
|
|
53
|
+
#onSubmit: ((submission: SurveySubmission) => void | Promise<void>) | null = null;
|
|
54
|
+
|
|
55
|
+
#root: Root | null = null;
|
|
56
|
+
#mount: HTMLDivElement | null = null;
|
|
57
|
+
#apiSchema: Survey | null = null;
|
|
58
|
+
#apiError: Error | null = null;
|
|
59
|
+
|
|
60
|
+
constructor() {
|
|
61
|
+
super();
|
|
62
|
+
this.attachShadow({ mode: 'open' });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── Lifecycle ───────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
connectedCallback(): void {
|
|
68
|
+
if (!this.shadowRoot) return;
|
|
69
|
+
// Inject the renderer's stylesheet once. Shadow DOM blocks the host page's
|
|
70
|
+
// CSS from leaking in — we own our visual layer end-to-end.
|
|
71
|
+
if (!this.shadowRoot.querySelector('style[data-shift-survey]')) {
|
|
72
|
+
const style = document.createElement('style');
|
|
73
|
+
style.setAttribute('data-shift-survey', '');
|
|
74
|
+
style.textContent = rendererStyles;
|
|
75
|
+
this.shadowRoot.appendChild(style);
|
|
76
|
+
}
|
|
77
|
+
if (!this.#mount) {
|
|
78
|
+
this.#mount = document.createElement('div');
|
|
79
|
+
this.#mount.className = 'shift-survey-mount';
|
|
80
|
+
this.shadowRoot.appendChild(this.#mount);
|
|
81
|
+
}
|
|
82
|
+
if (!this.#root) this.#root = createRoot(this.#mount);
|
|
83
|
+
this.#renderIfReady();
|
|
84
|
+
this.#maybeFetchSchema();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
disconnectedCallback(): void {
|
|
88
|
+
// Defer the unmount — disconnectedCallback fires synchronously during DOM
|
|
89
|
+
// moves, and React 19 complains if we tear down a root mid-render. A
|
|
90
|
+
// microtask delay plus a re-check gives React room to settle. Guard against
|
|
91
|
+
// test teardown races where `window` has been torn down by jsdom.
|
|
92
|
+
queueMicrotask(() => {
|
|
93
|
+
if (this.isConnected || typeof window === 'undefined') return;
|
|
94
|
+
try {
|
|
95
|
+
this.#root?.unmount();
|
|
96
|
+
} catch {
|
|
97
|
+
// jsdom tears down mid-unmount during test runs — no-op is safe.
|
|
98
|
+
}
|
|
99
|
+
this.#root = null;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
attributeChangedCallback(name: string, oldVal: string | null, newVal: string | null): void {
|
|
104
|
+
if (oldVal === newVal) return;
|
|
105
|
+
if (name === 'instance-id' || name === 'api-base') {
|
|
106
|
+
// API-mode inputs changed — drop any cached schema and refetch.
|
|
107
|
+
this.#apiSchema = null;
|
|
108
|
+
this.#apiError = null;
|
|
109
|
+
this.#maybeFetchSchema();
|
|
110
|
+
}
|
|
111
|
+
this.#renderIfReady();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─── Properties ──────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
get schema(): Survey | null {
|
|
117
|
+
return this.#schema;
|
|
118
|
+
}
|
|
119
|
+
set schema(value: Survey | null) {
|
|
120
|
+
this.#schema = value;
|
|
121
|
+
this.#renderIfReady();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
get onSubmit(): ((submission: SurveySubmission) => void | Promise<void>) | null {
|
|
125
|
+
return this.#onSubmit;
|
|
126
|
+
}
|
|
127
|
+
set onSubmit(value: ((submission: SurveySubmission) => void | Promise<void>) | null) {
|
|
128
|
+
this.#onSubmit = value;
|
|
129
|
+
this.#renderIfReady();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Builder-preview jump target. Assigning a screen id makes the renderer
|
|
133
|
+
* jump to that screen (answers preserved); the user can navigate freely
|
|
134
|
+
* afterwards. Mirrors the `active-screen-id` attribute; the property wins
|
|
135
|
+
* when both are set. */
|
|
136
|
+
#activeScreenId: string | null = null;
|
|
137
|
+
get activeScreenId(): string | null {
|
|
138
|
+
return this.#activeScreenId ?? this.getAttribute('active-screen-id');
|
|
139
|
+
}
|
|
140
|
+
set activeScreenId(value: string | null) {
|
|
141
|
+
this.#activeScreenId = value;
|
|
142
|
+
this.#renderIfReady();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Bump to re-issue a jump to the screen already set on {@link activeScreenId}.
|
|
146
|
+
* Property-only (no attribute) — it is a transient signal, not page state. */
|
|
147
|
+
#activeScreenJumpToken = 0;
|
|
148
|
+
get activeScreenJumpToken(): number {
|
|
149
|
+
return this.#activeScreenJumpToken;
|
|
150
|
+
}
|
|
151
|
+
set activeScreenJumpToken(value: number) {
|
|
152
|
+
this.#activeScreenJumpToken = value;
|
|
153
|
+
this.#renderIfReady();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ─── Internals ───────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
#maybeFetchSchema(): void {
|
|
159
|
+
// Only fetch if we're in API mode (instance-id set) AND not already served
|
|
160
|
+
// via the schema property.
|
|
161
|
+
if (this.#schema) return;
|
|
162
|
+
const instanceId = this.getAttribute('instance-id');
|
|
163
|
+
if (!instanceId) return;
|
|
164
|
+
const apiBase = this.getAttribute('api-base');
|
|
165
|
+
if (!apiBase) return;
|
|
166
|
+
const client = new SurveyClient({ baseUrl: apiBase });
|
|
167
|
+
client
|
|
168
|
+
.fetchSchema(instanceId)
|
|
169
|
+
.then((schema) => {
|
|
170
|
+
this.#apiSchema = schema;
|
|
171
|
+
this.#renderIfReady();
|
|
172
|
+
})
|
|
173
|
+
.catch((error: Error) => {
|
|
174
|
+
this.#apiError = error;
|
|
175
|
+
this.#dispatch('survey:error', { message: error.message });
|
|
176
|
+
this.#renderIfReady();
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
#renderIfReady(): void {
|
|
181
|
+
if (!this.#root) return;
|
|
182
|
+
|
|
183
|
+
const apiBase = this.getAttribute('api-base');
|
|
184
|
+
const instanceId = this.getAttribute('instance-id');
|
|
185
|
+
const locale = this.getAttribute('locale') ?? undefined;
|
|
186
|
+
const agentMode = this.getAttribute('mode') === 'agent';
|
|
187
|
+
// `locale-picker` is an override, not a switch: absent means "let the renderer
|
|
188
|
+
// decide" (shown when the survey declares more than one locale), so an existing
|
|
189
|
+
// host that sets nothing gets the picker without changing its markup.
|
|
190
|
+
const localePickerAttr = this.getAttribute('locale-picker');
|
|
191
|
+
const showLocalePicker =
|
|
192
|
+
localePickerAttr === null
|
|
193
|
+
? undefined
|
|
194
|
+
: localePickerAttr !== 'false' && localePickerAttr !== 'off';
|
|
195
|
+
|
|
196
|
+
// Prefer the explicit schema property (schema mode) over a fetched one.
|
|
197
|
+
const schema = this.#schema ?? this.#apiSchema;
|
|
198
|
+
|
|
199
|
+
if (this.#apiError && !schema) {
|
|
200
|
+
this.#root.render(
|
|
201
|
+
createElement(
|
|
202
|
+
'div',
|
|
203
|
+
{ className: 'shift-survey-error', role: 'alert' },
|
|
204
|
+
this.#apiError.message,
|
|
205
|
+
),
|
|
206
|
+
);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (!schema) {
|
|
210
|
+
this.#root.render(
|
|
211
|
+
createElement('div', { className: 'shift-survey-loading' }, 'Loading…'),
|
|
212
|
+
);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Wire onSubmit. Schema mode uses the property the host set; API mode hits
|
|
217
|
+
// the public submit endpoint via SurveyClient.
|
|
218
|
+
const onSubmit = this.#schema
|
|
219
|
+
? (this.#onSubmit ??
|
|
220
|
+
((submission: SurveySubmission) => {
|
|
221
|
+
this.#dispatch('survey:completed', { ...submission });
|
|
222
|
+
}))
|
|
223
|
+
: async (submission: SurveySubmission) => {
|
|
224
|
+
if (!apiBase || !instanceId)
|
|
225
|
+
throw new Error('shift-survey: API mode requires both instance-id and api-base attributes.');
|
|
226
|
+
const client = new SurveyClient({ baseUrl: apiBase });
|
|
227
|
+
await client.submitResponse(instanceId, submission);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const activeScreenId = this.activeScreenId;
|
|
231
|
+
|
|
232
|
+
this.#root.render(
|
|
233
|
+
createElement(SurveyRenderer, {
|
|
234
|
+
schema,
|
|
235
|
+
onSubmit,
|
|
236
|
+
...(locale ? { locale } : {}),
|
|
237
|
+
...(showLocalePicker === undefined ? {} : { showLocalePicker }),
|
|
238
|
+
// Mirror the respondent's pick back onto the element so a host can read it
|
|
239
|
+
// (and so the attribute stays an accurate description of what is showing).
|
|
240
|
+
onLocaleChange: (next: string) => {
|
|
241
|
+
if (this.getAttribute('locale') !== next) this.setAttribute('locale', next);
|
|
242
|
+
this.#dispatch('survey:locale-changed', { locale: next });
|
|
243
|
+
},
|
|
244
|
+
...(activeScreenId
|
|
245
|
+
? { activeScreenId, activeScreenJumpToken: this.#activeScreenJumpToken }
|
|
246
|
+
: {}),
|
|
247
|
+
// Let the element be the resume key in API mode so two surveys on the
|
|
248
|
+
// same host page don't clobber each other.
|
|
249
|
+
...(instanceId ? { resumeKey: instanceId } : {}),
|
|
250
|
+
...(agentMode ? { submissionMeta: { mode: 'agent' } } : {}),
|
|
251
|
+
// CustomEvents are the web-component's channel; postMessage stays opt-in
|
|
252
|
+
// via iframe auto-detect on the enclosing page (unchanged).
|
|
253
|
+
onScreenChange: (screenId: string | null) =>
|
|
254
|
+
this.#dispatch('survey:screen-changed', { screenId }),
|
|
255
|
+
onCompleted: (screenId: string | null) =>
|
|
256
|
+
this.#dispatch('survey:completed', { screenId }),
|
|
257
|
+
}),
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
if (!this.#loadedDispatched) {
|
|
261
|
+
this.#loadedDispatched = true;
|
|
262
|
+
this.#dispatch('survey:loaded', {});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
#loadedDispatched = false;
|
|
267
|
+
|
|
268
|
+
#dispatch(type: string, detail: Record<string, unknown>): void {
|
|
269
|
+
this.dispatchEvent(
|
|
270
|
+
new CustomEvent(type, { detail, bubbles: true, composed: true }),
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import './library-fonts.js'; // first: see the file
|
|
2
|
+
import { ShiftSurveyElement } from './ShiftSurveyElement.js';
|
|
3
|
+
|
|
4
|
+
export { ShiftSurveyElement };
|
|
5
|
+
|
|
6
|
+
/** Register the `<shift-survey>` custom element. Idempotent — calling twice on
|
|
7
|
+
* the same page is a no-op. Consumers who want a different tag name can import
|
|
8
|
+
* `ShiftSurveyElement` directly and call `customElements.define(...)` with it. */
|
|
9
|
+
export function registerShiftSurvey(tagName = 'shift-survey'): void {
|
|
10
|
+
if (typeof window === 'undefined' || typeof customElements === 'undefined') return;
|
|
11
|
+
if (customElements.get(tagName)) return;
|
|
12
|
+
customElements.define(tagName, ShiftSurveyElement);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Auto-register on import so plain `<script type="module" src="...">` embeds
|
|
16
|
+
// Just Work™. Callers who want to rename the tag should import the class and
|
|
17
|
+
// register themselves BEFORE loading this module, or set the tag name before
|
|
18
|
+
// import. This default covers the 90% case.
|
|
19
|
+
registerShiftSurvey();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The booking question's calendar comes from adp-web-components, whose global script adds a
|
|
3
|
+
* Google Fonts stylesheet to the page when it first loads. The survey sets its own font on the
|
|
4
|
+
* calendar, so it opts out. This must be the entry's first import: imports run in order, before
|
|
5
|
+
* any module body, and the library reads the flag as it loads.
|
|
6
|
+
*/
|
|
7
|
+
if (typeof window !== 'undefined') (window as { adpWebComponentsFonts?: boolean }).adpWebComponentsFonts ??= false;
|
package/src/styles.d.ts
ADDED
package/README.md
DELETED