Skip to content

Commit

Permalink
Merge pull request #755 from folio-org/stcom-372-textfield-expose-ref…
Browse files Browse the repository at this point in the history
…-prop

STCOM-372 expose ref prop for TextField - "inputRef"
  • Loading branch information
JohnC-80 committed Dec 5, 2018
2 parents 7a4de2e + 5da5a42 commit fa4f916
Show file tree
Hide file tree
Showing 11 changed files with 175 additions and 24 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Change history for stripes-components

## 4.6.0 (In Progress)

* Add `inputRef` prop to `<TextField>`. Part of STCOM-372.
* Deprecate `getInput()` and `focusInput()` methods of `TextField`. Part of STCOM-372.
* Add [documentation](lib/TextField/Readme.md) for `<TextField>`.

## [4.5.0](https://github.com/folio-org/stripes-components/tree/v4.5.0) (2018-11-29)
[Full Changelog](https://github.com/folio-org/stripes-components/compare/v4.4.0...v4.5.0)

Expand Down
21 changes: 10 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,15 @@ This software is distributed under the terms of the Apache License,
Version 2.0. See the file "[LICENSE](LICENSE)" for more information.

<!-- md2toc -l 2 README.md -->
* [Introduction](#introduction)
* [Component categories](#component-categories)
* [Links to documentation of specific components and utilities](#links-to-documentation-of-specific-components-and-utilities)
* [Patterns - UI Recipes](#patterns)
* [Testing](#testing)
* [FAQ](#faq)
* [Migration Paths](MIGRATIONPATHS.md)
* [Change Management](CHANGEMANAGEMENT.md)
* [To be documented](#to-be-documented)
* [Additional information](#additional-information)
- [stripes-components](#stripes-components)
- [Introduction](#introduction)
- [Component categories](#component-categories)
- [Links to documentation of specific components and utilities](#links-to-documentation-of-specific-components-and-utilities)
- [Patterns](#patterns)
- [Testing](#testing)
- [FAQ](#faq)
- [To be documented](#to-be-documented)
- [Additional information](#additional-information)


## Introduction
Expand Down Expand Up @@ -108,7 +107,7 @@ Component | doc | categories
[`<SRStatus>`](lib/SRStatus) | [doc](lib/SRStatus/readme.md) | accessibility, user-feedback
[`<TabButton>`](lib/TabButton) | | control
[`<TextArea>`](lib/TextArea) | | control
[`<TextField>`](lib/TextField) | | control
[`<TextField>`](lib/TextField) | [doc](lib/TextField/readme.md) | control
[`<Timepicker>`](lib/Timepicker) | [doc](lib/Timepicker/readme.md) | control

There are also various [utility _functions_](util) (as opposed to React components), which are [documented separately](util/README.md).
Expand Down
2 changes: 1 addition & 1 deletion lib/AutoSuggest/AutoSuggest.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ class AutoSuggest extends React.Component {
required,
label,
validationEnabled,
ref: textfieldRef,
inputRef: textfieldRef,
id: testId,
error,
};
Expand Down
7 changes: 5 additions & 2 deletions lib/Datepicker/Datepicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const propTypes = {
hideOnChoose: PropTypes.bool,
id: PropTypes.string,
input: PropTypes.object,
inputRef: PropTypes.object,
label: PropTypes.node,
locale: PropTypes.string,
meta: PropTypes.object,
Expand All @@ -47,6 +48,7 @@ const defaultProps = {
backendDateStandard: 'ISO8601',
hideOnChoose: true,
screenReaderMessage: '',
inputRef: React.createRef(),
tether: {
attachment: 'top center',
targetAttachment: 'bottom center',
Expand Down Expand Up @@ -77,6 +79,7 @@ class Datepicker extends React.Component {
this.picker = null;
this.container = null;
this.srSpace = null;
this.textField = props.inputRef;

this.dbhideCalendar = debounce(this.hideCalendar, 10);

Expand Down Expand Up @@ -283,7 +286,7 @@ class Datepicker extends React.Component {
e.preventDefault();

if (this.props.onChange) { this.props.onChange(e); }
this.textfield.current.getInput().focus();
this.textfield.current.focus();

if (this.props.input) {
if (moment(stringDate, this._dateFormat, true).isValid()) {
Expand Down Expand Up @@ -496,7 +499,7 @@ class Datepicker extends React.Component {
required,
autoFocus,
'id': this.testId,
'ref': this.textfield,
'inputRef': this.textfield,
'onChange': this.handleSetDate,
'value': this.getPresentedValue(),
'aria-label': ariaLabel,
Expand Down
1 change: 1 addition & 0 deletions lib/SearchField/SearchField.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const propTypes = {
clearSearchId: PropTypes.string,
id: PropTypes.string,
inputClass: PropTypes.string,
inputRef: PropTypes.object,
loading: PropTypes.bool,
onChange: PropTypes.func,
onChangeIndex: PropTypes.func,
Expand Down
109 changes: 109 additions & 0 deletions lib/TextField/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# TextField
Text input component with label and validation controls.

## Common Usage with a form framework...
Form state frameworks such as `react-final-form` provide `<Field>` components that manage form state. `<Field>` components often automatically apply particular props such as `onChange` and `value` under the hood, so you don't have to.
```
import { TextField } from '@folio/stripes/components';
import { Field } from 'react-final-form';
...
<Field name="username" component={TextField} />
```

## Basic vanilla usage (controlled)
If used without a form state manager, you will have to supply your own state and handlers.
```
import { TextField } from '@folio/stripes/components';
...
<TextField
label="Username"
value={this.state.username}
onChange={this.handleChange}
/>
```

## Basic Props
Name | type | description | default | required
--- | --- | --- | --- | ---
`ariaLabel` | string | Applies an `aria-label` attribute - prefer visible `label` prop. Use only if the labeling case warrants. | |
`ariaLabelledBy` | string | Applies an `aria-labelledBy` attribute to the `<input>` - prefer visible `label` prop. Use only as the labeling case may warrant. | |
`autoComplete` | bool | If this prop is `true`, control will automatically focus on mount | |
`autoFocus` | bool | If this prop is `true`, control will automatically focus on mount | |
`clearFieldId` | string | Id to apply to clear field button. | |
`disabled` | bool | Disables the TextField. | |
`endControl` | element | Element to render as a tail-end control to the textfield. | |
`hasClearIcon` | bool | If `true` and value is defined, `<TextField>` will render a button for easy clearing of its value. | `true` |
`id` | string | Sets the `id` html attribute on the control | |
`inputRef` | object or func | Supplies a ref to the rendered `<input>` | |
`label` | string | If provided, will render a `<label>` tag with an `htmlFor` attribute directed at the provided `id` prop (an id is generated if not available.) | |
`name` | string | `name` attribute of input | |
`placeholder` | string | `placeholder` attribute of the input. Appears as gray assistive text if no value is present. | |
`readOnly` | bool | Apply `readonly` attribute to `<input>` | |
`required` | bool | Apply `required` attribute to `<input>` | |
`startControl` | element | Element to render as a leading control to the textfield. | |
`type` | string | Type attribute of `<input>` | "text" |
`value` | string or number | Sets the value for the control. | |

## Callback Props
Name | type | description | default | required
--- | --- | --- | --- | ---
`onBlur` | func | Listener for `onBlur` event. | |
`onChange` | func | Callback fired when input value is changed. | |
`onClearField` | func | Callback when input is cleared using the `clearIcon` control | |
`onFocus` | func | Callback fired when input is focused | |

## Validation Props
Name | type | description | default | required
--- | --- | --- | --- | ---
`dirty` | bool | Mark 'true' when value has changes. | |
`error` | node | Error string to display after textfield in case of validation error. | |
`loading` | bool | Applies a loading animation - useful for async validation or loading search results. | |
`valid` | bool | Applies success validation style to `<input>` | |
`validStylesEnabled` | bool | When set to false, `<input>` will not display validation styles. | `true` |
`warning` | node | Validation warning. Renders node below textfield with warning styling. | |

## Style Props
Name | type | description | default | required
--- | --- | --- | --- | ---
`className` | string | Apply a custom class name to the root element. | |
`focusedClass` | string | CSS class to apply on input element's focus event. | |
`fullWidth` | bool | If `true` `<TextField>` will fill its container. | |
`inputClass` | string | Custom CSS class to apply to the input | |
`inputStyle` | object | Applies an inline style to the `<input>` | |
`marginBottom0` | bool | Remove bottom margin of styling. | |
`noBorder` | bool | Renders `<input>` borderless. | |

## Accessible Labeling
Text inputs should always have an appropriate label so that will be announced through screen-readers when the TextField is focused. This can be accomplished in a few different ways:
### Label prop
The most common use case for form labeling.
```
<TextField label="Username" />
```
### AriaLabel prop
If the design case requires a **visually hidden label**
```
<TextField ariaLabel="Username" />
```

### AriaLabelledBy prop
If the label is designed visible, but needs to exist outside of `<TextField>`'s root element.
```
<div id="myLabel">Username</div>
<TextField ariaLabelledBy="myLabel" />
```

## Focus Management
Requirements may call for programmatic focus of `<TextField>`'s `<input>`.
```
this.input = React.createRef();
...
// function to call in order to focus the input.
focusBarcode() {
if (this.input.current) {
this.input.current.focus();
}
}
...
<TextField label="barcode" inputRef={this.input} />
```
30 changes: 29 additions & 1 deletion lib/TextField/TextField.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ class TextField extends Component {
* Apply an ARIA label.
*/
ariaLabel: PropTypes.string,
/**
* Id of element that contains an external label.
*/
ariaLabelledBy: PropTypes.string,
/**
* Toggle browser autocomplete.
*/
Expand Down Expand Up @@ -69,6 +73,10 @@ class TextField extends Component {
* Class applied to the input.
*/
inputClass: PropTypes.string,
/**
* Ref to input - used for app-level focus management.
*/
inputRef: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
/**
* String of preset styles to apply to textfield. possible values: noBorder
*/
Expand Down Expand Up @@ -152,6 +160,7 @@ class TextField extends Component {

static defaultProps = {
hasClearIcon: true,
inputRef: React.createRef(),
type: 'text',
validationEnabled: true,
value: ''
Expand All @@ -173,6 +182,16 @@ class TextField extends Component {
value: props.value
};

// typecheck for ref callbacks...
if (typeof this.props.inputRef === 'function') {
this.input = (ref) => {
props.inputRef(ref);
this.input.current = ref;
};
} else {
this.input = props.inputRef;
}

// if no id has been supplied, generate a unique one
if (Object.prototype.hasOwnProperty.call(props, 'id')) {
this.testId = props.id;
Expand All @@ -192,6 +211,8 @@ class TextField extends Component {
}

getInput() {
console.warn(`[DEPRECATION] - the "getInput()" method for TextField is deprecated.
Use the "inputRef" prop instead to obtain a ref to the input.`);
return this.input.current;
}

Expand All @@ -211,6 +232,8 @@ class TextField extends Component {
}

focusInput() {
console.warn(`[DEPRECATION] - the "focusInput()" method for TextField is deprecated.
Use the "inputRef" prop instead to obtain a ref to the input.`);
this.input.current.focus();
}

Expand Down Expand Up @@ -253,7 +276,9 @@ class TextField extends Component {
}

// Set focus on input again
this.focusInput();
if (this.input.current) {
this.input.current.focus();
}
});
}

Expand Down Expand Up @@ -290,13 +315,15 @@ class TextField extends Component {
this.props,
[
'ariaLabel',
'ariaLabelledBy',
'clearFieldId',
'dirty',
'endControl',
'focusedClass',
'fullWidth',
'hasClearIcon',
'inputClass',
'inputRef',
'label',
'loading',
'marginBottom0',
Expand All @@ -315,6 +342,7 @@ class TextField extends Component {
<input
{...inputCustom}
aria-label={this.props.ariaLabel}
aria-labelledBy={this.props.ariaLabelledBy}
aria-required={required}
aria-invalid={!!(this.props.error)}
autoComplete={this.props.autoComplete}
Expand Down
5 changes: 5 additions & 0 deletions lib/TextField/stories/BasicUsage.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ const BasicUsage = () => (
label="Field with label"
placeholder="Placeholder Text"
/>
<div id="myLabel">TaxtField</div>
<TextField
ariaLabelledBy="myLabel"
placeholder="Aria-labelledby Text"
/>
<form>
<TextField
label="Required field"
Expand Down
6 changes: 3 additions & 3 deletions lib/TextField/tests/TextField-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,17 +205,17 @@ describe('TextField', () => {
});

describe('getInput()', () => {
let textFieldComponent;
const textFieldComponent = React.createRef();

beforeEach(async () => {
await mountWithContext(
<TextField ref={(ref) => { textFieldComponent = ref; }} />
<TextField inputRef={textFieldComponent} />
);
});

describe('focusing the field programmatically', () => {
beforeEach(async () => {
await textFieldComponent.getInput().focus();
await textFieldComponent.current.focus();
});

it('focuses the input', () => {
Expand Down
10 changes: 5 additions & 5 deletions lib/Timepicker/TimeDropdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ class TimeDropdown extends React.Component {
}

if (!nextProps.visible && nextProps.visible !== this.props.visible) {
this._focusTimeout = setTimeout(() => { this.props.mainControl.getInput().focus(); }, 20);
this._focusTimeout = setTimeout(() => { this.props.mainControl.current.focus(); }, 20);
}
}

Expand Down Expand Up @@ -211,7 +211,7 @@ class TimeDropdown extends React.Component {
// refocus the datepicker textfield if it's tabbed out...
this.props.onBlur(() => {
setTimeout(() => {
this.props.mainControl.getInput().focus();
this.props.mainControl.focus();
}, 20);
});
}
Expand All @@ -222,7 +222,7 @@ class TimeDropdown extends React.Component {
// refocus the datepicker textfield if the users shift-tabs out...
this.props.onBlur(() => {
setTimeout(() => {
this.props.mainControl.getInput().focus();
this.props.mainControl.focus();
}, 20);
});
}
Expand Down Expand Up @@ -347,7 +347,7 @@ class TimeDropdown extends React.Component {
<Col xs={4}>
<TextField
aria-label="hours"
ref={(h) => { this.hourField = h; }}
inputRef={(h) => { this.hourField = h; }}
placeholder="HH"
onKeyDown={this.hoursHandleKeyDown}
tabIndex={this.props.visible ? '0' : '-1'}
Expand All @@ -365,7 +365,7 @@ class TimeDropdown extends React.Component {
<Col xs={4}>
<TextField
aria-label="minutes"
ref={(m) => { this.minuteField = m; }}
inputRef={(m) => { this.minuteField = m; }}
placeholder="MM"
tabIndex={this.props.visible ? '0' : '-1'}
type="number"
Expand Down
Loading

0 comments on commit fa4f916

Please sign in to comment.