Skip to content

Commit 8eadd7a

Browse files
authored
Render managed auth field input modes (#29)
* Apply managed auth field input modes * Export managed auth input modes * Preserve identifier autofill hints * Prioritize semantic autocomplete hints * Cover input mode autocomplete precedence
1 parent 3261be8 commit 8eadd7a

7 files changed

Lines changed: 110 additions & 4 deletions

File tree

‎.changeset/gentle-input-modes.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@onkernel/managed-auth-react": patch
3+
---
4+
5+
Apply canonical field input modes as virtual keyboard hints without enabling browser format validation.

‎packages/managed-auth-react/src/components/UnifiedAuthForm.test.ts‎

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,95 @@ describe("getAutocomplete", () => {
1717
}),
1818
).toBe("username");
1919
});
20+
21+
test("uses input modes for dedicated email and telephone autofill", () => {
22+
expect(
23+
getAutocomplete({
24+
name: "field_email",
25+
label: "Email address",
26+
type: "text",
27+
input_mode: "email",
28+
}),
29+
).toBe("email");
30+
expect(
31+
getAutocomplete({
32+
name: "field_phone",
33+
label: "Phone number",
34+
type: "text",
35+
input_mode: "tel",
36+
}),
37+
).toBe("tel");
38+
expect(
39+
getAutocomplete({
40+
ref: "username",
41+
name: "field_identifier",
42+
label: "Account identifier",
43+
type: "text",
44+
input_mode: "email",
45+
}),
46+
).toBe("email");
47+
});
48+
49+
test("keeps one-time-code autocomplete ahead of keyboard hints", () => {
50+
expect(
51+
getAutocomplete({
52+
name: "field_code",
53+
label: "Verification code",
54+
type: "code",
55+
input_mode: "tel",
56+
}),
57+
).toBe("one-time-code");
58+
});
59+
60+
test("does not infer email autofill for mixed identifier inputs", () => {
61+
expect(
62+
getAutocomplete({
63+
ref: "email",
64+
name: "field_identifier",
65+
label: "Mobile number, username, or email",
66+
type: "text",
67+
input_mode: "text",
68+
}),
69+
).toBeUndefined();
70+
});
2071
});
2172

