Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Update autocomplete design and scroll it correctly #419

Merged
merged 2 commits into from
Aug 24, 2016
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/autocomplete/AutocompleteProvider.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Q from 'q';
import React from 'react';

export default class AutocompleteProvider {
constructor(commandRegex?: RegExp, fuseOpts?: any) {
Expand Down Expand Up @@ -51,4 +52,9 @@ export default class AutocompleteProvider {
getName(): string {
return 'Default Provider';
}

renderCompletions(completions: [React.Component]): ?React.Component {
console.error('stub; should be implemented in subclasses');
return null;
}
}
8 changes: 7 additions & 1 deletion src/autocomplete/CommandProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export default class CommandProvider extends AutocompleteProvider {
}

getName() {
return 'Commands';
return '*️⃣ Commands';
}

static getInstance(): CommandProvider {
Expand All @@ -83,4 +83,10 @@ export default class CommandProvider extends AutocompleteProvider {

return instance;
}

renderCompletions(completions: [React.Component]): ?React.Component {
return <div className="mx_Autocomplete_Completion_container_block">
{completions}
</div>;
}
Copy link
Member

Choose a reason for hiding this comment

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

Given the superclass function here is taking the array and putting it in a div wrapper, the inheritance here seems a bit redundant, since it's doing this wrapping for the overridden function to clone it, add a class, return that and throw the original away.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I would agree, in the context that it's a bit of unnecessary OOPism. I do
admit I wrote it this way expecting it would be required later. (If there's
a base structure and each layout needs to customise it a bit.)

On Wed 17 Aug, 2016, 7:26 PM David Baker, notifications@github.com wrote:

In src/autocomplete/CommandProvider.js
#419 (comment)
:

@@ -83,4 +83,10 @@ export default class CommandProvider extends AutocompleteProvider {

     return instance;
 }
  • renderCompletions(completions: [React.Component]): ?React.Component {
  •    return React.cloneElement(super.renderCompletions(completions), {
    
  •        className: 'mx_Autocomplete_Completion_container_block',
    
  •    });
    
  • }

Given the superclass function here is taking the array and putting it in a
div wrapper, the inheritance here seems a bit redundant, since it's doing
this wrapping for the overridden function to clone it, add a class, return
that and throw the original away.


You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
https://github.com/matrix-org/matrix-react-sdk/pull/419/files/e1739008089fdc97d4c21fb7039382764f2e92b2#r75127209,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAOlRCseayDyFTzE96zslfUvGhmmQqsmks5qgxMagaJpZM4JmX4X
.

}
75 changes: 59 additions & 16 deletions src/autocomplete/Components.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,62 @@
import React from 'react';
import ReactDOM from 'react-dom';
import classNames from 'classnames';

export function TextualCompletion({
title,
subtitle,
description,
}: {
title: ?string,
subtitle: ?string,
description: ?string
}) {
return (
<div style={{width: '100%'}}>
<span>{title}</span>
<em>{subtitle}</em>
<span style={{color: 'gray', float: 'right'}}>{description}</span>
</div>
);
/* These were earlier stateless functional components but had to be converted
since we need to use refs/findDOMNode to access the underlying DOM node to focus the correct completion,
something that is not entirely possible with stateless functional components. One could
presumably wrap them in a <div> before rendering but I think this is the better way to do it.
*/

export class TextualCompletion extends React.Component {
render() {
const {
title,
subtitle,
description,
className,
...restProps,
} = this.props;
return (
<div className={classNames('mx_Autocomplete_Completion_block', className)} {...restProps}>
<span className="mx_Autocomplete_Completion_title">{title}</span>
<span className="mx_Autocomplete_Completion_subtitle">{subtitle}</span>
<span className="mx_Autocomplete_Completion_description">{description}</span>
</div>
);
}
}
TextualCompletion.propTypes = {
title: React.PropTypes.string,
subtitle: React.PropTypes.string,
description: React.PropTypes.string,
className: React.PropTypes.string,
};

export class PillCompletion extends React.Component {
render() {
const {
title,
subtitle,
description,
initialComponent,
className,
...restProps,
} = this.props;
return (
<div className={classNames('mx_Autocomplete_Completion_pill', className)} {...restProps}>
{initialComponent}
<span className="mx_Autocomplete_Completion_title">{title}</span>
<span className="mx_Autocomplete_Completion_subtitle">{subtitle}</span>
<span className="mx_Autocomplete_Completion_description">{description}</span>
</div>
);
}
}
PillCompletion.propTypes = {
title: React.PropTypes.string,
subtitle: React.PropTypes.string,
description: React.PropTypes.string,
initialComponent: React.PropTypes.element,
className: React.PropTypes.string,
};
8 changes: 7 additions & 1 deletion src/autocomplete/DuckDuckGoProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
}

getName() {
return 'Results from DuckDuckGo';
return '🔍 Results from DuckDuckGo';
}

static getInstance(): DuckDuckGoProvider {
Expand All @@ -87,4 +87,10 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
}
return instance;
}

renderCompletions(completions: [React.Component]): ?React.Component {
return <div className="mx_Autocomplete_Completion_container_block">
{completions}
</div>;
}
}
24 changes: 16 additions & 8 deletions src/autocomplete/EmojiProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import AutocompleteProvider from './AutocompleteProvider';
import Q from 'q';
import {emojioneList, shortnameToImage, shortnameToUnicode} from 'emojione';
import Fuse from 'fuse.js';
import sdk from '../index';
import {PillCompletion} from './Components';

