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

Add optional bottom guard for rendering suggestion list #339

Merged
merged 6 commits into from
Oct 9, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 10 additions & 9 deletions README.md
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,16 @@ 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 |
| singleLine | boolean | `false` | Renders a single line text input instead of a textarea, if set to `true` |
| onBlur | function (event, clickedSuggestion) | empty function | Passes `true` as second argument if the blur was caused by a mousedown on a suggestion |
| allowSpaceInQuery | boolean | false | Keep suggestions open even if the user separates keywords with spaces. |
| suggestionsPortalHost | DOM Element | undefined | Render suggestions into the DOM in the supplied host element. |
| inputRef | React ref | undefined | Accepts a React ref to forward to the underlying input element |
| 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 |
| singleLine | boolean | `false` | Renders a single line text input instead of a textarea, if set to `true` |
| onBlur | function (event, clickedSuggestion) | empty function | Passes `true` as second argument if the blur was caused by a mousedown on a suggestion |
| allowSpaceInQuery | boolean | false | Keep suggestions open even if the user separates keywords with spaces. |
| suggestionsPortalHost | DOM Element | undefined | Render suggestions into the DOM in the supplied host element. |
| inputRef | React ref | undefined | Accepts a React ref to forward to the underlying input element |
| allowSuggestionsAboveCursor | boolean | false | Renders the SuggestionList above the cursor if there is not enough space below |

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

Expand Down
70 changes: 70 additions & 0 deletions demo/src/examples/BottomGuard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React from 'react'
import { Mention, MentionsInput } from '../../../src'

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

function BottomGuard({ value, data, onChange, onAdd }) {
return (
<div
id="suggestionPortal"
style={{
height: '400px',
}}
ref={el => {
container = el
}}
>
<h3>Bottom guard example</h3>
<p>
Note that the bottom input will open the suggestions list above the cursor
</p>
<div
style={{
position: 'absolute',
height: '300px',
width: '400px',
overflow: 'auto',
border: '1px solid green',
padding: '8px',
}}
>
<MentionsInput
value={value}
onChange={onChange}
style={defaultStyle}
placeholder={"Mention people using '@'"}
suggestionsPortalHost={container}
allowSuggestionsAboveCursor={true}
>
<Mention data={data} onAdd={onAdd} style={defaultMentionStyle} />
</MentionsInput>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<br/>
<MentionsInput
value={value}
onChange={onChange}
style={defaultStyle}
placeholder={"Mention people using '@'"}
suggestionsPortalHost={container}
allowSuggestionsAboveCursor={true}
>
<Mention data={data} onAdd={onAdd} style={defaultMentionStyle} />
</MentionsInput>
</div>
</div>
)
}

const asExample = provideExampleValue('')

export default asExample(BottomGuard)
2 changes: 2 additions & 0 deletions demo/src/examples/Examples.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import MultipleTrigger from './MultipleTrigger'
import Scrollable from './Scrollable'
import SingleLine from './SingleLine'
import SuggestionPortal from './SuggestionPortal'
import BottomGuard from "./BottomGuard";

