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

Added RenderInput #623

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/six-actors-clean.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-mentions": minor
---

Added RenderInput
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ You can find more examples here: [demo/src/examples](https://github.com/signavio
The `MentionsInput` supports the following props for configuring the widget:

| Prop name | Type | Default value | Description |
| --------------------------- | ------------------------------------------------------- | -------------- | -------------------------------------------------------------------------------------- |
|-----------------------------|---------------------------------------------------------|----------------|----------------------------------------------------------------------------------------|
| value | string | `''` | The value containing markup for mentions |
| onChange | function (event, newValue, newPlainTextValue, mentions) | empty function | A callback that is invoked when the user changes the value in the mentions input |
| onKeyDown | function (event) | empty function | A callback that is invoked when the user presses a key in the mentions input |
Expand All @@ -63,6 +63,7 @@ The `MentionsInput` supports the following props for configuring the widget:
| forceSuggestionsAboveCursor | boolean | false | Forces the SuggestionList to be rendered above the cursor |
| a11ySuggestionsListLabel | string | `''` | This label would be exposed to screen readers when suggestion popup appears |
| customSuggestionsContainer | function(children) | empty function | Allows customizing the container of the suggestions |
| renderInput | React component | undefined | Allows customizing the input element |

Each data source is configured using a `Mention` component, which has the following props:

Expand Down
2 changes: 2 additions & 0 deletions demo/src/examples/Examples.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import SingleLineIgnoringAccents from './SingleLineIgnoringAccents'
import SuggestionPortal from './SuggestionPortal'
import BottomGuard from './BottomGuard'
import CustomSuggestionsContainer from './CustomSuggestionsContainer'
import RendererInput from './RenderInput'

const users = [
{
Expand Down Expand Up @@ -90,6 +91,7 @@ export default function Examples() {
<SuggestionPortal data={users} />
<BottomGuard data={users} />
<CustomSuggestionsContainer data={users} />
<RendererInput data={users} />
</div>
</StylesViaJss>
)
Expand Down
37 changes: 37 additions & 0 deletions demo/src/examples/RenderInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react'

import { Mention, MentionsInput } from '../../../src'

import { provideExampleValue } from './higher-order'
import defaultStyle from './defaultStyle'
import defaultMentionStyle from './defaultMentionStyle'

const CustomRenderer = React.forwardRef((props, ref) => (
<label>
I am a custom input!
<input type="text" {...props} ref={ref} />
</label>
))

function RenderInput({ value, data, onChange, onAdd }) {
return (
<div className="single-line">
<h3>Single line input</h3>

<MentionsInput
renderInput={CustomRenderer}
value={value}
onChange={onChange}
style={defaultStyle}
placeholder={"Mention people using '@'"}
a11ySuggestionsListLabel={'Suggested mentions'}
>
<Mention data={data} onAdd={onAdd} style={defaultMentionStyle} />
</MentionsInput>
</div>
)
}

const asExample = provideExampleValue('')

export default asExample(RenderInput)
45 changes: 27 additions & 18 deletions src/MentionsInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ const propTypes = {
PropTypes.element,
PropTypes.arrayOf(PropTypes.element),
]).isRequired,

renderInput: PropTypes.elementType,
}

class MentionsInput extends React.Component {
Expand Down Expand Up @@ -183,7 +185,7 @@ class MentionsInput extends React.Component {
)
}

setContainerElement = (el) => {
setContainerElement = el => {
this.containerElement = el
}

Expand Down Expand Up @@ -229,28 +231,35 @@ class MentionsInput extends React.Component {
}

renderControl = () => {
let { singleLine, style } = this.props
let { singleLine, style, renderInput } = this.props
let inputProps = this.getInputProps()

return (
<div {...style('control')}>
{this.renderHighlighter()}
{singleLine
{renderInput
? this.renderCustom(inputProps)
: singleLine
? this.renderInput(inputProps)
: this.renderTextarea(inputProps)}
</div>
)
}

renderInput = (props) => {
renderCustom = props => {
let { renderInput: RenderInput } = this.props
return <RenderInput ref={this.setInputRef} {...props} />
}

renderInput = props => {
return <input type="text" ref={this.setInputRef} {...props} />
}

renderTextarea = (props) => {
return <textarea autoFocus ref={this.setInputRef} {...props} />
renderTextarea = props => {
return <textarea ref={this.setInputRef} {...props} />
}

setInputRef = (el) => {
setInputRef = el => {
this.inputElement = el
const { inputRef } = this.props
if (typeof inputRef === 'function') {
Expand All @@ -260,7 +269,7 @@ class MentionsInput extends React.Component {
}
}

setSuggestionsElement = (el) => {
setSuggestionsElement = el => {
this.suggestionsElement = el
}

Expand Down Expand Up @@ -325,11 +334,11 @@ class MentionsInput extends React.Component {
)
}

setHighlighterElement = (el) => {
setHighlighterElement = el => {
this.highlighterElement = el
}

handleCaretPositionChange = (position) => {
handleCaretPositionChange = position => {
this.setState({ caretPosition: position })
}

Expand Down Expand Up @@ -499,7 +508,7 @@ class MentionsInput extends React.Component {
}

// Handle input element's change event
handleChange = (ev) => {
handleChange = ev => {
isComposing = false
if (isIE()) {
// if we are inside iframe, we need to find activeElement within its contentDocument
Expand Down Expand Up @@ -572,7 +581,7 @@ class MentionsInput extends React.Component {
}

// Handle input element's select event
handleSelect = (ev) => {
handleSelect = ev => {
// keep track of selection range / caret position
this.setState({
selectionStart: ev.target.selectionStart,
Expand All @@ -596,7 +605,7 @@ class MentionsInput extends React.Component {
this.props.onSelect(ev)
}

handleKeyDown = (ev) => {
handleKeyDown = ev => {
// do not intercept key events if the suggestions overlay is not shown
const suggestionsCount = countSuggestions(this.state.suggestions)

Expand Down Expand Up @@ -638,7 +647,7 @@ class MentionsInput extends React.Component {
}
}

shiftFocus = (delta) => {
shiftFocus = delta => {
const suggestionsCount = countSuggestions(this.state.suggestions)

this.setState({
Expand All @@ -654,7 +663,7 @@ class MentionsInput extends React.Component {
const { result, queryInfo } = Object.values(suggestions).reduce(
(acc, { results, queryInfo }) => [
...acc,
...results.map((result) => ({ result, queryInfo })),
...results.map(result => ({ result, queryInfo })),
],
[]
)[focusIndex]
Expand All @@ -666,7 +675,7 @@ class MentionsInput extends React.Component {
})
}

handleBlur = (ev) => {
handleBlur = ev => {
const clickedSuggestion = this._suggestionsMouseDown
this._suggestionsMouseDown = false

Expand All @@ -686,11 +695,11 @@ class MentionsInput extends React.Component {
this.props.onBlur(ev, clickedSuggestion)
}

handleSuggestionsMouseDown = (ev) => {
handleSuggestionsMouseDown = ev => {
this._suggestionsMouseDown = true
}

handleSuggestionsMouseEnter = (focusIndex) => {
handleSuggestionsMouseEnter = focusIndex => {
this.setState({
focusIndex,
scrollFocusedIntoView: false,
Expand Down
45 changes: 33 additions & 12 deletions src/MentionsInput.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@ describe('MentionsInput', () => {
expect(wrapper.find('input').length).toEqual(1)
})

it('should render a custom input when supplied.', () => {
wrapper.setProps({
renderInput: React.forwardRef((props, ref) => (
<div id="testDiv" {...props} ref={ref} />
)),
})

expect(wrapper.find('textarea').length).toEqual(0)
expect(wrapper.find('input').length).toEqual(0)
expect(wrapper.find('#testDiv').length).toEqual(1)
})

it.todo(
'should show a list of suggestions once the trigger key has been entered.'
)
Expand All @@ -51,7 +63,10 @@ describe('MentionsInput', () => {
it.todo('should be possible to close the suggestions with esc.')

it('should be able to handle sync responses from multiple mentions sources', () => {
const extraData = [{ id: 'a', value: 'A' }, { id: 'b', value: 'B' }]
const extraData = [
{ id: 'a', value: 'A' },
{ id: 'b', value: 'B' },
]

const wrapper = mount(
<MentionsInput value="@">
Expand All @@ -64,7 +79,10 @@ describe('MentionsInput', () => {
wrapper.find('textarea').simulate('select', {
target: { selectionStart: 1, selectionEnd: 1 },
})
wrapper.find('textarea').getDOMNode().setSelectionRange(1, 1)
wrapper
.find('textarea')
.getDOMNode()
.setSelectionRange(1, 1)

expect(
wrapper.find('SuggestionsOverlay').find('Suggestion').length
Expand Down Expand Up @@ -104,7 +122,7 @@ describe('MentionsInput', () => {
<div id="root">
<div
id="portalDiv"
ref={(el) => {
ref={el => {
portalNode = el
}}
>
Expand Down Expand Up @@ -133,16 +151,19 @@ describe('MentionsInput', () => {
})

it('should accept a custom regex attribute', () => {
const data = [{ id: 'aaaa', display: '@A' }, { id: 'bbbb', display: '@B' }]
const data = [
{ id: 'aaaa', display: '@A' },
{ id: 'bbbb', display: '@B' },
]
const wrapper = mount(
<MentionsInput value=":aaaa and :bbbb and :invalidId">
<Mention
trigger="@"
data={data}
markup=":__id__"
regex={/:(\S+)/}
displayTransform={(id) => {
let mention = data.find((item) => item.id === id)
displayTransform={id => {
let mention = data.find(item => item.id === id)
return mention ? mention.display : `:${id}`
}}
/>
Expand Down Expand Up @@ -216,7 +237,7 @@ describe('MentionsInput', () => {

it.each(['cut', 'copy'])(
'should include the whole mention for a "%s" event when the selection starts in one.',
(eventType) => {
eventType => {
const textarea = component.find('textarea')

const selectionStart = plainTextValue.indexOf('First') + 2
Expand Down Expand Up @@ -251,7 +272,7 @@ describe('MentionsInput', () => {

it.each(['cut', 'copy'])(
'should include the whole mention for a "%s" event when the selection ends in one.',
(eventType) => {
eventType => {
const textarea = component.find('textarea')

const selectionStart = 0
Expand Down Expand Up @@ -286,7 +307,7 @@ describe('MentionsInput', () => {

it.each(['cut', 'copy'])(
'should fallback to the browsers behavior if the "%s" event does not support clipboardData',
(eventType) => {
eventType => {
// IE 11 has no clipboardData attached to the event and only supports mime type "text"
// therefore, the new mechanism should ignore those events and let the browser handle them
const textarea = component.find('textarea')
Expand Down Expand Up @@ -380,7 +401,7 @@ describe('MentionsInput', () => {

const event = new Event('paste', { bubbles: true })
event.clipboardData = {
getData: jest.fn((type) =>
getData: jest.fn(type =>
type === 'text/react-mentions' ? pastedText : ''
),
}
Expand Down Expand Up @@ -408,7 +429,7 @@ describe('MentionsInput', () => {

const event = new Event('paste', { bubbles: true })
event.clipboardData = {
getData: jest.fn((type) => (type === 'text/plain' ? pastedText : '')),
getData: jest.fn(type => (type === 'text/plain' ? pastedText : '')),
}

expect(onChange).not.toHaveBeenCalled()
Expand All @@ -430,7 +451,7 @@ describe('MentionsInput', () => {
const event = new Event('paste', { bubbles: true })

event.clipboardData = {
getData: jest.fn((type) => (type === 'text/plain' ? pastedText : '')),
getData: jest.fn(type => (type === 'text/plain' ? pastedText : '')),
}

const onChange = jest.fn()
Expand Down