Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[EuiValidatableControl] Fixed ref not being handled properly when used with react-hook-form #4001

Merged
merged 7 commits into from
Sep 9, 2020
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
## [`master`](https://github.com/elastic/eui/tree/master)

No public interface changes since `28.4.0`.
**Bug fixes**

- Fixed ref not being handled properly in `EuiValidatableControl` when used with [react-hook-form](https://react-hook-form.com/) ([#4001](https://github.com/elastic/eui/pull/4001))

## [`28.4.0`](https://github.com/elastic/eui/tree/v28.4.0)

Expand Down
102 changes: 102 additions & 0 deletions src/components/form/validatable_control/validatable_control.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,107 @@ describe('EuiValidatableControl', () => {
expect(ref.current).not.toBeNull();
expect(ref.current!.getAttribute('id')).toBe('testInput');
});

it('calls stable ref function only once on re-render', async () => {
const ref = jest.fn();

const Component = () => (
<EuiValidatableControl>
<input id="testInput" ref={ref} />
</EuiValidatableControl>
);

const wrapper = mount(<Component />);

expect(ref).toHaveBeenCalledTimes(1);
expect(ref.mock.calls[0][0].getAttribute('id')).toBe('testInput');

// Force re-render
wrapper.setProps({});

expect(ref).toHaveBeenCalledTimes(1);
expect(ref.mock.calls[0][0].getAttribute('id')).toBe('testInput');
});

it('calls unstable ref function again on re-render', async () => {
const ref = jest.fn();

const Component = () => (
<EuiValidatableControl>
<input id="testInput" ref={el => ref(el)} />
</EuiValidatableControl>
);

const wrapper = mount(<Component />);

expect(ref).toHaveBeenCalledTimes(1);
expect(ref.mock.calls[0][0].getAttribute('id')).toBe('testInput');

// Force re-render
wrapper.setProps({});

expect(ref).toHaveBeenCalledTimes(3);

expect(ref.mock.calls[1][0]).toBe(null);
expect(ref.mock.calls[2][0].getAttribute('id')).toBe('testInput');
});

it('calls a ref function again when the child element changes', () => {
const ref = jest.fn();

const Component = ({ change }: { change: boolean }) => (
<EuiValidatableControl>
{!change ? (
<input key="1" id="testInput" ref={ref} />
) : (
<input key="2" id="testInput2" ref={ref} />
)}
</EuiValidatableControl>
);

const wrapper = mount(<Component change={false} />);

expect(ref).toHaveBeenCalledTimes(1);
expect(ref.mock.calls[0][0].getAttribute('id')).toBe('testInput');

wrapper.setProps({ change: true });

expect(ref).toHaveBeenCalledTimes(3);

expect(ref.mock.calls[1][0]).toBe(null);
expect(ref.mock.calls[2][0].getAttribute('id')).toBe('testInput2');

// Ensure that the child element has changed
expect(ref.mock.calls[0][0]).not.toBe(ref.mock.calls[2][0]);
});

it('sets a ref object\'s "current" property when the child element changes', () => {
const ref = React.createRef<HTMLInputElement>();

const Component = ({ change }: { change: boolean }) => (
<EuiValidatableControl>
{!change ? (
<input key="1" id="testInput" ref={ref} />
) : (
<input key="2" id="testInput2" ref={ref} />
)}
</EuiValidatableControl>
);

const wrapper = mount(<Component change={false} />);

expect(ref.current).not.toBeNull();
expect(ref.current!.getAttribute('id')).toBe('testInput');

const prevRef = ref.current;

wrapper.setProps({ change: true });

expect(ref.current).not.toBeNull();
expect(ref.current!.getAttribute('id')).toBe('testInput2');

// Ensure that the child element has changed
expect(ref.current).not.toBe(prevRef);
});
});
});
74 changes: 35 additions & 39 deletions src/components/form/validatable_control/validatable_control.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@
import {
Children,
cloneElement,
Component,
MutableRefObject,
ReactElement,
Ref,
FunctionComponent,
useRef,
useEffect,
useCallback,
} from 'react';
import { CommonProps } from '../../common';

Expand All @@ -49,50 +52,43 @@ export interface EuiValidatableControlProps {
children: ReactElementWithRef;
}

export class EuiValidatableControl extends Component<
CommonProps & EuiValidatableControlProps
> {
private control?: HTMLConstraintValidityElement;
export const EuiValidatableControl: FunctionComponent<CommonProps &
EuiValidatableControlProps> = ({ isInvalid, children }) => {
const control = useRef<HTMLConstraintValidityElement | null>(null);

updateValidity() {
const child = Children.only(children);
const childRef = child.ref;

const replacedRef = useCallback(
(element: HTMLConstraintValidityElement) => {
control.current = element;

// Call the original ref, if any
if (typeof childRef === 'function') {
childRef(element);
} else if (isMutableRef(childRef)) {
childRef.current = element;
}
},
[childRef]
);

useEffect(() => {
if (
this.control == null ||
typeof this.control.setCustomValidity !== 'function'
control.current === null ||
typeof control.current.setCustomValidity !== 'function'
) {
return; // jsdom doesn't polyfill this for the server-side
}

if (this.props.isInvalid) {
this.control.setCustomValidity('Invalid');
if (isInvalid) {
control.current.setCustomValidity('Invalid');
} else {
this.control.setCustomValidity('');
control.current.setCustomValidity('');
}
}

componentDidMount() {
this.updateValidity();
}

componentDidUpdate() {
this.updateValidity();
}
});

setRef = (element: HTMLConstraintValidityElement) => {
this.control = element;

// Call the original ref, if any
const { ref } = this.props.children;
if (typeof ref === 'function') {
ref(element);
} else if (isMutableRef(ref)) {
ref.current = element;
}
};

render() {
const child = Children.only(this.props.children);
return cloneElement(child, {
ref: this.setRef,
});
}
}
return cloneElement(child, {
ref: replacedRef,
});
};