Skip to content

Commit 9f6bdc4

Browse files
committed
Add keyboard type support for input fields
1 parent 8bbca48 commit 9f6bdc4

File tree

9 files changed

+759
-599
lines changed

9 files changed

+759
-599
lines changed

COVERAGE.md

Lines changed: 599 additions & 599 deletions
Large diffs are not rendered by default.

lib/components/controls/text-area.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { HTMLAttributes } from "react";
2+
13
export function TextArea({
24
id,
35
value,
@@ -7,6 +9,7 @@ export function TextArea({
79
rows = 4,
810
ariaLabelledBy,
911
ariaDescribedBy,
12+
inputMode,
1013
}: {
1114
id?: string | undefined;
1215
value: string;
@@ -16,6 +19,7 @@ export function TextArea({
1619
rows?: number | undefined;
1720
ariaLabelledBy?: string | undefined;
1821
ariaDescribedBy?: string | undefined;
22+
inputMode?: HTMLAttributes<Element>["inputMode"] | undefined;
1923
}) {
2024
return (
2125
<textarea
@@ -28,6 +32,7 @@ export function TextArea({
2832
rows={rows}
2933
aria-labelledby={ariaLabelledBy}
3034
aria-describedby={ariaDescribedBy}
35+
inputMode={inputMode}
3136
/>
3237
);
3338
}

lib/components/controls/text-input.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { HTMLAttributes } from "react";
2+
13
export function TextInput({
24
id,
35
type = "text",
@@ -7,6 +9,7 @@ export function TextInput({
79
placeholder,
810
ariaLabelledBy,
911
ariaDescribedBy,
12+
inputMode,
1013
}: {
1114
id?: string | undefined;
1215
type?: string | undefined;
@@ -16,6 +19,7 @@ export function TextInput({
1619
placeholder?: string | undefined;
1720
ariaLabelledBy?: string | undefined;
1821
ariaDescribedBy?: string | undefined;
22+
inputMode?: HTMLAttributes<Element>["inputMode"] | undefined;
1923
}) {
2024
return (
2125
<input
@@ -28,6 +32,7 @@ export function TextInput({
2832
placeholder={placeholder}
2933
aria-labelledby={ariaLabelledBy}
3034
aria-describedby={ariaDescribedBy}
35+
inputMode={inputMode}
3136
/>
3237
);
3338
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { describe, expect, it } from "vitest";
2+
import { render, screen } from "@testing-library/react";
3+
import type { Questionnaire } from "fhir/r5";
4+
5+
import { FormStore } from "../../../stores/form-store.ts";
6+
import { isQuestionNode } from "../../../stores/question-store.ts";
7+
import { QuestionNode } from "../../nodes/question-node.tsx";
8+
import { EXT } from "../../../utils.ts";
9+
10+
function getQuestion(form: FormStore, linkId: string) {
11+
const node = form.scope.lookupNode(linkId);
12+
expect(node && isQuestionNode(node)).toBe(true);
13+
if (!node || !isQuestionNode(node)) {
14+
throw new Error("Expected question node");
15+
}
16+
return node;
17+
}
18+
19+
describe("keyboard extension", () => {
20+
it("applies tel input mode for phone keyboard type on string questions", () => {
21+
const questionnaire: Questionnaire = {
22+
resourceType: "Questionnaire",
23+
status: "active",
24+
item: [
25+
{
26+
linkId: "contact-phone",
27+
text: "Contact phone",
28+
type: "string",
29+
extension: [
30+
{
31+
url: EXT.SDC_KEYBOARD,
32+
valueCoding: {
33+
system: "http://hl7.org/fhir/uv/sdc/ValueSet/keyboardType",
34+
code: "phone",
35+
},
36+
},
37+
],
38+
},
39+
],
40+
};
41+
42+
const form = new FormStore(questionnaire);
43+
const question = getQuestion(form, "contact-phone");
44+
45+
expect(question.keyboardType).toBe("tel");
46+
47+
render(<QuestionNode item={question} />);
48+
49+
const input = screen.getByLabelText("Contact phone") as HTMLInputElement;
50+
expect(input.getAttribute("inputmode")).toBe("tel");
51+
});
52+
53+
it("applies text input mode for chat keyboard type on text questions", () => {
54+
const questionnaire: Questionnaire = {
55+
resourceType: "Questionnaire",
56+
status: "active",
57+
item: [
58+
{
59+
linkId: "chat-text",
60+
text: "Chat message",
61+
type: "text",
62+
extension: [
63+
{
64+
url: EXT.SDC_KEYBOARD,
65+
valueCoding: {
66+
system: "http://hl7.org/fhir/uv/sdc/ValueSet/keyboardType",
67+
code: "chat",
68+
},
69+
},
70+
],
71+
},
72+
],
73+
};
74+
75+
const form = new FormStore(questionnaire);
76+
const question = getQuestion(form, "chat-text");
77+
78+
expect(question.keyboardType).toBe("text");
79+
80+
render(<QuestionNode item={question} />);
81+
82+
const textarea = screen.getByLabelText(
83+
"Chat message",
84+
) as HTMLTextAreaElement;
85+
expect(textarea.getAttribute("inputmode")).toBe("text");
86+
});
87+
88+
it("ignores unsupported keyboard codes", () => {
89+
const questionnaire: Questionnaire = {
90+
resourceType: "Questionnaire",
91+
status: "active",
92+
item: [
93+
{
94+
linkId: "other",
95+
text: "Other",
96+
type: "string",
97+
extension: [
98+
{
99+
url: EXT.SDC_KEYBOARD,
100+
valueCoding: {
101+
code: "unsupported",
102+
},
103+
},
104+
],
105+
},
106+
],
107+
};
108+
109+
const form = new FormStore(questionnaire);
110+
const question = getQuestion(form, "other");
111+
112+
expect(question.keyboardType).toBeUndefined();
113+
114+
render(<QuestionNode item={question} />);
115+
116+
const input = screen.getByLabelText("Other") as HTMLInputElement;
117+
expect(input.hasAttribute("inputmode")).toBe(false);
118+
});
119+
});

lib/components/questions/nodes/string-node.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const StringNode = observer(function StringNode({
2323
value={value ?? ""}
2424
onChange={setValue}
2525
disabled={item.readOnly}
26+
inputMode={item.keyboardType}
2627
/>
2728
)}
2829
/>

lib/components/questions/nodes/text-node.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const TextNode = observer(function TextNode({
2323
value={value ?? ""}
2424
onChange={setValue}
2525
disabled={item.readOnly}
26+
inputMode={item.keyboardType}
2627
/>
2728
)}
2829
/>

lib/stores/question-store.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,11 @@ import { QuestionValidator } from "./question-validator.ts";
3232
import {
3333
ANSWER_TYPE_TO_DATA_TYPE,
3434
answerHasContent,
35+
EXT,
36+
extractExtensionValue,
3537
getValue,
3638
} from "../utils.ts";
39+
import type { HTMLAttributes } from "react";
3740

3841
type AnswerLifecycle =
3942
| "pristine"
@@ -88,6 +91,29 @@ export class QuestionStore<T extends AnswerType = AnswerType>
8891
return !!this.template.repeats;
8992
}
9093

94+
@computed
95+
get keyboardType(): HTMLAttributes<Element>["inputMode"] | undefined {
96+
if (this.type !== "string" && this.type !== "text") {
97+
return undefined;
98+
}
99+
100+
const coding = extractExtensionValue(
101+
this.template,
102+
EXT.SDC_KEYBOARD,
103+
"Coding",
104+
);
105+
106+
const keyboardMap: Record<string, HTMLAttributes<Element>["inputMode"]> = {
107+
phone: "tel",
108+
email: "email",
109+
number: "numeric",
110+
url: "url",
111+
chat: "text",
112+
};
113+
114+
return coding?.code ? keyboardMap[coding.code] : undefined;
115+
}
116+
91117
@override
92118
override get maxOccurs(): number {
93119
return this.repeats ? super.maxOccurs : 1;

lib/stores/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
} from "fhir/r5";
4343
import type { EvaluationCoordinator } from "./evaluation-coordinator.ts";
4444
import { ExpressionRegistry } from "./expression-registry.ts";
45+
import type { HTMLAttributes } from "react";
4546

4647
export type OperationOutcomeIssueCode =
4748
| "business-rule" // Expression cycles / logic conflicts
@@ -366,6 +367,7 @@ export interface IQuestionNode<T extends AnswerType = AnswerType>
366367
extends IActualNode {
367368
readonly type: T;
368369
readonly repeats: boolean;
370+
readonly keyboardType: HTMLAttributes<Element>["inputMode"] | undefined;
369371

370372
answers: Array<IAnswerInstance<DataTypeToType<AnswerTypeToDataType<T>>>>;
371373

lib/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export const EXT = {
6262
SDC_CALCULATED_EXPR: "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression",
6363
SDC_INITIAL_EXPR: "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression",
6464
SDC_VARIABLE: "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-variable",
65+
SDC_KEYBOARD: "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-keyboard",
6566
CQF_EXPRESSION: "http://hl7.org/fhir/StructureDefinition/cqf-expression",
6667
CQF_CALCULATED_VALUE: "http://hl7.org/fhir/uv/cql/StructureDefinition/cqf-calculatedValue",
6768
TARGET_CONSTRAINT: "http://hl7.org/fhir/StructureDefinition/targetConstraint",

0 commit comments

Comments
 (0)