Skip to content

Commit

Permalink
feat(ReactTags): Add inputFieldPosition prop to Specify position of i…
Browse files Browse the repository at this point in the history
…nput field relative to tags (react-bootstrap#491)

fixes react-bootstrap#297, react-bootstrap#233
  • Loading branch information
atiqueansari1987 authored and ad1992 committed Mar 2, 2019
1 parent 3c85e6d commit 75fd6ad
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 11 deletions.
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ Option | Type | Default | Description
|[`id`](#idOption) | `String` | `undefined` | The `id` attribute added to the input
|[`maxLength`](#maxLength) | `Number` | `Infinity` | The `maxLength` attribute added to the input
|[`inline`](#inline) | `Boolean` | `true` | Render input field and selected tags in-line
|[`inputFieldPosition`](#inputFieldPosition) | `String` | `inline` | Specify position of input field relative to tags
|[`allowUnique`](#allowUnique) | `Boolean` | `true` | Boolean value to control whether tags should be unqiue
|[`allowDragDrop`](#allowDragDrop) | `Boolean` | `true` | Boolean value to control whether tags should have drag-n-drop features enabled
|[`renderSuggestion`](#renderSuggestion) | `Function` | `undefined` | Render prop for rendering your own suggestions
Expand All @@ -176,7 +177,7 @@ const tags = [ { id: "1", text: "Apples" } ]
const tags = [ { id: "1", name: "Apples" } ]

// With className
const tags = [ { id: "1", text: "Apples", className: 'red'} ]
const tags = [ { id: "1", text: "Apples", className: 'red'} ]
```

<a name="suggestionsOption"></a>
Expand Down Expand Up @@ -441,6 +442,36 @@ The inline attributes decides whether the input fields and selected tags will be

![img](docs/inline-false.png)

_This attribute is deprecated and will be removed in v7.x.x, see [inputFieldPosition](#inputFieldPosition)._

<a name="inputFieldPosition"></a>
##### inputFieldPosition (optional, defaults to `inline`)
The inputFieldPosition attribute decides the positioning of the input field relative to the tags. Can be one of `inline`, `top` or `bottom`.

```
<ReactTags
inputFieldPosition="inline"
...>
```

![img](docs/input-field-position-inline.png)

```
<ReactTags
inputFieldPosition="top"
...>
```

![img](docs/input-field-position-top.png)

```
<ReactTags
inputFieldPosition="bottom"
...>
```

![img](docs/input-field-position-bottom.png)

<a name="allowUnique"></a>
#### allowUnique (optional, defaults to `true`)
This prop controls whether tags should be unique.
Expand Down
61 changes: 60 additions & 1 deletion __tests__/reactTags.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { expect } from 'chai';
import { mount, shallow } from 'enzyme';
import { spy } from 'sinon';
import { spy, stub } from 'sinon';
import noop from 'lodash/noop';

import { WithContext as ReactTags } from '../src/components/ReactTags';
Expand All @@ -10,6 +10,7 @@ import {
KEYS,
DEFAULT_PLACEHOLDER,
DEFAULT_LABEL_FIELD,
INPUT_FIELD_POSITIONS,
} from '../src/components/constants';

/* eslint-disable no-console */
Expand Down Expand Up @@ -43,6 +44,7 @@ describe('Test ReactTags', () => {
autofocus: true,
labelField: DEFAULT_LABEL_FIELD,
inline: true,
inputFieldPosition: INPUT_FIELD_POSITIONS.INLINE,
handleDelete: noop,
handleAddition: noop,
allowDeleteFromEmptyInput: true,
Expand Down Expand Up @@ -639,3 +641,60 @@ test('should allow duplicate tags when allowUnique is false', () => {
},
]);
});

describe('Test inputFieldPosition', () => {
test('should display input field and tags inline when "inputFieldPosition" is inline', () => {
const $el = mount(
mockItem({
inputFieldPosition: INPUT_FIELD_POSITIONS.INLINE,
})
);

const $tagContainer = $el.find('.ReactTags__selected');
const childLength = $tagContainer.children().length;
expect(
$tagContainer.children().get(childLength - 1).props.className
).to.equal('ReactTags__tagInput');
});

test('should display input field above tags when "inputFieldPosition" is top', () => {
const $el = mount(
mockItem({
inputFieldPosition: INPUT_FIELD_POSITIONS.TOP,
})
);

const $tagContainer = $el.find('.ReactTags__tags');
expect($tagContainer.children().get(0).props.className).to.equal(
'ReactTags__tagInput'
);
});

test('should display input field below tags when "inputFieldPosition" is bottom', () => {
const $el = mount(
mockItem({
inputFieldPosition: INPUT_FIELD_POSITIONS.BOTTOM,
})
);

const $tagContainer = $el.find('.ReactTags__tags');
expect($tagContainer.children().get(1).props.className).to.equal(
'ReactTags__tagInput'
);
});

test('should show console warning when "inline" is false', () => {
const consoleWarnStub = stub(console, 'warn');

mount(
mockItem({
inline: false,
})
);

expect(consoleWarnStub.calledOnce ).to.be.true;
expect(consoleWarnStub.calledWithExactly('[Deprecation] The inline attribute is deprecated and will be removed in v7.x.x, please use inputFieldPosition instead.')).to.be.true;

consoleWarnStub.restore();
});
});
Binary file added docs/input-field-position-bottom.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/input-field-position-inline.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/input-field-position-top.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 27 additions & 9 deletions src/components/ReactTags.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
DEFAULT_PLACEHOLDER,
DEFAULT_CLASSNAMES,
DEFAULT_LABEL_FIELD,
INPUT_FIELD_POSITIONS,
} from './constants';

const updateClassNames = memoizeOne((classNames) =>
Expand All @@ -37,7 +38,8 @@ class ReactTags extends Component {
),
delimiters: PropTypes.arrayOf(PropTypes.number),
autofocus: PropTypes.bool,
inline: PropTypes.bool,
inline: PropTypes.bool, // TODO: Remove in v7.x.x
inputFieldPosition: PropTypes.oneOf([INPUT_FIELD_POSITIONS.INLINE, INPUT_FIELD_POSITIONS.TOP, INPUT_FIELD_POSITIONS.BOTTOM]),
handleDelete: PropTypes.func,
handleAddition: PropTypes.func,
handleDrag: PropTypes.func,
Expand Down Expand Up @@ -76,7 +78,8 @@ class ReactTags extends Component {
suggestions: [],
delimiters: [KEYS.ENTER, KEYS.TAB],
autofocus: true,
inline: true,
inline: true, // TODO: Remove in v7.x.x
inputFieldPosition: INPUT_FIELD_POSITIONS.INLINE,
handleDelete: noop,
handleAddition: noop,
allowDeleteFromEmptyInput: true,
Expand All @@ -91,6 +94,13 @@ class ReactTags extends Component {

constructor(props) {
super(props);

if (!props.inline) {
/* eslint-disable no-console */
console.warn('[Deprecation] The inline attribute is deprecated and will be removed in v7.x.x, please use inputFieldPosition instead.');
/* eslint-enable no-console */
}

const { suggestions, classNames } = props;
this.state = {
suggestions,
Expand Down Expand Up @@ -392,11 +402,18 @@ class ReactTags extends Component {
// get the suggestions for the given query
const query = this.state.query.trim(),
selectedIndex = this.state.selectedIndex,
suggestions = this.state.suggestions,
placeholder = this.props.placeholder,
inputName = this.props.name,
inputId = this.props.id,
maxLength = this.props.maxLength;
suggestions = this.state.suggestions;

const {
placeholder,
name: inputName,
id: inputId,
maxLength,
inline,
inputFieldPosition,
} = this.props;

const position = !inline ? INPUT_FIELD_POSITIONS.BOTTOM : inputFieldPosition;

const tagInput = !this.props.readOnly ? (
<div className={this.state.classNames.tagInput}>
Expand Down Expand Up @@ -437,11 +454,12 @@ class ReactTags extends Component {

return (
<div className={ClassNames(this.state.classNames.tags, 'react-tags-wrapper')}>
{position === INPUT_FIELD_POSITIONS.TOP && tagInput}
<div className={this.state.classNames.selected}>
{tagItems}
{this.props.inline && tagInput}
{position === INPUT_FIELD_POSITIONS.INLINE && tagInput}
</div>
{!this.props.inline && tagInput}
{position === INPUT_FIELD_POSITIONS.BOTTOM && tagInput}
</div>
);
}
Expand Down
6 changes: 6 additions & 0 deletions src/components/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,9 @@ export const DEFAULT_CLASSNAMES = {
suggestions: 'ReactTags__suggestions',
activeSuggestion: 'ReactTags__activeSuggestion',
};

export const INPUT_FIELD_POSITIONS = {
INLINE: 'inline',
TOP: 'top',
BOTTOM: 'bottom',
};

0 comments on commit 75fd6ad

Please sign in to comment.