const EMOJI_REGEX = /:\w*:?/g;
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
Expand All @@ -16,33 +18,39 @@ export default class EmojiProvider extends AutocompleteProvider {
}

getCompletions(query: string, selection: {start: number, end: number}) {
const EmojiText = sdk.getComponent('views.elements.EmojiText');

let completions = [];
let {command, range} = this.getCurrentCommand(query, selection);
if (command) {
completions = this.fuse.search(command[0]).map(result => {
let shortname = EMOJI_SHORTNAMES[result];
let imageHTML = shortnameToImage(shortname);
const shortname = EMOJI_SHORTNAMES[result];
const unicode = shortnameToUnicode(shortname);
return {
completion: shortnameToUnicode(shortname),
completion: unicode,
component: (
<div className="mx_Autocomplete_Completion">
<span style={{maxWidth: '1em'}} dangerouslySetInnerHTML={{__html: imageHTML}}></span>&nbsp;&nbsp;{shortname}
</div>
<PillCompletion title={shortname} initialComponent={<EmojiText style={{maxWidth: '1em'}}>{unicode}</EmojiText>} />
),
range,
};
}).slice(0, 4);
}).slice(0, 8);
}
return Q.when(completions);
}

getName() {
return 'Emoji';
return '😃 Emoji';
}

static getInstance() {
if (instance == null)
instance = new EmojiProvider();
return instance;
}

renderCompletions(completions: [React.Component]): ?React.Component {
return <div className="mx_Autocomplete_Completion_container_pill">
{completions}
</div>;
}
}
15 changes: 12 additions & 3 deletions src/autocomplete/RoomProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import AutocompleteProvider from './AutocompleteProvider';
import Q from 'q';
import MatrixClientPeg from '../MatrixClientPeg';
import Fuse from 'fuse.js';
import {TextualCompletion} from './Components';
import {PillCompletion} from './Components';
import {getDisplayAliasForRoom} from '../MatrixTools';
import sdk from '../index';

