Skip to content

Commit

Permalink
feat(textinput): support passing a ref
Browse files Browse the repository at this point in the history
Signed-off-by: Boaz Shuster <boaz.shuster.github@gmail.com>
  • Loading branch information
boaz0 committed Nov 27, 2019
1 parent 759c8f1 commit bad2b9a
Show file tree
Hide file tree
Showing 9 changed files with 102 additions and 99 deletions.
Expand Up @@ -55,19 +55,12 @@ exports[`Renders ContextSelector open 1`] = `
className="pf-c-context-selector__menu-input"
>
<Component>
<TextInput
aria-label={null}
<ForwardRef
aria-labelledby="pf-context-selector-search-button-id-0"
className=""
isDisabled={false}
isReadOnly={false}
isRequired={false}
isValid={true}
onChange={[Function]}
onKeyPress={[Function]}
placeholder="Search"
type="search"
validated="default"
value=""
/>
<Component
Expand Down
Expand Up @@ -14,19 +14,14 @@ exports[`LoginForm with rememberMeLabel 1`] = `
isValid={true}
label="Username"
>
<TextInput
aria-label={null}
<ForwardRef
autoFocus={true}
className=""
id="pf-login-username-id"
isDisabled={false}
isReadOnly={false}
isRequired={true}
isValid={true}
name="pf-login-username-id"
onChange={[Function]}
type="text"
validated="default"
value=""
/>
</Component>
Expand All @@ -36,18 +31,13 @@ exports[`LoginForm with rememberMeLabel 1`] = `
isValid={true}
label="Password"
>
<TextInput
aria-label={null}
className=""
<ForwardRef
id="pf-login-password-id"
isDisabled={false}
isReadOnly={false}
isRequired={true}
isValid={true}
name="pf-login-password-id"
onChange={[Function]}
type="password"
validated="default"
value=""
/>
</Component>
Expand Down Expand Up @@ -92,19 +82,14 @@ exports[`LoginForm with rememberMeLabel and rememberMeAriaLabel uses the remembe
isValid={true}
label="Username"
>
<TextInput
aria-label={null}
<ForwardRef
autoFocus={true}
className=""
id="pf-login-username-id"
isDisabled={false}
isReadOnly={false}
isRequired={true}
isValid={true}
name="pf-login-username-id"
onChange={[Function]}
type="text"
validated="default"
value=""
/>
</Component>
Expand All @@ -114,18 +99,13 @@ exports[`LoginForm with rememberMeLabel and rememberMeAriaLabel uses the remembe
isValid={true}
label="Password"
>
<TextInput
aria-label={null}
className=""
<ForwardRef
id="pf-login-password-id"
isDisabled={false}
isReadOnly={false}
isRequired={true}
isValid={true}
name="pf-login-password-id"
onChange={[Function]}
type="password"
validated="default"
value=""
/>
</Component>
Expand Down Expand Up @@ -170,19 +150,14 @@ exports[`should render Login form 1`] = `
isValid={true}
label="Username"
>
<TextInput
aria-label={null}
<ForwardRef
autoFocus={true}
className=""
id="pf-login-username-id"
isDisabled={false}
isReadOnly={false}
isRequired={true}
isValid={true}
name="pf-login-username-id"
onChange={[Function]}
type="text"
validated="default"
value=""
/>
</Component>
Expand All @@ -192,18 +167,13 @@ exports[`should render Login form 1`] = `
isValid={true}
label="Password"
>
<TextInput
aria-label={null}
className=""
<ForwardRef
id="pf-login-password-id"
isDisabled={false}
isReadOnly={false}
isRequired={true}
isValid={true}
name="pf-login-password-id"
onChange={[Function]}
type="password"
validated="default"
value=""
/>
</Component>
Expand Down
@@ -1,6 +1,6 @@
import React from 'react';
import { shallow } from 'enzyme';
import { TextInput } from './TextInput';
import { mount, shallow } from 'enzyme';
import { TextInput,TextInputBase } from './TextInput';
import { ValidatedOptions } from '../../helpers/constants';

const props = {
Expand All @@ -13,33 +13,33 @@ test('input passes value and event to onChange handler', () => {
const event = {
currentTarget: { value: newValue }
};
const view = shallow(<TextInput {...props} aria-label="test input" />);
const view = shallow(<TextInputBase {...props} aria-label="test input" />);
view.find('input').simulate('change', event);
expect(props.onChange).toBeCalledWith(newValue, event);
});

test('simple text input', () => {
const view = shallow(<TextInput {...props} aria-label="simple text input" />);
expect(view).toMatchSnapshot();
const view = mount(<TextInput {...props} aria-label="simple text input" />);
expect(view.find('input')).toMatchSnapshot();
});

test('disabled text input', () => {
const view = shallow(<TextInput isDisabled aria-label="disabled text input" />);
expect(view).toMatchSnapshot();
const view = mount(<TextInput isDisabled aria-label="disabled text input" />);
expect(view.find('input')).toMatchSnapshot();
});

test('readonly text input', () => {
const view = shallow(<TextInput isReadOnly value="read only" aria-label="readonly text input" />);
expect(view).toMatchSnapshot();
const view = mount(<TextInput isReadOnly value="read only" aria-label="readonly text input" />);
expect(view.find('input')).toMatchSnapshot();
});

test('invalid text input', () => {
const view = shallow(<TextInput {...props} required isValid={false} aria-label="invalid text input" />);
expect(view).toMatchSnapshot();
const view = mount(<TextInput {...props} required isValid={false} aria-label="invalid text input" />);
expect(view.find('input')).toMatchSnapshot();
});

test('validated text input success', () => {
const view = shallow(<TextInput {...props} required validated={ValidatedOptions.success} aria-label="validated text input" />);
const view = mount(<TextInput {...props} required validated={ValidatedOptions.success} aria-label="validated text input" />);
expect(view.find('.pf-c-form-control.pf-m-success').length).toBe(1);
expect(view).toMatchSnapshot();
});
Expand All @@ -52,27 +52,27 @@ test('validated text input', () => {
test('should throw console error when no aria-label, id or aria-labelledby is given', () => {
const myMock = jest.fn();
global.console = { error: myMock };
shallow(<TextInput {...props} />);
mount(<TextInput {...props} />);
expect(myMock).toBeCalled();
});

test('should not throw console error when id is given but no aria-label or aria-labelledby', () => {
const myMock = jest.fn();
global.console = { error: myMock };
shallow(<TextInput {...props} id="5" />);
mount(<TextInput {...props} id="5" />);
expect(myMock).not.toBeCalled();
});

test('should not throw console error when aria-label is given but no id or aria-labelledby', () => {
const myMock = jest.fn();
global.console = { error: myMock };
shallow(<TextInput {...props} aria-label="test input" />);
mount(<TextInput {...props} aria-label="test input" />);
expect(myMock).not.toBeCalled();
});

test('should not throw console error when aria-labelledby is given but no id or aria-label', () => {
const myMock = jest.fn();
global.console = { error: myMock };
shallow(<TextInput {...props} aria-labelledby="test input" />);
mount(<TextInput {...props} aria-labelledby="test input" />);
expect(myMock).not.toBeCalled();
});
@@ -1,7 +1,7 @@
import * as React from 'react';
import styles from '@patternfly/react-styles/css/components/FormControl/form-control';
import { css, getModifier } from '@patternfly/react-styles';
import { Omit } from '../../helpers/typeUtils';
import { Omit, withInnerRef } from '../../helpers'
import { ValidatedOptions } from '../../helpers/constants';

export enum TextInputTypes {
Expand Down Expand Up @@ -53,18 +53,20 @@ export interface TextInputProps extends Omit<React.HTMLProps<HTMLInputElement>,
value?: string | number;
/** Aria-label. The input requires an associated id or aria-label. */
'aria-label'?: string;
/** A reference object to attach to the input box. */
innerRef?: React.Ref<any>;
}

export class TextInput extends React.Component<TextInputProps> {
class TextInputBase extends React.Component<TextInputProps> {
static defaultProps = {
'aria-label': null as string,
className: '',
isRequired: false,
isValid: true,
validated: 'default',
validated: 'default' as 'success' | 'error' | 'default',
isDisabled: false,
isReadOnly: false,
type: 'text',
type: TextInputTypes.text,
onChange: (): any => undefined
};

Expand All @@ -84,6 +86,7 @@ export class TextInput extends React.Component<TextInputProps> {

render() {
const {
innerRef,
className,
type,
value,
Expand All @@ -110,7 +113,11 @@ export class TextInput extends React.Component<TextInputProps> {
required={isRequired}
disabled={isDisabled}
readOnly={isReadOnly}
ref={innerRef}
/>
);
}
}

const TextInputFR = withInnerRef<HTMLInputElement, TextInputProps>(TextInputBase)
export { TextInputFR as TextInput, TextInputBase }
Expand Up @@ -56,29 +56,55 @@ exports[`simple text input 1`] = `
`;

exports[`validated text input 1`] = `
<input
aria-invalid={true}
<TextInputBase
aria-label="validated text input"
className="pf-c-form-control"
disabled={false}
onChange={[Function]}
readOnly={false}
required={false}
className=""
innerRef={null}
isDisabled={false}
isReadOnly={false}
isRequired={false}
isValid={true}
onChange={[MockFunction]}
required={true}
type="text"
validated="error"
value="test input"
/>
`;

exports[`validated text input success 1`] = `
<input
aria-invalid={false}
<ForwardRef
aria-label="validated text input"
className="pf-c-form-control pf-m-success"
disabled={false}
onChange={[Function]}
readOnly={false}
required={false}
type="text"
onChange={[MockFunction]}
required={true}
validated="success"
value="test input"
/>
>
<TextInputBase
aria-label="validated text input"
className=""
innerRef={null}
isDisabled={false}
isReadOnly={false}
isRequired={false}
isValid={true}
onChange={[MockFunction]}
required={true}
type="text"
validated="success"
value="test input"
>
<input
aria-invalid={false}
aria-label="validated text input"
className="pf-c-form-control pf-m-success"
disabled={false}
onChange={[Function]}
readOnly={false}
required={false}
type="text"
value="test input"
/>
</TextInputBase>
</ForwardRef>
`;
Expand Up @@ -6,7 +6,7 @@ propComponents: ['TextInput']
typescript: true
---

import { TextInput } from '@patternfly/react-core';
import { TextInput, Button } from '@patternfly/react-core';

## Examples
```js title=Basic
Expand Down Expand Up @@ -79,3 +79,18 @@ class InvalidTextInput extends React.Component {
}
}
```

```js title=Select-text-using-ref
import React from 'react';
import { TextInput, Button } from '@patternfly/react-core';

TextInputSelectAll = () => {
const [value, setValue] = React.useState('select all on click');
const ref = React.useRef(null);
return (
<React.Fragment>
<TextInput ref={ref} value={value} onFocus={() => ref && ref.current && ref.current.select()} onChange={value => setValue(value)} aria-label="select-all" />
</React.Fragment>
)
}
```
1 change: 1 addition & 0 deletions packages/patternfly-4/react-core/src/helpers/index.ts
Expand Up @@ -3,3 +3,4 @@ export * from './util';
export * from './constants';
export * from './htmlConstants';
export * from './typeUtils';
export * from './withInnerRef';
7 changes: 7 additions & 0 deletions packages/patternfly-4/react-core/src/helpers/withInnerRef.tsx
@@ -0,0 +1,7 @@
import * as React from 'react';

export function withInnerRef<R, P extends { innerRef?: React.Ref<R> }>(
WrappedComponent: React.ComponentType<P>
): React.ForwardRefExoticComponent<React.PropsWithoutRef<P> & React.RefAttributes<R>> {
return React.forwardRef<R, P>((props, ref) => <WrappedComponent {...props} innerRef={ref} />);
}

0 comments on commit bad2b9a

Please sign in to comment.