2273
describe("UnifiedAuthForm", () => {
74+
test("applies a keyboard hint without enabling native format validation", () => {
75+
let renderer!: ReturnType<typeof create>;
76+
77+
act(() => {
78+
renderer = create(
79+
createElement(AppearanceProvider, {
80+
children: createElement(LocalizationProvider, {
81+
children: createElement(UnifiedAuthForm, {
82+
targetDomain: "example.com",
83+
fields: [
84+
{
85+
name: "field_email",
86+
label: "Email address",
87+
type: "text",
88+
input_mode: "email",
89+
},
90+
],
91+
onSubmitFields: () => {},
92+
onSSOClick: () => {},
93+
onMFASelect: () => {},
94+
onSignInOptionSelect: () => {},
95+
}),
96+
}),
97+
}),
98+
);
99+
});
100+
101+
const input = renderer.root.findByType("input");
102+
expect(input.props.type).toBe("text");
103+
expect(input.props.inputMode).toBe("email");
104+
expect(input.props.autoComplete).toBe("email");
105+
106+
act(() => renderer.unmount());
107+
});
108+
23109
test("renders an accessible, customizable rejection notice", () => {
24110
const fieldName = "password-field";
25111
const rejectedNoticeId = `${fieldName}-rejected-notice`;

‎packages/managed-auth-react/src/components/UnifiedAuthForm.tsx‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,12 @@ export function getAutocomplete(field: DiscoveredField): string | undefined {
8888
case "code":
8989
case "totp":
9090
return "one-time-code";
91-
default:
92-
if (identity.includes("user") || identity.includes("identifier"))
93-
return "username";
94-
return undefined;
9591
}
92+
if (field.input_mode === "email") return "email";
93+
if (field.input_mode === "tel") return "tel";
94+
if (identity.includes("user") || identity.includes("identifier"))
95+
return "username";
96+
return undefined;
9697
}
9798

9899
function getDescriptionIds(field: DiscoveredField): string | undefined {
@@ -265,6 +266,7 @@ export function UnifiedAuthForm({
265266
id={field.name}
266267
name={field.name}
267268
type={showPassword[field.name] ? "text" : "password"}
269+
inputMode={field.input_mode}
268270
placeholder={field.placeholder}
269271
required={field.required}
270272
autoComplete={getAutocomplete(field)}
@@ -300,6 +302,7 @@ export function UnifiedAuthForm({
300302
id={field.name}
301303
name={field.name}
302304
type={getInputType(field)}
305+
inputMode={field.input_mode}
303306
placeholder={field.placeholder}
304307
required={field.required}
305308
autoComplete={getAutocomplete(field)}

‎packages/managed-auth-react/src/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export type {
3737
AuthSuccessPayload,
3838
AuthErrorPayload,
3939
MFAType,
40+
InputMode,
4041
DiscoveredField,
4142
SSOButton,
4243
MFAOption,

‎packages/managed-auth-react/src/lib/types.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,15 @@ export type MFAType =
2727
| "switch"
2828
| "other";
2929

30+
export type InputMode = "text" | "email" | "tel" | "numeric";
31+
3032
export interface DiscoveredField {
3133
id?: string;
3234
ref?: string;
3335
name: string;
3436
label: string;
3537
type: "text" | "email" | "password" | "tel" | "code" | "totp";
38+
input_mode?: InputMode;
3639
placeholder?: string;
3740
required?: boolean;
3841
reason?: "missing" | "rejected";
@@ -72,6 +75,7 @@ export interface ManagedAuthField {
7275
| "totp_code"
7376
| "totp_secret"
7477
| "text";
78+
input_mode?: InputMode;
7579
label?: string;
7680
required?: boolean;
7781
reason: "missing" | "rejected";

‎packages/managed-auth-react/src/session/state.test.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,20 +195,23 @@ describe("normalizeManagedAuthState", () => {
195195
id: "field_username",
196196
ref: "username",
197197
type: "identifier",
198+
input_mode: "text",
198199
label: "Username",
199200
reason: "missing",
200201
},
201202
{
202203
id: "field_email",
203204
ref: "email",
204205
type: "identifier",
206+
input_mode: "text",
205207
label: "Mobile number, username, or email",
206208
reason: "missing",
207209
},
208210
{
209211
id: "field_phone",
210212
ref: "phone_number",
211213
type: "identifier",
214+
input_mode: "tel",
212215
label: "Phone",
213216
reason: "missing",
214217
},
@@ -232,6 +235,7 @@ describe("normalizeManagedAuthState", () => {
232235
ref: "username",
233236
name: "field_username",
234237
type: "text",
238+
input_mode: "text",
235239
label: "Username",
236240
required: true,
237241
},
@@ -240,6 +244,7 @@ describe("normalizeManagedAuthState", () => {
240244
ref: "email",
241245
name: "field_email",
242246
type: "text",
247+
input_mode: "text",
243248
label: "Mobile number, username, or email",
244249
required: true,
245250
},
@@ -248,6 +253,7 @@ describe("normalizeManagedAuthState", () => {
248253
ref: "phone_number",
249254
name: "field_phone",
250255
type: "text",
256+
input_mode: "tel",
251257
label: "Phone",
252258
required: true,
253259
},

‎packages/managed-auth-react/src/session/state.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ function fieldsFromCanonical(
6565
ref: field.ref,
6666
name: field.id,
6767
type: fieldTypeToDiscoveredType(field),
68+
...(field.input_mode ? { input_mode: field.input_mode } : {}),
6869
label: field.label || field.ref,
6970
placeholder: legacyField?.placeholder,
7071
required: field.required ?? true,

0 commit comments

Comments
 (0)