const ROOM_REGEX = /(?=#)([^\s]*)/g;

Expand All @@ -21,6 +22,8 @@ export default class RoomProvider extends AutocompleteProvider {
}

getCompletions(query: string, selection: {start: number, end: number}) {
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');

let client = MatrixClientPeg.get();
let completions = [];
const {command, range} = this.getCurrentCommand(query, selection);
Expand All @@ -39,7 +42,7 @@ export default class RoomProvider extends AutocompleteProvider {
return {
completion: displayAlias,
component: (
<TextualCompletion title={room.name} description={displayAlias} />
<PillCompletion initialComponent={<RoomAvatar width={24} height={24} room={room.room} />} title={room.name} description={displayAlias} />
),
range,
};
Expand All @@ -49,7 +52,7 @@ export default class RoomProvider extends AutocompleteProvider {
}

getName() {
return 'Rooms';
return '💬 Rooms';
}

static getInstance() {
Expand All @@ -59,4 +62,10 @@ export default class RoomProvider extends AutocompleteProvider {

return instance;
}

renderCompletions(completions: [React.Component]): ?React.Component {
return <div className="mx_Autocomplete_Completion_container_pill">
{completions}
</div>;
}
}
16 changes: 13 additions & 3 deletions src/autocomplete/UserProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import React from 'react';
import AutocompleteProvider from './AutocompleteProvider';
import Q from 'q';
import Fuse from 'fuse.js';
import {TextualCompletion} from './Components';
import {PillCompletion} from './Components';
import sdk from '../index';

const USER_REGEX = /@[^\s]*/g;

Expand All @@ -20,6 +21,8 @@ export default class UserProvider extends AutocompleteProvider {
}

getCompletions(query: string, selection: {start: number, end: number}) {
const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar');

let completions = [];
let {command, range} = this.getCurrentCommand(query, selection);
if (command) {
Expand All @@ -29,7 +32,8 @@ export default class UserProvider extends AutocompleteProvider {
return {
completion: user.userId,
component: (
<TextualCompletion
<PillCompletion
initialComponent={<MemberAvatar member={user} width={24} height={24}/>}
title={displayName}
description={user.userId} />
),
Expand All @@ -41,7 +45,7 @@ export default class UserProvider extends AutocompleteProvider {
}

getName() {
return 'Users';
return '👥 Users';
}

setUserList(users) {
Expand All @@ -54,4 +58,10 @@ export default class UserProvider extends AutocompleteProvider {
}
return instance;
}

renderCompletions(completions: [React.Component]): ?React.Component {
return <div className="mx_Autocomplete_Completion_container_pill">
{completions}
</div>;
}
}
56 changes: 30 additions & 26 deletions src/components/views/rooms/Autocomplete.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from 'react';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import ReactDOM from 'react-dom';
import classNames from 'classnames';
import flatMap from 'lodash/flatMap';
import sdk from '../../../index';

import {getCompletions} from '../../../autocomplete/Autocompleter';

Expand Down Expand Up @@ -100,11 +101,27 @@ export default class Autocomplete extends React.Component {
this.setState({selectionOffset});
}

componentDidUpdate() {
// this is the selected completion, so scroll it into view if needed
const selectedCompletion = this.refs[`completion${this.state.selectionOffset}`];
if (selectedCompletion && this.container) {
const domNode = ReactDOM.findDOMNode(selectedCompletion);
const offsetTop = domNode && domNode.offsetTop;
if (offsetTop > this.container.scrollTop + this.container.offsetHeight ||
offsetTop < this.container.scrollTop) {
this.container.scrollTop = offsetTop - this.container.offsetTop;
}
}
}

render() {
const EmojiText = sdk.getComponent('views.elements.EmojiText');

let position = 0;
let renderedCompletions = this.state.completions.map((completionResult, i) => {
let completions = completionResult.completions.map((completion, i) => {
let className = classNames('mx_Autocomplete_Completion', {

const className = classNames('mx_Autocomplete_Completion', {
'selected': position === this.state.selectionOffset,
});
let componentPosition = position;
Expand All @@ -116,40 +133,27 @@ export default class Autocomplete extends React.Component {
this.onConfirm();
};

return (
<div key={i}
className={className}
onMouseOver={onMouseOver}
onClick={onClick}>
{completion.component}
</div>
);
return React.cloneElement(completion.component, {
key: i,
ref: `completion${i}`,
className,
onMouseOver,
onClick,
});
});


return completions.length > 0 ? (
<div key={i} className="mx_Autocomplete_ProviderSection">
<span className="mx_Autocomplete_provider_name">{completionResult.provider.getName()}</span>
<ReactCSSTransitionGroup
component="div"
transitionName="autocomplete"
transitionEnterTimeout={300}
transitionLeaveTimeout={300}>
{completions}
</ReactCSSTransitionGroup>
<EmojiText element="div" className="mx_Autocomplete_provider_name">{completionResult.provider.getName()}</EmojiText>
{completionResult.provider.renderCompletions(completions)}
</div>
) : null;
});

return (
<div className="mx_Autocomplete">
<ReactCSSTransitionGroup
component="div"
transitionName="autocomplete"
transitionEnterTimeout={300}
transitionLeaveTimeout={300}>
{renderedCompletions}
</ReactCSSTransitionGroup>
<div className="mx_Autocomplete" ref={(e) => this.container = e}>
{renderedCompletions}
</div>
);
}
Expand Down