@welshare/questionnaire 0.2.4 → 0.2.6
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 +84 -2
- package/dist/esm/components/question-renderer.d.ts.map +1 -1
- package/dist/esm/components/question-renderer.js +68 -4
- package/dist/esm/components/questions/choice-question.d.ts +8 -2
- 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 +9 -3
- package/dist/esm/components/questions/multiple-choice-question.d.ts.map +1 -1
- package/dist/esm/components/questions/multiple-choice-question.js +19 -5
- 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 +2 -0
- 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/styles.css +87 -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
|
|
|
@@ -107,7 +107,26 @@ const {
|
|
|
107
107
|
- `renderRadioInput?: (props: RadioInputProps) => ReactNode` - Custom radio renderer
|
|
108
108
|
- `renderCheckboxInput?: (props: CheckboxInputProps) => ReactNode` - Custom checkbox renderer
|
|
109
109
|
|
|
110
|
-
**Supported Types:** `
|
|
110
|
+
**Supported Types:** `coding`, `boolean`, `integer`, `decimal`, `string`, `text`, `quantity`
|
|
111
|
+
|
|
112
|
+
### Choice Questions with Custom Text Answers
|
|
113
|
+
|
|
114
|
+
Choice fields **can** optionally support custom user text input alongside coded options when you set `answerConstraint: "optionsOrString"` in the FHIR questionnaire definition:
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"linkId": "referral-source",
|
|
119
|
+
"text": "How did you hear about us?",
|
|
120
|
+
"type": "coding",
|
|
121
|
+
"answerConstraint": "optionsOrString",
|
|
122
|
+
"answerOption": [
|
|
123
|
+
{ "valueCoding": { "code": "search", "display": "Search engine" } },
|
|
124
|
+
{ "valueCoding": { "code": "social", "display": "Social media" } }
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
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
130
|
|
|
112
131
|
### BmiForm
|
|
113
132
|
|
|
@@ -168,6 +187,69 @@ function MyComponent() {
|
|
|
168
187
|
}
|
|
169
188
|
```
|
|
170
189
|
|
|
190
|
+
### Quantity Questions with Unit Selection
|
|
191
|
+
|
|
192
|
+
Quantity questions allow users to enter numeric values with unit selection. Use the standard FHIR `questionnaire-unitOption` extension to define available units:
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
const waistCircumferenceItem = {
|
|
196
|
+
linkId: "waist",
|
|
197
|
+
type: "quantity",
|
|
198
|
+
text: "What is your waist circumference?",
|
|
199
|
+
required: false,
|
|
200
|
+
code: [
|
|
201
|
+
{
|
|
202
|
+
system: "http://loinc.org",
|
|
203
|
+
code: "8280-0",
|
|
204
|
+
display: "Waist circumference at umbilicus by tape measure",
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
extension: [
|
|
208
|
+
{
|
|
209
|
+
url: "http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption",
|
|
210
|
+
valueCoding: {
|
|
211
|
+
system: "http://unitsofmeasure.org",
|
|
212
|
+
code: "cm",
|
|
213
|
+
display: "cm",
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
url: "http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption",
|
|
218
|
+
valueCoding: {
|
|
219
|
+
system: "http://unitsofmeasure.org",
|
|
220
|
+
code: "[in_i]",
|
|
221
|
+
display: "in",
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
};
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**Features:**
|
|
229
|
+
|
|
230
|
+
- Automatic unit toggle when multiple units are defined
|
|
231
|
+
- Single unit display when only one unit is provided
|
|
232
|
+
- Fallback to simple decimal input when no units are configured
|
|
233
|
+
- Value automatically clears when switching units
|
|
234
|
+
|
|
235
|
+
**Response Format:**
|
|
236
|
+
|
|
237
|
+
```json
|
|
238
|
+
{
|
|
239
|
+
"linkId": "waist",
|
|
240
|
+
"answer": [
|
|
241
|
+
{
|
|
242
|
+
"valueQuantity": {
|
|
243
|
+
"value": 85,
|
|
244
|
+
"unit": "cm",
|
|
245
|
+
"system": "http://unitsofmeasure.org",
|
|
246
|
+
"code": "cm"
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
]
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
171
253
|
### LegalConsentForm
|
|
172
254
|
|
|
173
255
|
A self-contained form component for collecting user consent before data submission. This component handles:
|
|
@@ -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,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;;;;;;;;;;;;;;;;;;;;;;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
|
*
|
|
@@ -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,21 +184,66 @@ 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, 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, 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 }));
|
|
@@ -10,10 +10,16 @@ export interface ChoiceQuestionProps {
|
|
|
10
10
|
display?: string;
|
|
11
11
|
}, valueInteger?: number) => void;
|
|
12
12
|
choiceClassName?: string;
|
|
13
|
+
inputClassName?: string;
|
|
13
14
|
renderRadioInput?: (props: RadioInputProps) => ReactNode;
|
|
15
|
+
onOtherTextChange?: (value: string) => void;
|
|
14
16
|
}
|
|
15
17
|
/**
|
|
16
|
-
* Renders a single-select choice question with radio buttons
|
|
18
|
+
* Renders a single-select choice question with radio buttons.
|
|
19
|
+
*
|
|
20
|
+
* FHIR R5: When answerConstraint="optionsOrString", displays an optional
|
|
21
|
+
* "Other" text field alongside coded options. Single-select means selecting
|
|
22
|
+
* a coded option or entering text is mutually exclusive (per FHIR spec).
|
|
17
23
|
*/
|
|
18
|
-
export declare const ChoiceQuestion: ({ item, currentAnswer, onChoiceChange, choiceClassName, renderRadioInput, }: ChoiceQuestionProps) => import("react/jsx-runtime").JSX.Element;
|
|
24
|
+
export declare const ChoiceQuestion: ({ item, currentAnswer, onChoiceChange, choiceClassName, inputClassName, renderRadioInput, onOtherTextChange, }: ChoiceQuestionProps) => import("react/jsx-runtime").JSX.Element;
|
|
19
25
|
//# 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,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAGnD,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,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,gHAQ5B,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 = "", 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 ${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
|
};
|
|
@@ -10,11 +10,17 @@ export interface MultipleChoiceQuestionProps {
|
|
|
10
10
|
display?: string;
|
|
11
11
|
}, valueInteger?: number) => void;
|
|
12
12
|
choiceClassName?: string;
|
|
13
|
+
inputClassName?: string;
|
|
13
14
|
renderCheckboxInput?: (props: CheckboxInputProps) => ReactNode;
|
|
15
|
+
onOtherTextChange?: (value: string) => void;
|
|
14
16
|
}
|
|
15
17
|
/**
|
|
16
|
-
* Renders a multi-select choice question with checkboxes
|
|
17
|
-
* Supports exclusive options and maxAnswers limits
|
|
18
|
+
* Renders a multi-select choice question with checkboxes.
|
|
19
|
+
* Supports exclusive options and maxAnswers limits.
|
|
20
|
+
*
|
|
21
|
+
* FHIR R5: When answerConstraint="optionsOrString", displays an optional
|
|
22
|
+
* "Other" text field alongside coded options. Multi-select means both
|
|
23
|
+
* coded options and free-text can coexist in the answers array.
|
|
18
24
|
*/
|
|
19
|
-
export declare const MultipleChoiceQuestion: ({ item, currentAnswers, onMultipleChoiceToggle, choiceClassName, renderCheckboxInput, }: MultipleChoiceQuestionProps) => import("react/jsx-runtime").JSX.Element;
|
|
25
|
+
export declare const MultipleChoiceQuestion: ({ item, currentAnswers, onMultipleChoiceToggle, choiceClassName, inputClassName, renderCheckboxInput, onOtherTextChange, }: MultipleChoiceQuestionProps) => import("react/jsx-runtime").JSX.Element;
|
|
20
26
|
//# 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,MAAM,sBAAsB,CAAC;AAG1D,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,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,4HAQpC,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 = "", 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;
|
|
@@ -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,5 +1,7 @@
|
|
|
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";
|
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,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
|
package/dist/styles.css
CHANGED
|
@@ -106,6 +106,65 @@
|
|
|
106
106
|
font-weight: var(--wq-font-weight-normal);
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/* === Quantity Inputs === */
|
|
110
|
+
.wq-quantity-question {
|
|
111
|
+
display: flex;
|
|
112
|
+
flex-direction: column;
|
|
113
|
+
gap: var(--wq-space-md);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.wq-quantity-input-row {
|
|
117
|
+
display: flex;
|
|
118
|
+
align-items: center;
|
|
119
|
+
gap: var(--wq-space-sm);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.wq-quantity-input-field {
|
|
123
|
+
flex: 1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.wq-quantity-unit-label {
|
|
127
|
+
font-size: var(--wq-font-size-base);
|
|
128
|
+
font-weight: var(--wq-font-weight-medium);
|
|
129
|
+
color: var(--wq-color-text-primary);
|
|
130
|
+
opacity: 0.7;
|
|
131
|
+
min-width: 2rem;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.wq-quantity-unit-toggle {
|
|
135
|
+
display: flex;
|
|
136
|
+
gap: 0;
|
|
137
|
+
border: var(--wq-border-width) solid var(--wq-color-border);
|
|
138
|
+
border-radius: var(--wq-radius-md);
|
|
139
|
+
overflow: hidden;
|
|
140
|
+
width: fit-content;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.wq-quantity-unit-option {
|
|
144
|
+
padding: var(--wq-space-sm) var(--wq-space-lg);
|
|
145
|
+
font-size: var(--wq-font-size-sm);
|
|
146
|
+
font-family: inherit;
|
|
147
|
+
background: var(--wq-color-surface);
|
|
148
|
+
color: var(--wq-color-text-primary);
|
|
149
|
+
border: none;
|
|
150
|
+
cursor: pointer;
|
|
151
|
+
transition: all var(--wq-transition-fast);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.wq-quantity-unit-option:not(:last-child) {
|
|
155
|
+
border-right: var(--wq-border-width) solid var(--wq-color-border);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.wq-quantity-unit-option.wq-selected {
|
|
159
|
+
background: var(--wq-color-primary);
|
|
160
|
+
color: white;
|
|
161
|
+
font-weight: var(--wq-font-weight-semibold);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.wq-quantity-unit-option:hover:not(.wq-selected) {
|
|
165
|
+
background-color: var(--wq-color-background);
|
|
166
|
+
}
|
|
167
|
+
|
|
109
168
|
/* === Text Inputs === */
|
|
110
169
|
.wq-question-input {
|
|
111
170
|
width: 100%;
|
|
@@ -248,6 +307,34 @@
|
|
|
248
307
|
font-weight: var(--wq-font-weight-medium);
|
|
249
308
|
}
|
|
250
309
|
|
|
310
|
+
/* === Open-Choice (coded options + free-text "Other") === */
|
|
311
|
+
.wq-open-choice {
|
|
312
|
+
display: flex;
|
|
313
|
+
flex-direction: column;
|
|
314
|
+
gap: var(--wq-space-md);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.wq-open-choice-other {
|
|
318
|
+
display: flex;
|
|
319
|
+
flex-direction: column;
|
|
320
|
+
gap: var(--wq-space-sm);
|
|
321
|
+
margin-top: var(--wq-space-sm);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.wq-open-choice-other-label {
|
|
325
|
+
font-size: var(--wq-font-size-sm);
|
|
326
|
+
font-weight: var(--wq-font-weight-medium);
|
|
327
|
+
color: var(--wq-color-text-secondary);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.wq-open-choice-other-active .wq-open-choice-other-label {
|
|
331
|
+
color: var(--wq-color-text-primary);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.wq-open-choice-other-input {
|
|
335
|
+
max-width: 24rem;
|
|
336
|
+
}
|
|
337
|
+
|
|
251
338
|
/* === Unsupported Type === */
|
|
252
339
|
.wq-unsupported-type {
|
|
253
340
|
padding: var(--wq-space-lg);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@welshare/questionnaire",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
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
|
},
|