@openmrs/esm-login-app 8.0.1-pre.3547 → 8.0.1-pre.3552
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/.turbo/turbo-build.log +3 -3
- package/dist/8333.js +1 -1
- package/dist/8333.js.map +1 -1
- package/dist/8450.js +1 -1
- package/dist/8450.js.map +1 -1
- package/dist/main.js +3 -3
- package/dist/main.js.map +1 -1
- package/dist/openmrs-esm-login-app.js +3 -3
- package/dist/openmrs-esm-login-app.js.buildmanifest.json +10 -10
- package/dist/routes.json +1 -1
- package/package.json +3 -3
- package/src/login/login.component.tsx +28 -16
- package/src/login/login.scss +11 -0
- package/src/login/login.test.tsx +32 -3
|
@@ -51,7 +51,10 @@ const Login: React.FC = () => {
|
|
|
51
51
|
useEffect(() => {
|
|
52
52
|
if (showPasswordOnSeparateScreen) {
|
|
53
53
|
if (showPasswordField) {
|
|
54
|
-
|
|
54
|
+
// Only focus password input if it's empty (to preserve browser autofilled values)
|
|
55
|
+
if (!passwordInputRef.current?.value) {
|
|
56
|
+
passwordInputRef.current?.focus();
|
|
57
|
+
}
|
|
55
58
|
} else {
|
|
56
59
|
usernameInputRef.current?.focus();
|
|
57
60
|
}
|
|
@@ -157,6 +160,8 @@ const Login: React.FC = () => {
|
|
|
157
160
|
<TextInput
|
|
158
161
|
id="username"
|
|
159
162
|
type="text"
|
|
163
|
+
name="username"
|
|
164
|
+
autoComplete="username"
|
|
160
165
|
labelText={t('username', 'Username')}
|
|
161
166
|
value={username}
|
|
162
167
|
onChange={changeUsername}
|
|
@@ -165,19 +170,25 @@ const Login: React.FC = () => {
|
|
|
165
170
|
autoFocus
|
|
166
171
|
/>
|
|
167
172
|
{showPasswordOnSeparateScreen ? (
|
|
168
|
-
|
|
169
|
-
|
|
173
|
+
<>
|
|
174
|
+
{/* Password input is always in DOM for browser autofill support, but visually hidden until username step is complete */}
|
|
175
|
+
<div className={showPasswordField ? undefined : styles.hiddenPasswordField}>
|
|
170
176
|
<PasswordInput
|
|
171
177
|
id="password"
|
|
172
178
|
labelText={t('password', 'Password')}
|
|
173
179
|
name="password"
|
|
180
|
+
autoComplete="current-password"
|
|
174
181
|
onChange={changePassword}
|
|
175
182
|
ref={passwordInputRef}
|
|
176
|
-
required
|
|
183
|
+
required={showPasswordField}
|
|
177
184
|
value={password}
|
|
178
185
|
showPasswordLabel={t('showPassword', 'Show password')}
|
|
179
186
|
invalidText={t('validValueRequired', 'A valid value is required')}
|
|
187
|
+
aria-hidden={!showPasswordField}
|
|
188
|
+
tabIndex={showPasswordField ? 0 : -1}
|
|
180
189
|
/>
|
|
190
|
+
</div>
|
|
191
|
+
{showPasswordField ? (
|
|
181
192
|
<Button
|
|
182
193
|
type="submit"
|
|
183
194
|
className={styles.continueButton}
|
|
@@ -191,24 +202,25 @@ const Login: React.FC = () => {
|
|
|
191
202
|
t('login', 'Log in')
|
|
192
203
|
)}
|
|
193
204
|
</Button>
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
205
|
+
) : (
|
|
206
|
+
<Button
|
|
207
|
+
className={styles.continueButton}
|
|
208
|
+
renderIcon={(props) => <ArrowRightIcon size={24} {...props} />}
|
|
209
|
+
iconDescription="Continue to password"
|
|
210
|
+
onClick={continueLogin}
|
|
211
|
+
disabled={!isLoginEnabled}
|
|
212
|
+
>
|
|
213
|
+
{t('continue', 'Continue')}
|
|
214
|
+
</Button>
|
|
215
|
+
)}
|
|
216
|
+
</>
|
|
206
217
|
) : (
|
|
207
218
|
<>
|
|
208
219
|
<PasswordInput
|
|
209
220
|
id="password"
|
|
210
221
|
labelText={t('password', 'Password')}
|
|
211
222
|
name="password"
|
|
223
|
+
autoComplete="current-password"
|
|
212
224
|
onChange={changePassword}
|
|
213
225
|
ref={passwordInputRef}
|
|
214
226
|
required
|
package/src/login/login.scss
CHANGED
|
@@ -156,3 +156,14 @@
|
|
|
156
156
|
bottom: 100%;
|
|
157
157
|
left: 0;
|
|
158
158
|
}
|
|
159
|
+
|
|
160
|
+
// Hidden password field for browser autofill support
|
|
161
|
+
// Uses visibility:hidden instead of display:none to keep the input in the DOM
|
|
162
|
+
// This allows browsers to autofill both username and password credentials
|
|
163
|
+
.hiddenPasswordField {
|
|
164
|
+
visibility: hidden;
|
|
165
|
+
height: 0;
|
|
166
|
+
overflow: hidden;
|
|
167
|
+
position: absolute;
|
|
168
|
+
pointer-events: none;
|
|
169
|
+
}
|
package/src/login/login.test.tsx
CHANGED
|
@@ -172,7 +172,7 @@ describe('Login', () => {
|
|
|
172
172
|
expect(loginButton).toBeInTheDocument();
|
|
173
173
|
});
|
|
174
174
|
|
|
175
|
-
it('should
|
|
175
|
+
it('should render password field hidden but present for autofill when showPasswordOnSeparateScreen config is true (default)', async () => {
|
|
176
176
|
mockUseConfig.mockReturnValue({
|
|
177
177
|
...mockConfig,
|
|
178
178
|
});
|
|
@@ -187,12 +187,14 @@ describe('Login', () => {
|
|
|
187
187
|
|
|
188
188
|
const usernameInput = screen.queryByRole('textbox', { name: /username/i });
|
|
189
189
|
const continueButton = screen.queryByRole('button', { name: /Continue/i });
|
|
190
|
-
const passwordInput = screen.queryByLabelText(
|
|
190
|
+
const passwordInput = screen.queryByLabelText(/^password$/i);
|
|
191
191
|
const loginButton = screen.queryByRole('button', { name: /log in/i });
|
|
192
192
|
|
|
193
193
|
expect(usernameInput).toBeInTheDocument();
|
|
194
194
|
expect(continueButton).toBeInTheDocument();
|
|
195
|
-
expect(passwordInput).
|
|
195
|
+
expect(passwordInput).toBeInTheDocument();
|
|
196
|
+
expect(passwordInput).toHaveAttribute('aria-hidden', 'true');
|
|
197
|
+
expect(passwordInput).toHaveAttribute('tabIndex', '-1');
|
|
196
198
|
expect(loginButton).not.toBeInTheDocument();
|
|
197
199
|
});
|
|
198
200
|
|
|
@@ -283,4 +285,31 @@ describe('Login', () => {
|
|
|
283
285
|
|
|
284
286
|
expect(usernameInput).toHaveFocus();
|
|
285
287
|
});
|
|
288
|
+
|
|
289
|
+
it('should make password input visible and accessible after continuing from username step', async () => {
|
|
290
|
+
const user = userEvent.setup();
|
|
291
|
+
mockUseConfig.mockReturnValue({
|
|
292
|
+
...mockConfig,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
renderWithRouter(
|
|
296
|
+
Login,
|
|
297
|
+
{},
|
|
298
|
+
{
|
|
299
|
+
route: '/login',
|
|
300
|
+
},
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
const usernameInput = screen.getByRole('textbox', { name: /username/i });
|
|
304
|
+
const continueButton = screen.getByRole('button', { name: /Continue/i });
|
|
305
|
+
|
|
306
|
+
await user.type(usernameInput, 'testuser');
|
|
307
|
+
await user.click(continueButton);
|
|
308
|
+
|
|
309
|
+
const passwordInput = screen.getByLabelText(/^password$/i);
|
|
310
|
+
|
|
311
|
+
// Password should now be visible and accessible
|
|
312
|
+
expect(passwordInput).toHaveAttribute('aria-hidden', 'false');
|
|
313
|
+
expect(passwordInput).toHaveAttribute('tabIndex', '0');
|
|
314
|
+
});
|
|
286
315
|
});
|