Skip to content

Commit ccae070

Browse files
committed
ui: fix scroll to invalid text field in iOS devices
fixes #1248 Some iOS browser have a security restrictions to prevent programmatically focusing on elements without direct user interaction. This preventing from scrolling to the invalid field if that field was a text field. Scroll to the first invalid field in all cases, and try to focus on text field. If it does not work, at least the invalid field will be visible on screen. Change scrolling method from `window.scrollTo` to `element.scrollIntoView` as the latter focuses more on the element itself and should be preferred when the intention is to make the element visible.
1 parent 214368a commit ccae070

File tree

2 files changed

+29
-17
lines changed

2 files changed

+29
-17
lines changed

packages/evolution-frontend/src/components/hooks/__tests__/useSectionTemplate.test.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ describe('useSectionTemplate', () => {
5858
});
5959

6060
it('should focus on the first invalid input if form is submitted and not all widgets are valid', () => {
61+
// Setup scrollIntoView mock
62+
const scrollIntoViewMock = jest.fn();
63+
Element.prototype.scrollIntoView = scrollIntoViewMock;
64+
6165
document.body.innerHTML = `
6266
<div class="question-invalid">
6367
<input id="invalid-input" type="text" />
@@ -66,24 +70,32 @@ describe('useSectionTemplate', () => {
6670
props.allWidgetsValid = false;
6771
props.submitted = true;
6872
renderHook(() => useSectionTemplate(props), { wrapper: MemoryRouter });
73+
74+
// Verify scrollIntoView was called
75+
expect(scrollIntoViewMock).toHaveBeenCalledWith({ behavior: 'smooth', block: 'nearest' });
76+
77+
// Verify focus was set on text input
6978
const inputElement = document.getElementById('invalid-input');
7079
expect(document.activeElement).toBe(inputElement);
7180
});
7281

7382
it('should scroll to the first invalid question if form is submitted and not all widgets are valid', () => {
74-
const gotoPosition = 1000;
83+
// Setup scrollIntoView mock
84+
const scrollIntoViewMock = jest.fn();
85+
Element.prototype.scrollIntoView = scrollIntoViewMock;
86+
7587
document.body.innerHTML = `
76-
<div class="question-invalid" style="margin-top: 1000px;">
88+
<div class="question-invalid">
7789
<input id="invalid-input" type="checkbox" />
7890
</div>
7991
`;
80-
const invalidElement = document.querySelector('.question-invalid');
81-
Object.defineProperty(invalidElement, 'offsetTop', { value: gotoPosition });
82-
window.scrollTo = jest.fn();
92+
8393
props.allWidgetsValid = false;
8494
props.submitted = true;
8595
renderHook(() => useSectionTemplate(props), { wrapper: MemoryRouter });
86-
expect(window.scrollTo).toHaveBeenCalledWith(0, gotoPosition);
96+
97+
// Verify scrollIntoView was called with correct parameters
98+
expect(scrollIntoViewMock).toHaveBeenCalledWith({ behavior: 'smooth', block: 'nearest' });
8799
});
88100

89101
});

packages/evolution-frontend/src/components/hooks/useSectionTemplate.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
*/
77
import { useEffect, useState } from 'react';
88
import { getPathForSection } from '../../services/url'; // Adjust the import path
9-
import _get from 'lodash/get';
109
import { SectionConfig, UserRuntimeInterviewAttributes } from 'evolution-common/lib/services/questionnaire/types';
1110
import { CliUser } from 'chaire-lib-common/lib/services/user/userType';
1211
import { InterviewUpdateCallbacks } from 'evolution-common/lib/services/questionnaire/types';
@@ -46,23 +45,24 @@ export function useSectionTemplate(props: SectionProps) {
4645
// Scroll to first invalid component, if any
4746
useEffect(() => {
4847
if (!props.allWidgetsValid && props.submitted && props.loadingState === 0) {
49-
const invalidInputs = document.querySelectorAll('.question-invalid input') as NodeListOf<HTMLInputElement>;
48+
// Scroll to the position of the first invalid question in all
49+
// cases. Some browsers, like Safari iOS have security features
50+
// preventing focus without explicit user interaction (and this is
51+
// not considered one)
52+
const firstInvalidElement = document.getElementsByClassName('question-invalid')[0];
53+
if (firstInvalidElement) {
54+
firstInvalidElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
55+
}
56+
5057
// Not all widgets types focus correclty on all browsers, so we do the
51-
// actual focus only on text widgets. For the others, we scroll to the
52-
// position of the first invalid question to make sure it is in view. This
53-
// works for all widgets types.
58+
// actual focus only on text widgets.
59+
const invalidInputs = document.querySelectorAll('.question-invalid input') as NodeListOf<HTMLInputElement>;
5460
if (invalidInputs.length > 0 && invalidInputs[0].id && invalidInputs[0].type === 'text') {
5561
// Focus on invalid input if found, it has an ID, and is of type text
5662
const inputElement = document.getElementById(invalidInputs[0].id);
5763
if (inputElement) {
5864
inputElement.focus();
5965
}
60-
} else {
61-
// Otherwise scroll to the position of the first invalid question
62-
const scrollPosition = _get(document.getElementsByClassName('question-invalid'), '[0].offsetTop', null);
63-
if (scrollPosition && scrollPosition >= 0) {
64-
window.scrollTo(0, scrollPosition);
65-
}
6666
}
6767
}
6868
}, [props.allWidgetsValid, props.submitted, props.loadingState]);

0 commit comments

Comments
 (0)