From 7734501ad8af7dd2cb0f600d42c9b878c4085927 Mon Sep 17 00:00:00 2001 From: Tamas Kovacs Date: Fri, 30 May 2025 16:50:28 +0200 Subject: [PATCH] feat(many): add missing inputRef prop to input components INSTUI-4400 --- .../Checkbox/__new-tests__/Checkbox.test.tsx | 8 ++++++++ packages/ui-checkbox/src/Checkbox/index.tsx | 11 ++++++++--- packages/ui-checkbox/src/Checkbox/props.ts | 10 ++++++++-- .../__new-tests__/ColorPicker.test.tsx | 8 ++++++++ .../ui-color-picker/src/ColorPicker/index.tsx | 4 +++- .../ui-color-picker/src/ColorPicker/props.ts | 11 +++++++++-- .../__new-tests__/RadioInput.test.tsx | 15 +++++++++++++++ .../ui-radio-input/src/RadioInput/index.tsx | 11 ++++++++--- packages/ui-radio-input/src/RadioInput/props.ts | 10 ++++++++-- .../__new-tests__/RangeInput.test.tsx | 17 +++++++++++++++++ .../ui-range-input/src/RangeInput/index.tsx | 11 ++++++++--- packages/ui-range-input/src/RangeInput/props.ts | 11 +++++++++-- 12 files changed, 109 insertions(+), 18 deletions(-) diff --git a/packages/ui-checkbox/src/Checkbox/__new-tests__/Checkbox.test.tsx b/packages/ui-checkbox/src/Checkbox/__new-tests__/Checkbox.test.tsx index 08feecc783..69a1148288 100644 --- a/packages/ui-checkbox/src/Checkbox/__new-tests__/Checkbox.test.tsx +++ b/packages/ui-checkbox/src/Checkbox/__new-tests__/Checkbox.test.tsx @@ -105,6 +105,14 @@ describe('', () => { expect(inputElem).toHaveAttribute('aria-checked', 'mixed') }) + it('should provide an inputRef prop', async () => { + const inputRef = vi.fn() + renderCheckbox({ inputRef }) + const input = screen.getByRole('checkbox') + + expect(inputRef).toHaveBeenCalledWith(input) + }) + describe('events', () => { it('when clicked, fires onClick and onChange events', async () => { const onClick = vi.fn() diff --git a/packages/ui-checkbox/src/Checkbox/index.tsx b/packages/ui-checkbox/src/Checkbox/index.tsx index 911b6f1541..c33a937ecb 100644 --- a/packages/ui-checkbox/src/Checkbox/index.tsx +++ b/packages/ui-checkbox/src/Checkbox/index.tsx @@ -102,6 +102,13 @@ class Checkbox extends Component { this.ref = el } + handleInputRef = (el: HTMLInputElement | null) => { + this._input = el + if (typeof this.props.inputRef === 'function') { + this.props.inputRef(el) + } + } + componentDidMount() { if (this._input) { this._input.indeterminate = this.props.indeterminate! @@ -323,9 +330,7 @@ class Checkbox extends Component { id={this.id} value={value} type="checkbox" - ref={(c) => { - this._input = c - }} + ref={this.handleInputRef} required={isRequired} disabled={disabled || readOnly} aria-checked={indeterminate ? 'mixed' : undefined} diff --git a/packages/ui-checkbox/src/Checkbox/props.ts b/packages/ui-checkbox/src/Checkbox/props.ts index b9d106aae6..eb7274cc7b 100644 --- a/packages/ui-checkbox/src/Checkbox/props.ts +++ b/packages/ui-checkbox/src/Checkbox/props.ts @@ -80,6 +80,10 @@ type CheckboxOwnProps = { inline?: boolean labelPlacement?: 'top' | 'start' | 'end' isRequired?: boolean + /** + * A function that provides a reference to the actual underlying input element + */ + inputRef?: (inputElement: HTMLInputElement | null) => void } type PropKeys = keyof CheckboxOwnProps @@ -121,7 +125,8 @@ const propTypes: PropValidators = { variant: PropTypes.oneOf(['simple', 'toggle']), inline: PropTypes.bool, labelPlacement: PropTypes.oneOf(['top', 'start', 'end']), - isRequired: PropTypes.bool + isRequired: PropTypes.bool, + inputRef: PropTypes.func } const allowedProps: AllowedPropKeys = [ @@ -144,7 +149,8 @@ const allowedProps: AllowedPropKeys = [ 'variant', 'inline', 'labelPlacement', - 'isRequired' + 'isRequired', + 'inputRef' ] type CheckboxState = { diff --git a/packages/ui-color-picker/src/ColorPicker/__new-tests__/ColorPicker.test.tsx b/packages/ui-color-picker/src/ColorPicker/__new-tests__/ColorPicker.test.tsx index a05b875109..02ca9685b8 100644 --- a/packages/ui-color-picker/src/ColorPicker/__new-tests__/ColorPicker.test.tsx +++ b/packages/ui-color-picker/src/ColorPicker/__new-tests__/ColorPicker.test.tsx @@ -389,6 +389,14 @@ describe('', () => { expect(errorMessage).toBeInTheDocument() }) }) + + it('should provide an inputRef prop', async () => { + const inputRef = vi.fn() + render() + const input = screen.getByRole('textbox') + + expect(inputRef).toHaveBeenCalledWith(input) + }) }) describe('complex mode', () => { diff --git a/packages/ui-color-picker/src/ColorPicker/index.tsx b/packages/ui-color-picker/src/ColorPicker/index.tsx index 6ce83d5198..8c9dfc0d5d 100644 --- a/packages/ui-color-picker/src/ColorPicker/index.tsx +++ b/packages/ui-color-picker/src/ColorPicker/index.tsx @@ -608,7 +608,8 @@ class ColorPicker extends Component { ) render() { - const { disabled, isRequired, placeholderText, width, id } = this.props + const { disabled, isRequired, placeholderText, width, id, inputRef } = + this.props return (
{ onPaste={(event) => this.handleOnPaste(event)} onBlur={() => this.handleOnBlur()} messages={this.renderMessages()} + inputRef={inputRef} /> {!this.isSimple && (
void } type ColorPickerState = { @@ -326,7 +331,8 @@ const propTypes: PropValidators = { value: PropTypes.string, width: PropTypes.string, withAlpha: PropTypes.bool, - margin: PropTypes.string + margin: PropTypes.string, + inputRef: PropTypes.func } const allowedProps: AllowedPropKeys = [ @@ -350,7 +356,8 @@ const allowedProps: AllowedPropKeys = [ 'value', 'width', 'withAlpha', - 'margin' + 'margin', + 'inputRef' ] export type { diff --git a/packages/ui-radio-input/src/RadioInput/__new-tests__/RadioInput.test.tsx b/packages/ui-radio-input/src/RadioInput/__new-tests__/RadioInput.test.tsx index 6b7ef55460..d62282f7c6 100644 --- a/packages/ui-radio-input/src/RadioInput/__new-tests__/RadioInput.test.tsx +++ b/packages/ui-radio-input/src/RadioInput/__new-tests__/RadioInput.test.tsx @@ -61,6 +61,21 @@ describe('', () => { expect(input).toHaveAttribute('type', 'radio') }) + it('should provide an inputRef prop', async () => { + const inputRef = vi.fn() + const { container } = render( + + ) + const input = container.querySelector('input') + + expect(inputRef).toHaveBeenCalledWith(input) + }) + describe('events', () => { it('responds to onClick event', async () => { const onClick = vi.fn() diff --git a/packages/ui-radio-input/src/RadioInput/index.tsx b/packages/ui-radio-input/src/RadioInput/index.tsx index fb6135e622..0610211fd4 100644 --- a/packages/ui-radio-input/src/RadioInput/index.tsx +++ b/packages/ui-radio-input/src/RadioInput/index.tsx @@ -84,6 +84,13 @@ class RadioInput extends Component { this.props.makeStyles?.() } + handleInputRef = (el: HTMLInputElement | null) => { + this._input = el + if (typeof this.props.inputRef === 'function') { + this.props.inputRef(el) + } + } + handleClick: React.MouseEventHandler = (e) => { if (this.props.disabled || this.props.readOnly) { e.preventDefault() @@ -144,9 +151,7 @@ class RadioInput extends Component { { - this._input = c - }} + ref={this.handleInputRef} value={value} name={name} checked={this.checked} diff --git a/packages/ui-radio-input/src/RadioInput/props.ts b/packages/ui-radio-input/src/RadioInput/props.ts index 79ec8d0588..cc8defa463 100644 --- a/packages/ui-radio-input/src/RadioInput/props.ts +++ b/packages/ui-radio-input/src/RadioInput/props.ts @@ -72,6 +72,10 @@ type RadioInputOwnProps = { * event.target.value will contain the new value. It will always be a string. */ onChange?: (event: React.ChangeEvent) => void + /** + * A function that provides a reference to the actual underlying input element + */ + inputRef?: (inputElement: HTMLInputElement | null) => void } type PropKeys = keyof RadioInputOwnProps @@ -107,7 +111,8 @@ const propTypes: PropValidators = { context: PropTypes.oneOf(['success', 'warning', 'danger', 'off']), inline: PropTypes.bool, onClick: PropTypes.func, - onChange: PropTypes.func + onChange: PropTypes.func, + inputRef: PropTypes.func } const allowedProps: AllowedPropKeys = [ @@ -123,7 +128,8 @@ const allowedProps: AllowedPropKeys = [ 'context', 'inline', 'onClick', - 'onChange' + 'onChange', + 'inputRef' ] export type { RadioInputProps, RadioInputState, RadioInputStyle } diff --git a/packages/ui-range-input/src/RangeInput/__new-tests__/RangeInput.test.tsx b/packages/ui-range-input/src/RangeInput/__new-tests__/RangeInput.test.tsx index c9c3d34d52..1776246291 100644 --- a/packages/ui-range-input/src/RangeInput/__new-tests__/RangeInput.test.tsx +++ b/packages/ui-range-input/src/RangeInput/__new-tests__/RangeInput.test.tsx @@ -107,6 +107,23 @@ describe('', () => { expect(input).toHaveValue('25') }) + it('should provide an inputRef prop', async () => { + const inputRef = vi.fn() + const { container } = render( + + ) + const input = container.querySelector('input') + + expect(inputRef).toHaveBeenCalledWith(input) + }) + describe('thumbVariant prop', () => { it('should throw deprecation warning by default', async () => { render( diff --git a/packages/ui-range-input/src/RangeInput/index.tsx b/packages/ui-range-input/src/RangeInput/index.tsx index 31bb933d43..928909105e 100644 --- a/packages/ui-range-input/src/RangeInput/index.tsx +++ b/packages/ui-range-input/src/RangeInput/index.tsx @@ -86,6 +86,13 @@ class RangeInput extends Component { this.ref = el } + handleInputRef = (el: HTMLInputElement | null) => { + this._input = el + if (typeof this.props.inputRef === 'function') { + this.props.inputRef(el) + } + } + constructor(props: RangeInputProps) { super(props) @@ -195,9 +202,7 @@ class RangeInput extends Component {
{ - this._input = c - }} + ref={this.handleInputRef} type="range" id={this.id} min={this.props.min} diff --git a/packages/ui-range-input/src/RangeInput/props.ts b/packages/ui-range-input/src/RangeInput/props.ts index 7db2b2bd97..38abef131d 100644 --- a/packages/ui-range-input/src/RangeInput/props.ts +++ b/packages/ui-range-input/src/RangeInput/props.ts @@ -98,6 +98,11 @@ type RangeInputOwnProps = { thumbVariant?: | 'deprecated' // TODO: deprecated, remove in V9. | 'accessible' + + /** + * A function that provides a reference to the actual underlying input element + */ + inputRef?: (inputElement: HTMLInputElement | null) => void } type PropKeys = keyof RangeInputOwnProps @@ -147,7 +152,8 @@ const propTypes: PropValidators = { PropTypes.oneOf(['deprecated', 'accessible']), ['deprecated'], 'The `deprecated` variant is not fully accessible and will be removed in V9. The connected theme variables will be removed as well: `handleShadowColor`, `handleFocusOutlineColor`, `handleFocusOutlineWidth`. Please use the `accessible` variant.' - ) + ), + inputRef: PropTypes.func } const allowedProps: AllowedPropKeys = [ @@ -167,7 +173,8 @@ const allowedProps: AllowedPropKeys = [ 'inline', 'disabled', 'readOnly', - 'thumbVariant' + 'thumbVariant', + 'inputRef' ] export type { RangeInputProps, RangeInputState, RangeInputStyle }