@welshare/questionnaire 0.2.4 → 0.2.7
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 +191 -2
- package/dist/esm/components/question-renderer.d.ts +8 -1
- package/dist/esm/components/question-renderer.d.ts.map +1 -1
- package/dist/esm/components/question-renderer.js +70 -6
- package/dist/esm/components/questions/boolean-question.d.ts +3 -2
- package/dist/esm/components/questions/boolean-question.d.ts.map +1 -1
- package/dist/esm/components/questions/boolean-question.js +2 -2
- package/dist/esm/components/questions/choice-question.d.ts +10 -3
- package/dist/esm/components/questions/choice-question.d.ts.map +1 -1
- package/dist/esm/components/questions/choice-question.js +31 -23
- package/dist/esm/components/questions/multiple-choice-question.d.ts +11 -4
- package/dist/esm/components/questions/multiple-choice-question.d.ts.map +1 -1
- package/dist/esm/components/questions/multiple-choice-question.js +20 -6
- package/dist/esm/components/questions/other-text-input.d.ts +39 -0
- package/dist/esm/components/questions/other-text-input.d.ts.map +1 -0
- package/dist/esm/components/questions/other-text-input.js +35 -0
- package/dist/esm/components/questions/quantity-question.d.ts +13 -0
- package/dist/esm/components/questions/quantity-question.d.ts.map +1 -0
- package/dist/esm/components/questions/quantity-question.js +79 -0
- package/dist/esm/index.d.ts +3 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/lib/constants.d.ts +7 -0
- package/dist/esm/lib/constants.d.ts.map +1 -1
- package/dist/esm/lib/constants.js +7 -0
- package/dist/esm/types/index.d.ts +6 -0
- package/dist/esm/types/index.d.ts.map +1 -1
- package/dist/styles.css +117 -0
- package/dist/tokens.css +6 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @welshare/questionnaire
|
|
2
2
|
|
|
3
|
-
FHIR
|
|
3
|
+
FHIR R5 Questionnaire components for React with state management, validation, and theming.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -104,10 +104,30 @@ const {
|
|
|
104
104
|
- `className?: string` - Container CSS classes
|
|
105
105
|
- `inputClassName?: string` - Input CSS classes
|
|
106
106
|
- `choiceClassName?: string` - Choice option CSS classes
|
|
107
|
+
- `choiceLayout?: ChoiceLayout` - `"stacked"` (default) or `"inline-wrap"` (horizontal chip layout)
|
|
107
108
|
- `renderRadioInput?: (props: RadioInputProps) => ReactNode` - Custom radio renderer
|
|
108
109
|
- `renderCheckboxInput?: (props: CheckboxInputProps) => ReactNode` - Custom checkbox renderer
|
|
109
110
|
|
|
110
|
-
**Supported Types:** `
|
|
111
|
+
**Supported Types:** `coding`, `boolean`, `integer`, `decimal`, `string`, `text`, `quantity`
|
|
112
|
+
|
|
113
|
+
### Choice Questions with Custom Text Answers
|
|
114
|
+
|
|
115
|
+
Choice fields **can** optionally support custom user text input alongside coded options when you set `answerConstraint: "optionsOrString"` in the FHIR questionnaire definition:
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"linkId": "referral-source",
|
|
120
|
+
"text": "How did you hear about us?",
|
|
121
|
+
"type": "coding",
|
|
122
|
+
"answerConstraint": "optionsOrString",
|
|
123
|
+
"answerOption": [
|
|
124
|
+
{ "valueCoding": { "code": "search", "display": "Search engine" } },
|
|
125
|
+
{ "valueCoding": { "code": "social", "display": "Social media" } }
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
This renders coded options with an "Other" text field. For single-select (`repeats: false`), selecting a coded option or entering text is mutually exclusive. For multi-select (`repeats: true`), coded selections and free text coexist in the answer array.
|
|
111
131
|
|
|
112
132
|
### BmiForm
|
|
113
133
|
|
|
@@ -168,6 +188,69 @@ function MyComponent() {
|
|
|
168
188
|
}
|
|
169
189
|
```
|
|
170
190
|
|
|
191
|
+
### Quantity Questions with Unit Selection
|
|
192
|
+
|
|
193
|
+
Quantity questions allow users to enter numeric values with unit selection. Use the standard FHIR `questionnaire-unitOption` extension to define available units:
|
|
194
|
+
|
|
195
|
+
```tsx
|
|
196
|
+
const waistCircumferenceItem = {
|
|
197
|
+
linkId: "waist",
|
|
198
|
+
type: "quantity",
|
|
199
|
+
text: "What is your waist circumference?",
|
|
200
|
+
required: false,
|
|
201
|
+
code: [
|
|
202
|
+
{
|
|
203
|
+
system: "http://loinc.org",
|
|
204
|
+
code: "8280-0",
|
|
205
|
+
display: "Waist circumference at umbilicus by tape measure",
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
extension: [
|
|
209
|
+
{
|
|
210
|
+
url: "http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption",
|
|
211
|
+
valueCoding: {
|
|
212
|
+
system: "http://unitsofmeasure.org",
|
|
213
|
+
code: "cm",
|
|
214
|
+
display: "cm",
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
url: "http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption",
|
|
219
|
+
valueCoding: {
|
|
220
|
+
system: "http://unitsofmeasure.org",
|
|
221
|
+
code: "[in_i]",
|
|
222
|
+
display: "in",
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
};
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**Features:**
|
|
230
|
+
|
|
231
|
+
- Automatic unit toggle when multiple units are defined
|
|
232
|
+
- Single unit display when only one unit is provided
|
|
233
|
+
- Fallback to simple decimal input when no units are configured
|
|
234
|
+
- Value automatically clears when switching units
|
|
235
|
+
|
|
236
|
+
**Response Format:**
|
|
237
|
+
|
|
238
|
+
```json
|
|
239
|
+
{
|
|
240
|
+
"linkId": "waist",
|
|
241
|
+
"answer": [
|
|
242
|
+
{
|
|
243
|
+
"valueQuantity": {
|
|
244
|
+
"value": 85,
|
|
245
|
+
"unit": "cm",
|
|
246
|
+
"system": "http://unitsofmeasure.org",
|
|
247
|
+
"code": "cm"
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
]
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
171
254
|
### LegalConsentForm
|
|
172
255
|
|
|
173
256
|
A self-contained form component for collecting user consent before data submission. This component handles:
|
|
@@ -311,6 +394,112 @@ Override CSS custom properties:
|
|
|
311
394
|
}
|
|
312
395
|
```
|
|
313
396
|
|
|
397
|
+
### Choice Layout
|
|
398
|
+
|
|
399
|
+
Choice-based questions (`coding`, `choice`, `boolean`) support two layout modes via the `choiceLayout` prop:
|
|
400
|
+
|
|
401
|
+
| Mode | Class applied | Behavior |
|
|
402
|
+
|------|---------------|----------|
|
|
403
|
+
| `"stacked"` (default) | `.wq-choice-layout-stacked` | Vertical column, one option per row |
|
|
404
|
+
| `"inline-wrap"` | `.wq-choice-layout-inline-wrap` | Horizontal chip layout that wraps |
|
|
405
|
+
|
|
406
|
+
```tsx
|
|
407
|
+
<QuestionRenderer
|
|
408
|
+
item={item}
|
|
409
|
+
choiceLayout="inline-wrap"
|
|
410
|
+
/>
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Both modes apply to single-select, multi-select (`repeats: true`), and boolean questions. Existing consumers that don't set `choiceLayout` see no change (defaults to `"stacked"`).
|
|
414
|
+
|
|
415
|
+
#### Chip Design Tokens
|
|
416
|
+
|
|
417
|
+
When using `inline-wrap`, these tokens control chip appearance:
|
|
418
|
+
|
|
419
|
+
| Token | Default | Purpose |
|
|
420
|
+
|-------|---------|---------|
|
|
421
|
+
| `--wq-choice-chip-radius` | `9999px` (pill) | Border radius |
|
|
422
|
+
| `--wq-choice-chip-padding-x` | `1rem` | Horizontal padding |
|
|
423
|
+
| `--wq-choice-chip-padding-y` | `0.5rem` | Vertical padding |
|
|
424
|
+
| `--wq-choice-chip-gap` | `0.5rem` | Gap between chips |
|
|
425
|
+
|
|
426
|
+
#### CSS Class Contract
|
|
427
|
+
|
|
428
|
+
The choice container always emits:
|
|
429
|
+
|
|
430
|
+
- `.wq-question-choice` — base container
|
|
431
|
+
- `.wq-choice-layout-stacked` or `.wq-choice-layout-inline-wrap` — layout variant
|
|
432
|
+
- Your `choiceClassName` value (if provided)
|
|
433
|
+
|
|
434
|
+
State classes on individual options remain unchanged: `.wq-selected`, `.wq-disabled`.
|
|
435
|
+
|
|
436
|
+
#### Quick Setup: Dark Chip Theme
|
|
437
|
+
|
|
438
|
+
To get a dark-themed chip-style UI (horizontal wrapping options with strong selected state), combine the layout prop with token overrides and a small scoped CSS file.
|
|
439
|
+
|
|
440
|
+
**1) Render with layout and a shared choice class:**
|
|
441
|
+
|
|
442
|
+
```tsx
|
|
443
|
+
<QuestionRenderer
|
|
444
|
+
item={item}
|
|
445
|
+
choiceLayout="inline-wrap"
|
|
446
|
+
choiceClassName="questionnaire-choice"
|
|
447
|
+
/>
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
**2) Set theme tokens (global CSS):**
|
|
451
|
+
|
|
452
|
+
```css
|
|
453
|
+
:root {
|
|
454
|
+
--wq-color-surface: hsl(246 65% 10%);
|
|
455
|
+
--wq-color-border: hsl(234 50% 20%);
|
|
456
|
+
--wq-color-border-hover: hsl(214 98% 52%);
|
|
457
|
+
--wq-color-border-focus: hsl(214 98% 52%);
|
|
458
|
+
--wq-color-text-primary: hsl(220 20% 95%);
|
|
459
|
+
--wq-color-text-secondary: hsl(220 15% 60%);
|
|
460
|
+
--wq-color-selected: hsl(214 98% 52%);
|
|
461
|
+
--wq-color-selected-border: hsl(214 98% 52%);
|
|
462
|
+
|
|
463
|
+
--wq-choice-chip-radius: 14px;
|
|
464
|
+
--wq-choice-chip-padding-x: 1rem;
|
|
465
|
+
--wq-choice-chip-padding-y: 0.625rem;
|
|
466
|
+
--wq-choice-chip-gap: 0.5rem;
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
**3) Add scoped brand overrides (e.g. `CustomInputs.css`):**
|
|
471
|
+
|
|
472
|
+
```css
|
|
473
|
+
.wq-question-choice.questionnaire-choice .wq-choice-option.wq-selected {
|
|
474
|
+
background: var(--wq-color-selected);
|
|
475
|
+
border-color: var(--wq-color-selected-border);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
.wq-question-choice.questionnaire-choice .wq-choice-label {
|
|
479
|
+
overflow-wrap: anywhere;
|
|
480
|
+
}
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
**4) (Optional) Hide native radio/checkbox visuals:**
|
|
484
|
+
|
|
485
|
+
Keep inputs in the DOM for accessibility; hide only the visual markers for pure chips:
|
|
486
|
+
|
|
487
|
+
```css
|
|
488
|
+
.wq-question-choice.questionnaire-choice input[type="radio"],
|
|
489
|
+
.wq-question-choice.questionnaire-choice input[type="checkbox"] {
|
|
490
|
+
position: absolute;
|
|
491
|
+
inline-size: 1px;
|
|
492
|
+
block-size: 1px;
|
|
493
|
+
opacity: 0;
|
|
494
|
+
pointer-events: none;
|
|
495
|
+
}
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
**Notes:**
|
|
499
|
+
- Keep overrides scoped to your `choiceClassName` to avoid unintended global changes.
|
|
500
|
+
- Use package state classes (`.wq-selected`, `.wq-disabled`) instead of custom state class names.
|
|
501
|
+
- This setup works consistently for single-choice, multi-select, and boolean question types.
|
|
502
|
+
|
|
314
503
|
### Custom Input Renderers
|
|
315
504
|
|
|
316
505
|
```tsx
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import type { QuestionnaireItem } from "../types/fhir.js";
|
|
3
|
-
import { RadioInputProps, CheckboxInputProps, HelperTriggerProps } from "../types/index.js";
|
|
3
|
+
import { ChoiceLayout, RadioInputProps, CheckboxInputProps, HelperTriggerProps } from "../types/index.js";
|
|
4
4
|
export interface QuestionRendererProps {
|
|
5
5
|
item: QuestionnaireItem;
|
|
6
6
|
/**
|
|
@@ -18,6 +18,13 @@ export interface QuestionRendererProps {
|
|
|
18
18
|
* Will be appended to the default wq-choice-option class
|
|
19
19
|
*/
|
|
20
20
|
choiceClassName?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Layout mode for choice-based questions (coding, choice,
|
|
23
|
+
* boolean).
|
|
24
|
+
* - `"stacked"` (default): vertical column, one option per row.
|
|
25
|
+
* - `"inline-wrap"`: horizontal chip layout that wraps.
|
|
26
|
+
*/
|
|
27
|
+
choiceLayout?: ChoiceLayout;
|
|
21
28
|
/**
|
|
22
29
|
* Custom renderer for radio button inputs.
|
|
23
30
|
* When provided, this function will be called to render each radio option,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"question-renderer.d.ts","sourceRoot":"","sources":["../../../src/components/question-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"question-renderer.d.ts","sourceRoot":"","sources":["../../../src/components/question-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAmB1D,OAAO,EACL,YAAY,EACZ,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EACnB,MAAM,mBAAmB,CAAC;AAE3B,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,iBAAiB,CAAC;IACxB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,SAAS,CAAC;IACzD;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,SAAS,CAAC;IAC/D;;;OAGG;IACH,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,SAAS,CAAC;CAChE;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,GAAI,eAAe,qBAAqB,4CAyBpE,CAAC"}
|
|
@@ -10,6 +10,7 @@ import { IntegerQuestion } from "./questions/integer-question.js";
|
|
|
10
10
|
import { DecimalQuestion, getDecimalHelperTriggerProps, } from "./questions/decimal-question.js";
|
|
11
11
|
import { StringQuestion } from "./questions/string-question.js";
|
|
12
12
|
import { BooleanQuestion } from "./questions/boolean-question.js";
|
|
13
|
+
import { QuantityQuestion } from "./questions/quantity-question.js";
|
|
13
14
|
/**
|
|
14
15
|
* Wrapper component that combines the library's QuestionRenderer with debug functionality.
|
|
15
16
|
*
|
|
@@ -38,7 +39,7 @@ export const QuestionRenderer = (rendererProps) => {
|
|
|
38
39
|
* - Auto-populated (hidden) fields
|
|
39
40
|
* - Validation error display
|
|
40
41
|
*/
|
|
41
|
-
const QuestionRendererInternal = ({ item, className = "", inputClassName = "", choiceClassName = "", renderRadioInput, renderCheckboxInput, renderHelperTrigger, }) => {
|
|
42
|
+
const QuestionRendererInternal = ({ item, className = "", inputClassName = "", choiceClassName = "", choiceLayout = "stacked", renderRadioInput, renderCheckboxInput, renderHelperTrigger, }) => {
|
|
42
43
|
const { updateAnswer, updateMultipleAnswers, getAnswer, getAnswers, hasValidationError, } = useQuestionnaire();
|
|
43
44
|
const currentAnswer = getAnswer(item.linkId);
|
|
44
45
|
const currentAnswers = getAnswers(item.linkId);
|
|
@@ -146,6 +147,24 @@ const QuestionRendererInternal = ({ item, className = "", inputClassName = "", c
|
|
|
146
147
|
updateAnswer(item.linkId, { valueDecimal: numValue });
|
|
147
148
|
}
|
|
148
149
|
};
|
|
150
|
+
const handleQuantityChange = (value, unit, system, code) => {
|
|
151
|
+
// Allow clearing the controlled input
|
|
152
|
+
if (value === "") {
|
|
153
|
+
updateAnswer(item.linkId, {});
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const numValue = parseFloat(value);
|
|
157
|
+
if (!isNaN(numValue)) {
|
|
158
|
+
updateAnswer(item.linkId, {
|
|
159
|
+
valueQuantity: {
|
|
160
|
+
value: numValue,
|
|
161
|
+
unit,
|
|
162
|
+
system,
|
|
163
|
+
code,
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
};
|
|
149
168
|
const handleStringChange = (value) => {
|
|
150
169
|
updateAnswer(item.linkId, { valueString: value });
|
|
151
170
|
};
|
|
@@ -165,26 +184,71 @@ const QuestionRendererInternal = ({ item, className = "", inputClassName = "", c
|
|
|
165
184
|
return { minValue, maxValue, step, unit };
|
|
166
185
|
};
|
|
167
186
|
const sliderConfig = getSliderConfig();
|
|
187
|
+
/**
|
|
188
|
+
* Handle free-text "Other" input for coding questions with answerConstraint.
|
|
189
|
+
*
|
|
190
|
+
* Single-select (repeats=false): writing text clears any coded selection
|
|
191
|
+
* (valueString and valueCoding are mutually exclusive per FHIR spec).
|
|
192
|
+
*
|
|
193
|
+
* Multi-select (repeats=true): the free-text answer coexists with coded answers.
|
|
194
|
+
*/
|
|
195
|
+
const handleOtherTextChange = (value) => {
|
|
196
|
+
if (item.repeats) {
|
|
197
|
+
// Multi-select: keep all coded answers, replace free-text
|
|
198
|
+
const codedAnswers = currentAnswers.filter((a) => a.valueCoding !== undefined);
|
|
199
|
+
if (value) {
|
|
200
|
+
updateMultipleAnswers(item.linkId, [
|
|
201
|
+
...codedAnswers,
|
|
202
|
+
{ valueString: value },
|
|
203
|
+
]);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
updateMultipleAnswers(item.linkId, codedAnswers);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
// Single-select: free-text replaces any coded answer
|
|
211
|
+
if (value) {
|
|
212
|
+
updateAnswer(item.linkId, { valueString: value });
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
updateAnswer(item.linkId, {});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
};
|
|
168
219
|
const renderQuestion = () => {
|
|
169
220
|
switch (item.type) {
|
|
170
|
-
// FHIR
|
|
171
|
-
|
|
221
|
+
// FHIR R5 'coding' type for coded answers
|
|
222
|
+
// When answerConstraint="optionsOrString", allows free-text "other" input
|
|
172
223
|
case "coding":
|
|
224
|
+
// @deprecated Legacy FHIR R4 types — use "coding" instead
|
|
225
|
+
// "open-choice" is equivalent to "coding" with answerConstraint="optionsOrString"
|
|
226
|
+
// eslint-disable-next-line no-fallthrough
|
|
227
|
+
case "choice":
|
|
228
|
+
case "open-choice": {
|
|
229
|
+
// Normalize R4 "open-choice" → answerConstraint="optionsOrString"
|
|
230
|
+
const normalizedItem = item.type === "open-choice" &&
|
|
231
|
+
item.answerConstraint !== "optionsOrString"
|
|
232
|
+
? { ...item, answerConstraint: "optionsOrString" }
|
|
233
|
+
: item;
|
|
173
234
|
// Multi-select with checkboxes when repeats is true
|
|
174
235
|
if (item.repeats) {
|
|
175
|
-
return (_jsx(MultipleChoiceQuestion, { item:
|
|
236
|
+
return (_jsx(MultipleChoiceQuestion, { item: normalizedItem, currentAnswers: currentAnswers, onMultipleChoiceToggle: handleMultipleChoiceToggle, onOtherTextChange: handleOtherTextChange, choiceClassName: choiceClassName, choiceLayout: choiceLayout, inputClassName: inputClassName, renderCheckboxInput: renderCheckboxInput }));
|
|
176
237
|
}
|
|
177
238
|
// Single-select with radio buttons (default)
|
|
178
|
-
return (_jsx(ChoiceQuestion, { item:
|
|
239
|
+
return (_jsx(ChoiceQuestion, { item: normalizedItem, currentAnswer: currentAnswer, onChoiceChange: handleChoiceChange, onOtherTextChange: handleOtherTextChange, choiceClassName: choiceClassName, choiceLayout: choiceLayout, inputClassName: inputClassName, renderRadioInput: renderRadioInput }));
|
|
240
|
+
}
|
|
179
241
|
case "integer":
|
|
180
242
|
return (_jsx(IntegerQuestion, { item: item, currentAnswer: currentAnswer, onIntegerChange: handleIntegerChange, inputClassName: inputClassName, sliderConfig: sliderConfig }));
|
|
181
243
|
case "decimal":
|
|
182
244
|
return (_jsx(DecimalQuestion, { item: item, currentAnswer: currentAnswer, onDecimalChange: handleDecimalChange, inputClassName: inputClassName }));
|
|
245
|
+
case "quantity":
|
|
246
|
+
return (_jsx(QuantityQuestion, { item: item, currentAnswer: currentAnswer, onQuantityChange: handleQuantityChange, inputClassName: inputClassName }));
|
|
183
247
|
case "string":
|
|
184
248
|
case "text":
|
|
185
249
|
return (_jsx(StringQuestion, { item: item, currentAnswer: currentAnswer, onStringChange: handleStringChange, inputClassName: inputClassName }));
|
|
186
250
|
case "boolean":
|
|
187
|
-
return (_jsx(BooleanQuestion, { item: item, currentAnswer: currentAnswer, onBooleanChange: handleBooleanChange, choiceClassName: choiceClassName, renderRadioInput: renderRadioInput }));
|
|
251
|
+
return (_jsx(BooleanQuestion, { item: item, currentAnswer: currentAnswer, onBooleanChange: handleBooleanChange, choiceClassName: choiceClassName, choiceLayout: choiceLayout, renderRadioInput: renderRadioInput }));
|
|
188
252
|
default:
|
|
189
253
|
return (_jsxs("div", { className: "wq-unsupported-type", children: ["Unsupported question type: ", item.type] }));
|
|
190
254
|
}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import type { QuestionnaireItem, QuestionnaireResponseAnswer } from "../../types/fhir.js";
|
|
3
|
-
import { RadioInputProps } from "@/types/index.js";
|
|
3
|
+
import { ChoiceLayout, RadioInputProps } from "@/types/index.js";
|
|
4
4
|
export interface BooleanQuestionProps {
|
|
5
5
|
item: QuestionnaireItem;
|
|
6
6
|
currentAnswer?: QuestionnaireResponseAnswer;
|
|
7
7
|
onBooleanChange: (value: boolean) => void;
|
|
8
8
|
choiceClassName?: string;
|
|
9
|
+
choiceLayout?: ChoiceLayout;
|
|
9
10
|
renderRadioInput?: (props: RadioInputProps) => ReactNode;
|
|
10
11
|
}
|
|
11
12
|
/**
|
|
12
13
|
* Renders a boolean question with Yes/No radio buttons
|
|
13
14
|
*/
|
|
14
|
-
export declare const BooleanQuestion: ({ item, currentAnswer, onBooleanChange, choiceClassName, renderRadioInput, }: BooleanQuestionProps) => import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
export declare const BooleanQuestion: ({ item, currentAnswer, onBooleanChange, choiceClassName, choiceLayout, renderRadioInput, }: BooleanQuestionProps) => import("react/jsx-runtime").JSX.Element;
|
|
15
16
|
//# sourceMappingURL=boolean-question.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"boolean-question.d.ts","sourceRoot":"","sources":["../../../../src/components/questions/boolean-question.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EACV,iBAAiB,EACjB,2BAA2B,EAC5B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"boolean-question.d.ts","sourceRoot":"","sources":["../../../../src/components/questions/boolean-question.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EACV,iBAAiB,EACjB,2BAA2B,EAC5B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEjE,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,iBAAiB,CAAC;IACxB,aAAa,CAAC,EAAE,2BAA2B,CAAC;IAC5C,eAAe,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IAC1C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,SAAS,CAAC;CAC1D;AAED;;GAEG;AACH,eAAO,MAAM,eAAe,GAAI,4FAO7B,oBAAoB,4CAoDtB,CAAC"}
|
|
@@ -2,8 +2,8 @@ import { Fragment as _Fragment, jsxs as _jsxs, jsx as _jsx } from "react/jsx-run
|
|
|
2
2
|
/**
|
|
3
3
|
* Renders a boolean question with Yes/No radio buttons
|
|
4
4
|
*/
|
|
5
|
-
export const BooleanQuestion = ({ item, currentAnswer, onBooleanChange, choiceClassName = "", renderRadioInput, }) => {
|
|
6
|
-
return (_jsx("div", { className: `wq-question-choice ${choiceClassName}`, children: renderRadioInput ? (_jsxs(_Fragment, { children: [renderRadioInput({
|
|
5
|
+
export const BooleanQuestion = ({ item, currentAnswer, onBooleanChange, choiceClassName = "", choiceLayout = "stacked", renderRadioInput, }) => {
|
|
6
|
+
return (_jsx("div", { className: `wq-question-choice wq-choice-layout-${choiceLayout} ${choiceClassName}`, children: renderRadioInput ? (_jsxs(_Fragment, { children: [renderRadioInput({
|
|
7
7
|
linkId: item.linkId,
|
|
8
8
|
checked: currentAnswer?.valueBoolean === true,
|
|
9
9
|
onChange: () => onBooleanChange(true),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import type { QuestionnaireItem, QuestionnaireResponseAnswer } from "../../types/fhir.js";
|
|
3
|
-
import { RadioInputProps } from "@/types/index.js";
|
|
3
|
+
import { ChoiceLayout, RadioInputProps } from "@/types/index.js";
|
|
4
4
|
export interface ChoiceQuestionProps {
|
|
5
5
|
item: QuestionnaireItem;
|
|
6
6
|
currentAnswer?: QuestionnaireResponseAnswer;
|
|
@@ -10,10 +10,17 @@ export interface ChoiceQuestionProps {
|
|
|
10
10
|
display?: string;
|
|
11
11
|
}, valueInteger?: number) => void;
|
|
12
12
|
choiceClassName?: string;
|
|
13
|
+
choiceLayout?: ChoiceLayout;
|
|
14
|
+
inputClassName?: string;
|
|
13
15
|
renderRadioInput?: (props: RadioInputProps) => ReactNode;
|
|
16
|
+
onOtherTextChange?: (value: string) => void;
|
|
14
17
|
}
|
|
15
18
|
/**
|
|
16
|
-
* Renders a single-select choice question with radio buttons
|
|
19
|
+
* Renders a single-select choice question with radio buttons.
|
|
20
|
+
*
|
|
21
|
+
* FHIR R5: When answerConstraint="optionsOrString", displays an optional
|
|
22
|
+
* "Other" text field alongside coded options. Single-select means selecting
|
|
23
|
+
* a coded option or entering text is mutually exclusive (per FHIR spec).
|
|
17
24
|
*/
|
|
18
|
-
export declare const ChoiceQuestion: ({ item, currentAnswer, onChoiceChange, choiceClassName, renderRadioInput, }: ChoiceQuestionProps) => import("react/jsx-runtime").JSX.Element;
|
|
25
|
+
export declare const ChoiceQuestion: ({ item, currentAnswer, onChoiceChange, choiceClassName, choiceLayout, inputClassName, renderRadioInput, onOtherTextChange, }: ChoiceQuestionProps) => import("react/jsx-runtime").JSX.Element;
|
|
19
26
|
//# sourceMappingURL=choice-question.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"choice-question.d.ts","sourceRoot":"","sources":["../../../../src/components/questions/choice-question.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EACV,iBAAiB,EACjB,2BAA2B,EAC5B,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"choice-question.d.ts","sourceRoot":"","sources":["../../../../src/components/questions/choice-question.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EACV,iBAAiB,EACjB,2BAA2B,EAC5B,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAGjE,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,iBAAiB,CAAC;IACxB,aAAa,CAAC,EAAE,2BAA2B,CAAC;IAC5C,cAAc,EAAE,CACd,WAAW,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,EACjE,YAAY,CAAC,EAAE,MAAM,KAClB,IAAI,CAAC;IACV,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,SAAS,CAAC;IACzD,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7C;AAED;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,GAAI,8HAS5B,mBAAmB,4CAyFrB,CAAC"}
|
|
@@ -1,31 +1,39 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { getAnswerOptionMedia } from "../../lib/questionnaire-utils.js";
|
|
3
3
|
import { MediaAttachment } from "../media-attachment.js";
|
|
4
|
+
import { OtherTextInput } from "./other-text-input.js";
|
|
4
5
|
/**
|
|
5
|
-
* Renders a single-select choice question with radio buttons
|
|
6
|
+
* Renders a single-select choice question with radio buttons.
|
|
7
|
+
*
|
|
8
|
+
* FHIR R5: When answerConstraint="optionsOrString", displays an optional
|
|
9
|
+
* "Other" text field alongside coded options. Single-select means selecting
|
|
10
|
+
* a coded option or entering text is mutually exclusive (per FHIR spec).
|
|
6
11
|
*/
|
|
7
|
-
export const ChoiceQuestion = ({ item, currentAnswer, onChoiceChange, choiceClassName = "", renderRadioInput, }) => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
export const ChoiceQuestion = ({ item, currentAnswer, onChoiceChange, choiceClassName = "", choiceLayout = "stacked", inputClassName = "", renderRadioInput, onOtherTextChange, }) => {
|
|
13
|
+
// Check if this item allows free-text "other" answers (FHIR R5)
|
|
14
|
+
// https://hl7.org/fhir/valueset-questionnaire-answer-constraint.html#expansion
|
|
15
|
+
const allowsOtherText = item.answerConstraint === "optionsOrString";
|
|
16
|
+
return (_jsxs("div", { className: `wq-question-choice wq-choice-layout-${choiceLayout} ${choiceClassName}`, children: [item.answerOption?.map((option, index) => {
|
|
17
|
+
const isSelected = currentAnswer?.valueCoding?.code === option.valueCoding?.code;
|
|
18
|
+
// Get media attachment for this answer option
|
|
19
|
+
const mediaAttachment = getAnswerOptionMedia(option);
|
|
20
|
+
// Use custom renderer if provided
|
|
21
|
+
if (renderRadioInput) {
|
|
22
|
+
return (_jsxs("div", { className: "wq-choice-option-wrapper", children: [mediaAttachment && (_jsx(MediaAttachment, { attachment: mediaAttachment, alt: mediaAttachment.title ||
|
|
23
|
+
option.valueCoding?.display ||
|
|
24
|
+
`Option ${index + 1}`, className: "wq-choice-option-image" })), renderRadioInput({
|
|
25
|
+
linkId: item.linkId,
|
|
26
|
+
valueCoding: option.valueCoding,
|
|
27
|
+
valueInteger: option.valueInteger,
|
|
28
|
+
checked: isSelected,
|
|
29
|
+
onChange: () => onChoiceChange(option.valueCoding || {}, option.valueInteger),
|
|
30
|
+
label: option.valueCoding?.display || "",
|
|
31
|
+
index,
|
|
32
|
+
})] }, index));
|
|
33
|
+
}
|
|
34
|
+
// Default rendering
|
|
14
35
|
return (_jsxs("div", { className: "wq-choice-option-wrapper", children: [mediaAttachment && (_jsx(MediaAttachment, { attachment: mediaAttachment, alt: mediaAttachment.title ||
|
|
15
36
|
option.valueCoding?.display ||
|
|
16
|
-
`Option ${index + 1}`, className: "wq-choice-option-image" })),
|
|
17
|
-
|
|
18
|
-
valueCoding: option.valueCoding,
|
|
19
|
-
valueInteger: option.valueInteger,
|
|
20
|
-
checked: isSelected,
|
|
21
|
-
onChange: () => onChoiceChange(option.valueCoding || {}, option.valueInteger),
|
|
22
|
-
label: option.valueCoding?.display || "",
|
|
23
|
-
index,
|
|
24
|
-
})] }, index));
|
|
25
|
-
}
|
|
26
|
-
// Default rendering
|
|
27
|
-
return (_jsxs("div", { className: "wq-choice-option-wrapper", children: [mediaAttachment && (_jsx(MediaAttachment, { attachment: mediaAttachment, alt: mediaAttachment.title ||
|
|
28
|
-
option.valueCoding?.display ||
|
|
29
|
-
`Option ${index + 1}`, className: "wq-choice-option-image" })), _jsxs("label", { className: `wq-choice-option ${isSelected ? "wq-selected" : ""}`, children: [_jsx("input", { type: "radio", name: item.linkId, value: option.valueCoding?.code, checked: isSelected, onChange: () => onChoiceChange(option.valueCoding || {}, option.valueInteger), "data-wq-input": "radio", "data-wq-selected": isSelected }), _jsx("span", { className: "wq-choice-label", children: option.valueCoding?.display })] })] }, index));
|
|
30
|
-
}) }));
|
|
37
|
+
`Option ${index + 1}`, className: "wq-choice-option-image" })), _jsxs("label", { className: `wq-choice-option ${isSelected ? "wq-selected" : ""}`, children: [_jsx("input", { type: "radio", name: item.linkId, value: option.valueCoding?.code, checked: isSelected, onChange: () => onChoiceChange(option.valueCoding || {}, option.valueInteger), "data-wq-input": "radio", "data-wq-selected": isSelected }), _jsx("span", { className: "wq-choice-label", children: option.valueCoding?.display })] })] }, index));
|
|
38
|
+
}), allowsOtherText && onOtherTextChange && (_jsx(OtherTextInput, { currentAnswer: currentAnswer, onTextChange: onOtherTextChange, inputClassName: inputClassName, isMultiSelect: false }))] }));
|
|
31
39
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import type { QuestionnaireItem, QuestionnaireResponseAnswer } from "../../types/fhir.js";
|
|
3
|
-
import { CheckboxInputProps } from "../../types/index.js";
|
|
3
|
+
import { CheckboxInputProps, ChoiceLayout } from "../../types/index.js";
|
|
4
4
|
export interface MultipleChoiceQuestionProps {
|
|
5
5
|
item: QuestionnaireItem;
|
|
6
6
|
currentAnswers: QuestionnaireResponseAnswer[];
|
|
@@ -10,11 +10,18 @@ export interface MultipleChoiceQuestionProps {
|
|
|
10
10
|
display?: string;
|
|
11
11
|
}, valueInteger?: number) => void;
|
|
12
12
|
choiceClassName?: string;
|
|
13
|
+
choiceLayout?: ChoiceLayout;
|
|
14
|
+
inputClassName?: string;
|
|
13
15
|
renderCheckboxInput?: (props: CheckboxInputProps) => ReactNode;
|
|
16
|
+
onOtherTextChange?: (value: string) => void;
|
|
14
17
|
}
|
|
15
18
|
/**
|
|
16
|
-
* Renders a multi-select choice question with checkboxes
|
|
17
|
-
* Supports exclusive options and maxAnswers limits
|
|
19
|
+
* Renders a multi-select choice question with checkboxes.
|
|
20
|
+
* Supports exclusive options and maxAnswers limits.
|
|
21
|
+
*
|
|
22
|
+
* FHIR R5: When answerConstraint="optionsOrString", displays an optional
|
|
23
|
+
* "Other" text field alongside coded options. Multi-select means both
|
|
24
|
+
* coded options and free-text can coexist in the answers array.
|
|
18
25
|
*/
|
|
19
|
-
export declare const MultipleChoiceQuestion: ({ item, currentAnswers, onMultipleChoiceToggle, choiceClassName, renderCheckboxInput, }: MultipleChoiceQuestionProps) => import("react/jsx-runtime").JSX.Element;
|
|
26
|
+
export declare const MultipleChoiceQuestion: ({ item, currentAnswers, onMultipleChoiceToggle, choiceClassName, choiceLayout, inputClassName, renderCheckboxInput, onOtherTextChange, }: MultipleChoiceQuestionProps) => import("react/jsx-runtime").JSX.Element;
|
|
20
27
|
//# sourceMappingURL=multiple-choice-question.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"multiple-choice-question.d.ts","sourceRoot":"","sources":["../../../../src/components/questions/multiple-choice-question.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EACV,iBAAiB,EACjB,2BAA2B,EAC5B,MAAM,qBAAqB,CAAC;AAI7B,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"multiple-choice-question.d.ts","sourceRoot":"","sources":["../../../../src/components/questions/multiple-choice-question.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EACV,iBAAiB,EACjB,2BAA2B,EAC5B,MAAM,qBAAqB,CAAC;AAI7B,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAGxE,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,iBAAiB,CAAC;IACxB,cAAc,EAAE,2BAA2B,EAAE,CAAC;IAC9C,sBAAsB,EAAE,CACtB,WAAW,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,EACjE,YAAY,CAAC,EAAE,MAAM,KAClB,IAAI,CAAC;IACV,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,SAAS,CAAC;IAC/D,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,GAAI,0IASpC,2BAA2B,4CA0I7B,CAAC"}
|
|
@@ -2,13 +2,27 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { WELSHARE_EXTENSIONS } from "../../lib/constants.js";
|
|
3
3
|
import { getAnswerOptionMedia } from "../../lib/questionnaire-utils.js";
|
|
4
4
|
import { MediaAttachment } from "../media-attachment.js";
|
|
5
|
+
import { OtherTextInput } from "./other-text-input.js";
|
|
5
6
|
/**
|
|
6
|
-
* Renders a multi-select choice question with checkboxes
|
|
7
|
-
* Supports exclusive options and maxAnswers limits
|
|
7
|
+
* Renders a multi-select choice question with checkboxes.
|
|
8
|
+
* Supports exclusive options and maxAnswers limits.
|
|
9
|
+
*
|
|
10
|
+
* FHIR R5: When answerConstraint="optionsOrString", displays an optional
|
|
11
|
+
* "Other" text field alongside coded options. Multi-select means both
|
|
12
|
+
* coded options and free-text can coexist in the answers array.
|
|
8
13
|
*/
|
|
9
|
-
export const MultipleChoiceQuestion = ({ item, currentAnswers, onMultipleChoiceToggle, choiceClassName = "", renderCheckboxInput, }) => {
|
|
14
|
+
export const MultipleChoiceQuestion = ({ item, currentAnswers, onMultipleChoiceToggle, choiceClassName = "", choiceLayout = "stacked", inputClassName = "", renderCheckboxInput, onOtherTextChange, }) => {
|
|
15
|
+
// Check if this item allows free-text "other" answers (FHIR R5)
|
|
16
|
+
// https://hl7.org/fhir/valueset-questionnaire-answer-constraint.html#expansion
|
|
17
|
+
const allowsOtherText = item.answerConstraint === "optionsOrString";
|
|
18
|
+
// Find the free-text answer for maxAnswers logic
|
|
19
|
+
const freeTextAnswer = currentAnswers.find((a) => a.valueString !== undefined && !a.valueCoding);
|
|
20
|
+
// Count only coded answers for maxAnswers logic
|
|
21
|
+
const codedAnswersCount = currentAnswers.filter((a) => a.valueCoding !== undefined).length;
|
|
10
22
|
const maxAnswers = item.maxAnswers || Number.MAX_SAFE_INTEGER;
|
|
11
|
-
|
|
23
|
+
// Account for free-text answer in max count
|
|
24
|
+
const totalCount = codedAnswersCount + (freeTextAnswer ? 1 : 0);
|
|
25
|
+
const atMaxAnswers = totalCount >= maxAnswers;
|
|
12
26
|
// Check if there's an exclusive option extension
|
|
13
27
|
const exclusiveOptionExt = item.extension?.find((ext) => ext.url === WELSHARE_EXTENSIONS.EXCLUSIVE_OPTION);
|
|
14
28
|
const exclusiveOptionCode = exclusiveOptionExt?.valueString;
|
|
@@ -19,7 +33,7 @@ export const MultipleChoiceQuestion = ({ item, currentAnswers, onMultipleChoiceT
|
|
|
19
33
|
const optionsToShow = exclusiveSelected
|
|
20
34
|
? item.answerOption?.filter((opt) => opt.valueCoding?.code === exclusiveOptionCode)
|
|
21
35
|
: item.answerOption;
|
|
22
|
-
return (_jsxs("div", { className: `wq-question-choice ${choiceClassName}`, children: [optionsToShow?.map((option, index) => {
|
|
36
|
+
return (_jsxs("div", { className: `wq-question-choice wq-choice-layout-${choiceLayout} ${choiceClassName}`, children: [optionsToShow?.map((option, index) => {
|
|
23
37
|
const isSelected = currentAnswers.some((answer) => answer.valueCoding?.code === option.valueCoding?.code);
|
|
24
38
|
const isDisabled = !isSelected && atMaxAnswers;
|
|
25
39
|
// Get media attachment for this answer option
|
|
@@ -43,5 +57,5 @@ export const MultipleChoiceQuestion = ({ item, currentAnswers, onMultipleChoiceT
|
|
|
43
57
|
return (_jsxs("div", { className: "wq-choice-option-wrapper", children: [mediaAttachment && (_jsx(MediaAttachment, { attachment: mediaAttachment, alt: mediaAttachment.title ||
|
|
44
58
|
option.valueCoding?.display ||
|
|
45
59
|
`Option ${index + 1}`, className: "wq-choice-option-image" })), _jsxs("label", { className: `wq-choice-option ${isSelected ? "wq-selected" : ""} ${isDisabled ? "wq-disabled" : ""}`, children: [_jsx("input", { type: "checkbox", name: item.linkId, value: option.valueCoding?.code, checked: isSelected, disabled: isDisabled, onChange: () => onMultipleChoiceToggle(option.valueCoding || {}, option.valueInteger), "data-wq-input": "checkbox", "data-wq-selected": isSelected }), _jsx("span", { className: "wq-choice-label", children: option.valueCoding?.display })] })] }, index));
|
|
46
|
-
}), item.maxAnswers && (_jsxs("div", { className: "wq-max-answers-hint", children: ["Selected: ",
|
|
60
|
+
}), item.maxAnswers && (_jsxs("div", { className: "wq-max-answers-hint", children: ["Selected: ", totalCount, " / ", item.maxAnswers] })), allowsOtherText && onOtherTextChange && (_jsx(OtherTextInput, { currentAnswers: currentAnswers, onTextChange: onOtherTextChange, inputClassName: inputClassName, isMultiSelect: true }))] }));
|
|
47
61
|
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { QuestionnaireResponseAnswer } from "../../types/fhir.js";
|
|
2
|
+
export interface OtherTextInputProps {
|
|
3
|
+
/**
|
|
4
|
+
* Current answer (for single-select) or answers array (for multi-select).
|
|
5
|
+
* Used to determine if free-text is active and what value to display.
|
|
6
|
+
*/
|
|
7
|
+
currentAnswer?: QuestionnaireResponseAnswer;
|
|
8
|
+
currentAnswers?: QuestionnaireResponseAnswer[];
|
|
9
|
+
/**
|
|
10
|
+
* Callback fired when the user types in the "Other" field.
|
|
11
|
+
*/
|
|
12
|
+
onTextChange: (value: string) => void;
|
|
13
|
+
/**
|
|
14
|
+
* Additional CSS class name for the input element.
|
|
15
|
+
*/
|
|
16
|
+
inputClassName?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Whether this is used in a multi-select context (repeats=true).
|
|
19
|
+
* Affects how we extract the free-text value.
|
|
20
|
+
*/
|
|
21
|
+
isMultiSelect?: boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Shared "Other" text input component for FHIR R5 coding questions
|
|
25
|
+
* with answerConstraint="optionsOrString".
|
|
26
|
+
*
|
|
27
|
+
* This component renders a labeled text field that allows users to enter
|
|
28
|
+
* free-text responses alongside coded options. It follows FHIR R5 semantics:
|
|
29
|
+
* - Single-select: free-text and coded options are mutually exclusive
|
|
30
|
+
* - Multi-select: free-text coexists with coded options
|
|
31
|
+
*
|
|
32
|
+
* Uses unified CSS classes from questionnaire-styles.css:
|
|
33
|
+
* - .wq-open-choice-other (container)
|
|
34
|
+
* - .wq-open-choice-other-label (label)
|
|
35
|
+
* - .wq-open-choice-other-input (input field)
|
|
36
|
+
* - .wq-open-choice-other-active (active state modifier)
|
|
37
|
+
*/
|
|
38
|
+
export declare const OtherTextInput: ({ currentAnswer, currentAnswers, onTextChange, inputClassName, isMultiSelect, }: OtherTextInputProps) => import("react/jsx-runtime").JSX.Element;
|
|
39
|
+
//# sourceMappingURL=other-text-input.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"other-text-input.d.ts","sourceRoot":"","sources":["../../../../src/components/questions/other-text-input.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAC;AAEvE,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,aAAa,CAAC,EAAE,2BAA2B,CAAC;IAC5C,cAAc,CAAC,EAAE,2BAA2B,EAAE,CAAC;IAC/C;;OAEG;IACH,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,cAAc,GAAI,iFAM5B,mBAAmB,4CAoCrB,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Shared "Other" text input component for FHIR R5 coding questions
|
|
4
|
+
* with answerConstraint="optionsOrString".
|
|
5
|
+
*
|
|
6
|
+
* This component renders a labeled text field that allows users to enter
|
|
7
|
+
* free-text responses alongside coded options. It follows FHIR R5 semantics:
|
|
8
|
+
* - Single-select: free-text and coded options are mutually exclusive
|
|
9
|
+
* - Multi-select: free-text coexists with coded options
|
|
10
|
+
*
|
|
11
|
+
* Uses unified CSS classes from questionnaire-styles.css:
|
|
12
|
+
* - .wq-open-choice-other (container)
|
|
13
|
+
* - .wq-open-choice-other-label (label)
|
|
14
|
+
* - .wq-open-choice-other-input (input field)
|
|
15
|
+
* - .wq-open-choice-other-active (active state modifier)
|
|
16
|
+
*/
|
|
17
|
+
export const OtherTextInput = ({ currentAnswer, currentAnswers = [], onTextChange, inputClassName = "", isMultiSelect = false, }) => {
|
|
18
|
+
// Extract free-text value based on context
|
|
19
|
+
let otherTextValue = "";
|
|
20
|
+
let hasCodedSelection = false;
|
|
21
|
+
if (isMultiSelect) {
|
|
22
|
+
// Multi-select: find free-text answer in array
|
|
23
|
+
const freeTextAnswer = currentAnswers.find((a) => a.valueString !== undefined && !a.valueCoding);
|
|
24
|
+
otherTextValue = freeTextAnswer?.valueString ?? "";
|
|
25
|
+
hasCodedSelection = currentAnswers.some((a) => a.valueCoding !== undefined);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
// Single-select: check current answer
|
|
29
|
+
otherTextValue = currentAnswer?.valueString ?? "";
|
|
30
|
+
hasCodedSelection = !!currentAnswer?.valueCoding;
|
|
31
|
+
}
|
|
32
|
+
// In single-select, the input is "active" when free-text is entered and no coded option is selected
|
|
33
|
+
const isActive = !isMultiSelect && !hasCodedSelection && !!otherTextValue;
|
|
34
|
+
return (_jsxs("div", { className: `wq-open-choice-other ${isActive ? "wq-open-choice-other-active" : ""}`, children: [_jsx("label", { className: "wq-open-choice-other-label", children: "Other" }), _jsx("input", { type: "text", className: `wq-question-input wq-open-choice-other-input ${inputClassName}`, value: otherTextValue, onChange: (e) => onTextChange(e.target.value), placeholder: "Enter other answer", "data-wq-input": "open-choice-text" })] }));
|
|
35
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { QuestionnaireItem, QuestionnaireResponseAnswer } from "../../types/fhir.js";
|
|
2
|
+
export interface QuantityQuestionProps {
|
|
3
|
+
item: QuestionnaireItem;
|
|
4
|
+
currentAnswer?: QuestionnaireResponseAnswer;
|
|
5
|
+
onQuantityChange: (value: string, unit: string, system: string, code: string) => void;
|
|
6
|
+
inputClassName?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Renders a quantity question with unit selection
|
|
10
|
+
* Supports the FHIR questionnaire-unitOption extension for defining available units
|
|
11
|
+
*/
|
|
12
|
+
export declare const QuantityQuestion: ({ item, currentAnswer, onQuantityChange, inputClassName, }: QuantityQuestionProps) => import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
//# sourceMappingURL=quantity-question.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quantity-question.d.ts","sourceRoot":"","sources":["../../../../src/components/questions/quantity-question.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,iBAAiB,EACjB,2BAA2B,EAC5B,MAAM,qBAAqB,CAAC;AAG7B,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,iBAAiB,CAAC;IACxB,aAAa,CAAC,EAAE,2BAA2B,CAAC;IAC5C,gBAAgB,EAAE,CAChB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,KACT,IAAI,CAAC;IACV,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAgCD;;;GAGG;AACH,eAAO,MAAM,gBAAgB,GAAI,4DAK9B,qBAAqB,4CA8GvB,CAAC"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from "react";
|
|
3
|
+
import { FHIR_EXTENSIONS } from "../../lib/constants.js";
|
|
4
|
+
/**
|
|
5
|
+
* Extract unit options from FHIR questionnaire-unitOption extensions
|
|
6
|
+
*/
|
|
7
|
+
const getUnitOptions = (item) => {
|
|
8
|
+
if (!item.extension)
|
|
9
|
+
return [];
|
|
10
|
+
const unitOptions = [];
|
|
11
|
+
for (const ext of item.extension) {
|
|
12
|
+
if (ext.url === FHIR_EXTENSIONS.QUESTIONNAIRE_UNIT_OPTION &&
|
|
13
|
+
ext.valueCoding) {
|
|
14
|
+
unitOptions.push({
|
|
15
|
+
display: ext.valueCoding.display || ext.valueCoding.code || "",
|
|
16
|
+
code: ext.valueCoding.code || "",
|
|
17
|
+
system: ext.valueCoding.system || "",
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return unitOptions;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Renders a quantity question with unit selection
|
|
25
|
+
* Supports the FHIR questionnaire-unitOption extension for defining available units
|
|
26
|
+
*/
|
|
27
|
+
export const QuantityQuestion = ({ item, currentAnswer, onQuantityChange, inputClassName = "", }) => {
|
|
28
|
+
const unitOptions = getUnitOptions(item);
|
|
29
|
+
const [selectedUnitIndex, setSelectedUnitIndex] = useState(0);
|
|
30
|
+
// Sync selected unit from existing answer
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (currentAnswer?.valueQuantity?.code && unitOptions.length > 0) {
|
|
33
|
+
const matchingIndex = unitOptions.findIndex((opt) => opt.code === currentAnswer.valueQuantity?.code);
|
|
34
|
+
if (matchingIndex !== -1) {
|
|
35
|
+
setSelectedUnitIndex(matchingIndex);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}, [currentAnswer?.valueQuantity?.code, unitOptions]);
|
|
39
|
+
// Default unit with proper bounds checking
|
|
40
|
+
const defaultUnit = { display: "", code: "", system: "" };
|
|
41
|
+
const currentUnit = unitOptions.length > 0 && selectedUnitIndex < unitOptions.length
|
|
42
|
+
? (unitOptions[selectedUnitIndex] ?? defaultUnit)
|
|
43
|
+
: defaultUnit;
|
|
44
|
+
// Get input value from current answer
|
|
45
|
+
const [inputValue, setInputValue] = useState(() => {
|
|
46
|
+
const qty = currentAnswer?.valueQuantity;
|
|
47
|
+
if (qty?.value !== undefined)
|
|
48
|
+
return String(qty.value);
|
|
49
|
+
return "";
|
|
50
|
+
});
|
|
51
|
+
const handleChange = (raw) => {
|
|
52
|
+
setInputValue(raw);
|
|
53
|
+
if (raw === "") {
|
|
54
|
+
onQuantityChange("", currentUnit?.display ?? "", currentUnit?.system ?? "", currentUnit?.code ?? "");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const num = parseFloat(raw);
|
|
58
|
+
if (!isNaN(num)) {
|
|
59
|
+
onQuantityChange(String(num), currentUnit?.display ?? "", currentUnit?.system ?? "", currentUnit?.code ?? "");
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const handleUnitChange = (index) => {
|
|
63
|
+
// Validate index bounds
|
|
64
|
+
if (index < 0 || index >= unitOptions.length)
|
|
65
|
+
return;
|
|
66
|
+
setSelectedUnitIndex(index);
|
|
67
|
+
// Clear the input when unit changes since the value is in different units
|
|
68
|
+
setInputValue("");
|
|
69
|
+
const newUnit = unitOptions[index];
|
|
70
|
+
if (newUnit) {
|
|
71
|
+
onQuantityChange("", newUnit.display, newUnit.system, newUnit.code);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
// If no unit options are defined, render as a simple decimal input
|
|
75
|
+
if (unitOptions.length === 0) {
|
|
76
|
+
return (_jsx("input", { type: "number", className: `wq-question-input ${inputClassName}`, value: currentAnswer?.valueQuantity?.value ?? "", onChange: (e) => onQuantityChange(e.target.value, "", "", ""), placeholder: "Enter a value", step: "0.1" }));
|
|
77
|
+
}
|
|
78
|
+
return (_jsxs("div", { className: "wq-quantity-question", children: [_jsxs("div", { className: "wq-quantity-input-row", children: [_jsx("input", { type: "number", className: `wq-question-input wq-quantity-input-field ${inputClassName}`, value: inputValue, onChange: (e) => handleChange(e.target.value), placeholder: `Enter value${currentUnit?.display ? ` in ${currentUnit.display}` : ""}`, step: "0.1" }), _jsx("span", { className: "wq-quantity-unit-label", children: currentUnit?.display ?? "" })] }), unitOptions.length > 1 && (_jsx("div", { className: "wq-quantity-unit-toggle", children: unitOptions.map((unit, i) => (_jsx("button", { type: "button", className: `wq-quantity-unit-option ${i === selectedUnitIndex ? "wq-selected" : ""}`, onClick: () => handleUnitChange(i), children: unit.display }, unit.code))) }))] }));
|
|
79
|
+
};
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
export { QuestionRenderer } from "./components/question-renderer.js";
|
|
2
2
|
export type { QuestionRendererProps } from "./components/question-renderer.js";
|
|
3
|
+
export { QuantityQuestion } from "./components/questions/quantity-question.js";
|
|
4
|
+
export type { QuantityQuestionProps } from "./components/questions/quantity-question.js";
|
|
3
5
|
export { BmiForm } from "./components/bmi-form.js";
|
|
4
6
|
export type { BmiFormProps } from "./components/bmi-form.js";
|
|
5
7
|
export { LegalConsentForm } from "./components/legal-consent-form.js";
|
|
6
8
|
export type { LegalConsentFormProps, LegalConsentResult, LegalCheckboxProps, LegalDocumentLinks, } from "./components/legal-consent-form.js";
|
|
7
9
|
export { QuestionnaireProvider, useQuestionnaire, type QuestionnaireContextType, type QuestionnaireProviderProps, } from "./contexts/questionnaire-context.js";
|
|
8
10
|
export type { Attachment, Coding, Extension, Questionnaire, QuestionnaireItem, QuestionnaireItemAnswerOption, QuestionnaireResponse, QuestionnaireResponseAnswer, QuestionnaireResponseItem, } from "./types/fhir.js";
|
|
9
|
-
export type { RadioInputProps, CheckboxInputProps, InputHelperConfig, HelperTriggerProps, } from "./types/index.js";
|
|
11
|
+
export type { ChoiceLayout, RadioInputProps, CheckboxInputProps, InputHelperConfig, HelperTriggerProps, } from "./types/index.js";
|
|
10
12
|
export { calculateProgress, findQuestionnaireItem, getAllQuestionsFromPage, getAnswerOptionMedia, getExclusiveOptionCode, getInputHelper, getItemMedia, getVisiblePages, hasAnswerValue, isQuestionHidden, } from "./lib/questionnaire-utils.js";
|
|
11
13
|
export { calculateBmi } from "./lib/bmi-helpers.js";
|
|
12
14
|
export { FHIR_EXTENSIONS, WELSHARE_EXTENSIONS, WELSHARE_CODE_SYSTEMS, FHIR_CODE_SYSTEMS, } from "./lib/constants.js";
|
package/dist/esm/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AACrE,YAAY,EAAE,qBAAqB,EAAE,MAAM,mCAAmC,CAAC;AAE/E,OAAO,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AACnD,YAAY,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAE7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;AACtE,YAAY,EACV,qBAAqB,EACrB,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,oCAAoC,CAAC;AAG5C,OAAO,EACL,qBAAqB,EACrB,gBAAgB,EAChB,KAAK,wBAAwB,EAC7B,KAAK,0BAA0B,GAChC,MAAM,qCAAqC,CAAC;AAG7C,YAAY,EACV,UAAU,EACV,MAAM,EACN,SAAS,EACT,aAAa,EACb,iBAAiB,EACjB,6BAA6B,EAC7B,qBAAqB,EACrB,2BAA2B,EAC3B,yBAAyB,GAC1B,MAAM,iBAAiB,CAAC;AAEzB,YAAY,EACV,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,uBAAuB,EACvB,oBAAoB,EACpB,sBAAsB,EACtB,cAAc,EACd,YAAY,EACZ,eAAe,EACf,cAAc,EACd,gBAAgB,GACjB,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAGpD,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,oBAAoB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AACrE,YAAY,EAAE,qBAAqB,EAAE,MAAM,mCAAmC,CAAC;AAE/E,OAAO,EAAE,gBAAgB,EAAE,MAAM,6CAA6C,CAAC;AAC/E,YAAY,EAAE,qBAAqB,EAAE,MAAM,6CAA6C,CAAC;AAEzF,OAAO,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AACnD,YAAY,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAE7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;AACtE,YAAY,EACV,qBAAqB,EACrB,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,oCAAoC,CAAC;AAG5C,OAAO,EACL,qBAAqB,EACrB,gBAAgB,EAChB,KAAK,wBAAwB,EAC7B,KAAK,0BAA0B,GAChC,MAAM,qCAAqC,CAAC;AAG7C,YAAY,EACV,UAAU,EACV,MAAM,EACN,SAAS,EACT,aAAa,EACb,iBAAiB,EACjB,6BAA6B,EAC7B,qBAAqB,EACrB,2BAA2B,EAC3B,yBAAyB,GAC1B,MAAM,iBAAiB,CAAC;AAEzB,YAAY,EACV,YAAY,EACZ,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,uBAAuB,EACvB,oBAAoB,EACpB,sBAAsB,EACtB,cAAc,EACd,YAAY,EACZ,eAAe,EACf,cAAc,EACd,gBAAgB,GACjB,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAGpD,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,oBAAoB,CAAC"}
|
package/dist/esm/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Components
|
|
2
2
|
export { QuestionRenderer } from "./components/question-renderer.js";
|
|
3
|
+
export { QuantityQuestion } from "./components/questions/quantity-question.js";
|
|
3
4
|
export { BmiForm } from "./components/bmi-form.js";
|
|
4
5
|
export { LegalConsentForm } from "./components/legal-consent-form.js";
|
|
5
6
|
// Contexts
|
|
@@ -21,6 +21,13 @@ export declare const FHIR_EXTENSIONS: {
|
|
|
21
21
|
* @see http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemAnswerMedia
|
|
22
22
|
*/
|
|
23
23
|
readonly ITEM_ANSWER_MEDIA: "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemAnswerMedia";
|
|
24
|
+
/**
|
|
25
|
+
* Standard FHIR extension for defining unit options for quantity questions
|
|
26
|
+
* Allows users to select from predefined units of measurement
|
|
27
|
+
* Type: valueCoding with system, code, and display for the unit
|
|
28
|
+
* @see http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption
|
|
29
|
+
*/
|
|
30
|
+
readonly QUESTIONNAIRE_UNIT_OPTION: "http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption";
|
|
24
31
|
};
|
|
25
32
|
/**
|
|
26
33
|
* Welshare Custom Extension URLs
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/lib/constants.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,eAAe;IAC1B;;;;OAIG;;IAIH;;;;OAIG;;IAIH;;;;OAIG;;CAGK,CAAC;AAEX;;;;GAIG;AACH,eAAO,MAAM,mBAAmB;IAC9B;;;;;;;;;;;OAWG;;IAIH;;;;;;;;;;;;;;;;;OAiBG;;IAIH;;;;;;;;;;;;;;;;OAgBG;;CAGK,CAAC;AAEX;;;GAGG;AACH,eAAO,MAAM,qBAAqB;IAChC;;;;OAIG;;CAEK,CAAC;AAEX;;;GAGG;AACH,eAAO,MAAM,iBAAiB;IAC5B;;;;OAIG;;CAEK,CAAC"}
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/lib/constants.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,eAAe;IAC1B;;;;OAIG;;IAIH;;;;OAIG;;IAIH;;;;OAIG;;IAIH;;;;;OAKG;;CAGK,CAAC;AAEX;;;;GAIG;AACH,eAAO,MAAM,mBAAmB;IAC9B;;;;;;;;;;;OAWG;;IAIH;;;;;;;;;;;;;;;;;OAiBG;;IAIH;;;;;;;;;;;;;;;;OAgBG;;CAGK,CAAC;AAEX;;;GAGG;AACH,eAAO,MAAM,qBAAqB;IAChC;;;;OAIG;;CAEK,CAAC;AAEX;;;GAGG;AACH,eAAO,MAAM,iBAAiB;IAC5B;;;;OAIG;;CAEK,CAAC"}
|
|
@@ -21,6 +21,13 @@ export const FHIR_EXTENSIONS = {
|
|
|
21
21
|
* @see http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemAnswerMedia
|
|
22
22
|
*/
|
|
23
23
|
ITEM_ANSWER_MEDIA: "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemAnswerMedia",
|
|
24
|
+
/**
|
|
25
|
+
* Standard FHIR extension for defining unit options for quantity questions
|
|
26
|
+
* Allows users to select from predefined units of measurement
|
|
27
|
+
* Type: valueCoding with system, code, and display for the unit
|
|
28
|
+
* @see http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption
|
|
29
|
+
*/
|
|
30
|
+
QUESTIONNAIRE_UNIT_OPTION: "http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption",
|
|
24
31
|
};
|
|
25
32
|
/**
|
|
26
33
|
* Welshare Custom Extension URLs
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout mode for choice-based questions.
|
|
3
|
+
* - `"stacked"`: Default column layout (one option per row).
|
|
4
|
+
* - `"inline-wrap"`: Horizontal chip layout that wraps to next line.
|
|
5
|
+
*/
|
|
6
|
+
export type ChoiceLayout = "stacked" | "inline-wrap";
|
|
1
7
|
/**
|
|
2
8
|
* Props for rendering a radio button option
|
|
3
9
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/types/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,WAAW,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACnE,iDAAiD;IACjD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,OAAO,EAAE,OAAO,CAAC;IACjB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,WAAW,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACnE,iDAAiD;IACjD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,OAAO,EAAE,OAAO,CAAC;IACjB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+BAA+B;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB,CAAC,CAAC,GAAG,MAAM,GAAG,MAAM;IACrD,kDAAkD;IAClD,MAAM,EAAE,iBAAiB,CAAC;IAC1B,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,YAAY,CAAC,EAAE,CAAC,CAAC;IACjB,0DAA0D;IAC1D,eAAe,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;CACrC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/types/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,aAAa,CAAC;AAErD;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,WAAW,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACnE,iDAAiD;IACjD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,OAAO,EAAE,OAAO,CAAC;IACjB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,WAAW,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACnE,iDAAiD;IACjD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,OAAO,EAAE,OAAO,CAAC;IACjB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+BAA+B;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB,CAAC,CAAC,GAAG,MAAM,GAAG,MAAM;IACrD,kDAAkD;IAClD,MAAM,EAAE,iBAAiB,CAAC;IAC1B,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,YAAY,CAAC,EAAE,CAAC,CAAC;IACjB,0DAA0D;IAC1D,eAAe,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;CACrC"}
|
package/dist/styles.css
CHANGED
|
@@ -99,6 +99,30 @@
|
|
|
99
99
|
font-weight: var(--wq-font-weight-medium);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/* === Inline-Wrap (Chip) Layout === */
|
|
103
|
+
.wq-question-choice.wq-choice-layout-inline-wrap {
|
|
104
|
+
flex-direction: row;
|
|
105
|
+
flex-wrap: wrap;
|
|
106
|
+
gap: var(--wq-choice-chip-gap);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.wq-question-choice.wq-choice-layout-inline-wrap
|
|
110
|
+
.wq-choice-option-wrapper {
|
|
111
|
+
max-width: 100%;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.wq-question-choice.wq-choice-layout-inline-wrap .wq-choice-option {
|
|
115
|
+
border-radius: var(--wq-choice-chip-radius);
|
|
116
|
+
padding: var(--wq-choice-chip-padding-y)
|
|
117
|
+
var(--wq-choice-chip-padding-x);
|
|
118
|
+
width: auto;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.wq-question-choice.wq-choice-layout-inline-wrap .wq-choice-label {
|
|
122
|
+
white-space: normal;
|
|
123
|
+
word-break: break-word;
|
|
124
|
+
}
|
|
125
|
+
|
|
102
126
|
.wq-max-answers-hint {
|
|
103
127
|
font-size: var(--wq-font-size-sm);
|
|
104
128
|
color: var(--wq-color-text-tertiary);
|
|
@@ -106,6 +130,65 @@
|
|
|
106
130
|
font-weight: var(--wq-font-weight-normal);
|
|
107
131
|
}
|
|
108
132
|
|
|
133
|
+
/* === Quantity Inputs === */
|
|
134
|
+
.wq-quantity-question {
|
|
135
|
+
display: flex;
|
|
136
|
+
flex-direction: column;
|
|
137
|
+
gap: var(--wq-space-md);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.wq-quantity-input-row {
|
|
141
|
+
display: flex;
|
|
142
|
+
align-items: center;
|
|
143
|
+
gap: var(--wq-space-sm);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.wq-quantity-input-field {
|
|
147
|
+
flex: 1;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.wq-quantity-unit-label {
|
|
151
|
+
font-size: var(--wq-font-size-base);
|
|
152
|
+
font-weight: var(--wq-font-weight-medium);
|
|
153
|
+
color: var(--wq-color-text-primary);
|
|
154
|
+
opacity: 0.7;
|
|
155
|
+
min-width: 2rem;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.wq-quantity-unit-toggle {
|
|
159
|
+
display: flex;
|
|
160
|
+
gap: 0;
|
|
161
|
+
border: var(--wq-border-width) solid var(--wq-color-border);
|
|
162
|
+
border-radius: var(--wq-radius-md);
|
|
163
|
+
overflow: hidden;
|
|
164
|
+
width: fit-content;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.wq-quantity-unit-option {
|
|
168
|
+
padding: var(--wq-space-sm) var(--wq-space-lg);
|
|
169
|
+
font-size: var(--wq-font-size-sm);
|
|
170
|
+
font-family: inherit;
|
|
171
|
+
background: var(--wq-color-surface);
|
|
172
|
+
color: var(--wq-color-text-primary);
|
|
173
|
+
border: none;
|
|
174
|
+
cursor: pointer;
|
|
175
|
+
transition: all var(--wq-transition-fast);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.wq-quantity-unit-option:not(:last-child) {
|
|
179
|
+
border-right: var(--wq-border-width) solid var(--wq-color-border);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.wq-quantity-unit-option.wq-selected {
|
|
183
|
+
background: var(--wq-color-primary);
|
|
184
|
+
color: white;
|
|
185
|
+
font-weight: var(--wq-font-weight-semibold);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.wq-quantity-unit-option:hover:not(.wq-selected) {
|
|
189
|
+
background-color: var(--wq-color-background);
|
|
190
|
+
}
|
|
191
|
+
|
|
109
192
|
/* === Text Inputs === */
|
|
110
193
|
.wq-question-input {
|
|
111
194
|
width: 100%;
|
|
@@ -248,6 +331,34 @@
|
|
|
248
331
|
font-weight: var(--wq-font-weight-medium);
|
|
249
332
|
}
|
|
250
333
|
|
|
334
|
+
/* === Open-Choice (coded options + free-text "Other") === */
|
|
335
|
+
.wq-open-choice {
|
|
336
|
+
display: flex;
|
|
337
|
+
flex-direction: column;
|
|
338
|
+
gap: var(--wq-space-md);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.wq-open-choice-other {
|
|
342
|
+
display: flex;
|
|
343
|
+
flex-direction: column;
|
|
344
|
+
gap: var(--wq-space-sm);
|
|
345
|
+
margin-top: var(--wq-space-sm);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.wq-open-choice-other-label {
|
|
349
|
+
font-size: var(--wq-font-size-sm);
|
|
350
|
+
font-weight: var(--wq-font-weight-medium);
|
|
351
|
+
color: var(--wq-color-text-secondary);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.wq-open-choice-other-active .wq-open-choice-other-label {
|
|
355
|
+
color: var(--wq-color-text-primary);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.wq-open-choice-other-input {
|
|
359
|
+
max-width: 24rem;
|
|
360
|
+
}
|
|
361
|
+
|
|
251
362
|
/* === Unsupported Type === */
|
|
252
363
|
.wq-unsupported-type {
|
|
253
364
|
padding: var(--wq-space-lg);
|
|
@@ -315,6 +426,12 @@
|
|
|
315
426
|
.wq-slider-value-display {
|
|
316
427
|
font-size: var(--wq-font-size-lg);
|
|
317
428
|
}
|
|
429
|
+
|
|
430
|
+
.wq-question-choice.wq-choice-layout-inline-wrap
|
|
431
|
+
.wq-choice-option {
|
|
432
|
+
padding: var(--wq-choice-chip-padding-y)
|
|
433
|
+
var(--wq-space-md);
|
|
434
|
+
}
|
|
318
435
|
}
|
|
319
436
|
|
|
320
437
|
/* === Print Styles === */
|
package/dist/tokens.css
CHANGED
|
@@ -107,6 +107,12 @@
|
|
|
107
107
|
--wq-choice-padding-x: var(--wq-space-lg);
|
|
108
108
|
--wq-choice-padding-y: var(--wq-space-md);
|
|
109
109
|
|
|
110
|
+
/* Chip tokens (used by inline-wrap layout) */
|
|
111
|
+
--wq-choice-chip-radius: var(--wq-radius-full);
|
|
112
|
+
--wq-choice-chip-padding-x: var(--wq-space-lg);
|
|
113
|
+
--wq-choice-chip-padding-y: var(--wq-space-sm);
|
|
114
|
+
--wq-choice-chip-gap: var(--wq-space-sm);
|
|
115
|
+
|
|
110
116
|
--wq-question-gap: var(--wq-space-xl);
|
|
111
117
|
|
|
112
118
|
--wq-slider-height: 0.5rem;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@welshare/questionnaire",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "FHIR Questionnaire components for React with state management and validation",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"storybook": "^10.1.11",
|
|
39
39
|
"@storybook/react": "^10.1.11",
|
|
40
40
|
"@storybook/react-vite": "^10.1.11",
|
|
41
|
-
"@welshare/sdk": "0.3.
|
|
41
|
+
"@welshare/sdk": "0.3.5",
|
|
42
42
|
"@workspace/eslint-config": "0.0.0",
|
|
43
43
|
"@workspace/typescript-config": "0.0.0"
|
|
44
44
|
},
|