const users = [
{
Expand Down Expand Up @@ -56,6 +57,7 @@ export default function Examples() {
<AsyncGithubUserMentions data={users} />
<Emojis data={users} />
<SuggestionPortal data={users} />
<BottomGuard data={users} />
</div>
</EnhancerProvider>
)
Expand Down
48 changes: 36 additions & 12 deletions src/MentionsInput.js
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ const propTypes = {
singleLine: PropTypes.bool,
allowSpaceInQuery: PropTypes.bool,
EXPERIMENTAL_cutCopyPaste: PropTypes.bool,
allowSuggestionsAboveCursor: PropTypes.bool,

value: PropTypes.string,
onKeyDown: PropTypes.func,
Expand Down Expand Up @@ -103,6 +104,7 @@ class MentionsInput extends React.Component {

static defaultProps = {
singleLine: false,
allowSuggestionsAboveCursor: false,
onKeyDown: () => null,
onSelect: () => null,
onBlur: () => null,
Expand Down Expand Up @@ -623,13 +625,22 @@ class MentionsInput extends React.Component {

updateSuggestionsPosition = () => {
let { caretPosition } = this.state
const { suggestionsPortalHost, allowSuggestionsAboveCursor } = this.props

if (!caretPosition || !this.suggestionsRef) {
return
}

let suggestions = ReactDOM.findDOMNode(this.suggestionsRef)
let highlighter = ReactDOM.findDOMNode(this.highlighterRef)
// first get viewport-relative position (highlighter is offsetParent of caret):
const caretOffsetParentRect = highlighter.getBoundingClientRect()
const caretHeight = getComputedStyleLengthProp(highlighter, 'font-size')
const viewportRelative = {
left: caretOffsetParentRect.left + caretPosition.left,
top: caretOffsetParentRect.top + caretPosition.top + caretHeight,
}
const viewportHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);

if (!suggestions) {
return
Expand All @@ -638,23 +649,16 @@ class MentionsInput extends React.Component {
let position = {}

// if suggestions menu is in a portal, update position to be releative to its portal node
if (this.props.suggestionsPortalHost) {
// first get viewport-relative position (highlighter is offsetParent of caret):
const caretOffsetParentRect = highlighter.getBoundingClientRect()
const caretHeight = getComputedStyleLengthProp(highlighter, 'font-size')
const viewportRelative = {
left: caretOffsetParentRect.left + caretPosition.left,
top: caretOffsetParentRect.top + caretPosition.top + caretHeight,
}
if (suggestionsPortalHost) {
position.position = 'fixed'
let left = viewportRelative.left
position.top = viewportRelative.top
let top = viewportRelative.top
// absolute/fixed positioned elements are positioned according to their entire box including margins; so we remove margins here:
left -= getComputedStyleLengthProp(suggestions, 'margin-left')
position.top -= getComputedStyleLengthProp(suggestions, 'margin-top')
top -= getComputedStyleLengthProp(suggestions, 'margin-top')
// take into account highlighter/textinput scrolling:
left -= highlighter.scrollLeft
position.top -= highlighter.scrollTop
top -= highlighter.scrollTop
// guard for mentions suggestions list clipped by right edge of window
const viewportWidth = Math.max(
document.documentElement.clientWidth,
Expand All @@ -665,15 +669,35 @@ class MentionsInput extends React.Component {
} else {
position.left = left
}
// guard for mentions suggestions list clipped by bottom edge of window if allowSuggestionsAboveCursor set to true.
// Move the list up above the caret if it's getting cut off by the bottom of the window, provided that the list height
// is small enough to NOT cover up the caret
if (allowSuggestionsAboveCursor &&
top + suggestions.offsetHeight > viewportHeight &&
suggestions.offsetHeight < top - caretHeight) {
position.top = Math.max(0, top - suggestions.offsetHeight - caretHeight)
} else {
position.top = top
}
} else {
let left = caretPosition.left - highlighter.scrollLeft
let top = caretPosition.top - highlighter.scrollTop
// guard for mentions suggestions list clipped by right edge of window
if (left + suggestions.offsetWidth > this.containerRef.offsetWidth) {
position.right = 0
} else {
position.left = left
}
position.top = caretPosition.top - highlighter.scrollTop
// guard for mentions suggestions list clipped by bottom edge of window if allowSuggestionsAboveCursor set to true.
// move the list up above the caret if it's getting cut off by the bottom of the window, provided that the list height
// is small enough to NOT cover up the caret
if (allowSuggestionsAboveCursor &&
viewportRelative.top - highlighter.scrollTop + suggestions.offsetHeight > viewportHeight &&
suggestions.offsetHeight < caretOffsetParentRect.top - caretHeight - highlighter.scrollTop) {
position.top = top - suggestions.offsetHeight - caretHeight
} else {
position.top = top
}
}

if (isEqual(position, this.state.suggestionsPosition)) {
Expand Down