Skip to content

Commit

Permalink
feat: Add optional bottom guard for rendering suggestion list (#339)
Browse files Browse the repository at this point in the history
* adds guard for mentions suggestions list clipped by bottom of window

* add guard for suggestionlist without portals

* Add prop so that the rendering above is opt in

* Add example for bottom guard
  • Loading branch information
hyan7 authored and jfschwarz committed Oct 9, 2019
1 parent d605c85 commit 38278e0
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 21 deletions.
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 @@ -636,13 +638,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 @@ -651,23 +662,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 @@ -678,15 +682,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

1 comment on commit 38278e0

@vercel
Copy link

@vercel vercel bot commented on 38278e0 Oct 9, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.