Merge pull request #538 from ueokande/scroll-console

Scroll console
jh-changes
Shin'ya Ueoka 6 years ago committed by GitHub
commit 124e221999
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      src/console/components/console.jsx
  2. 55
      src/console/components/console/completion.jsx
  3. 70
      src/console/reducers/index.js
  4. 138
      test/console/components/console/completion.test.jsx
  5. 38
      test/console/reducers/console.test.js

@ -6,6 +6,8 @@ import Completion from './console/completion';
import Message from './console/message'; import Message from './console/message';
import * as consoleActions from '../../console/actions/console'; import * as consoleActions from '../../console/actions/console';
const COMPLETION_MAX_ITEMS = 33;
class ConsoleComponent extends Component { class ConsoleComponent extends Component {
onBlur() { onBlur() {
if (this.props.mode === 'command' || this.props.mode === 'find') { if (this.props.mode === 'command' || this.props.mode === 'find') {
@ -105,7 +107,11 @@ class ConsoleComponent extends Component {
case 'command': case 'command':
case 'find': case 'find':
return <div className='vimvixen-console-command-wrapper'> return <div className='vimvixen-console-command-wrapper'>
<Completion /> <Completion
size={COMPLETION_MAX_ITEMS}
completions={this.props.completions}
select={this.props.select}
/>
<Input <Input
ref={(c) => { this.input = c; }} ref={(c) => { this.input = c; }}
mode={this.props.mode} mode={this.props.mode}

@ -1,5 +1,4 @@
import { Component, h } from 'preact'; import { Component, h } from 'preact';
import { connect } from 'preact-redux';
const CompletionTitle = (props) => { const CompletionTitle = (props) => {
return <li className='vimvixen-console-completion-title' >{props.title}</li>; return <li className='vimvixen-console-completion-title' >{props.title}</li>;
@ -25,25 +24,60 @@ const CompletionItem = (props) => {
class CompletionComponent extends Component { class CompletionComponent extends Component {
constructor() {
super();
this.state = { viewOffset: 0, select: -1 };
}
static getDerivedStateFromProps(nextProps, prevState) {
if (prevState.select === nextProps.select) {
return null;
}
let viewSelect = (() => {
let index = 0;
for (let i = 0; i < nextProps.completions.length; ++i) {
++index;
let g = nextProps.completions[i];
if (nextProps.select + i + 1 < index + g.items.length) {
return nextProps.select + i + 1;
}
index += g.items.length;
}
})();
let viewOffset = 0;
if (nextProps.select < 0) {
viewOffset = 0;
} else if (prevState.select < nextProps.select) {
viewOffset = Math.max(prevState.viewOffset,
viewSelect - nextProps.size + 1);
} else if (prevState.select > nextProps.select) {
viewOffset = Math.min(prevState.viewOffset, viewSelect);
}
return { viewOffset, select: nextProps.select };
}
render() { render() {
let eles = []; let eles = [];
for (let i = 0; i < this.props.completions.length; ++i) { let index = 0;
let group = this.props.completions[i];
for (let group of this.props.completions) {
eles.push(<CompletionTitle title={ group.name }/>); eles.push(<CompletionTitle title={ group.name }/>);
for (let j = 0; j < group.items.length; ++j) { for (let item of group.items) {
let item = group.items[j];
let selected =
i === this.props.groupSelection &&
j === this.props.itemSelection;
eles.push(<CompletionItem eles.push(<CompletionItem
icon={item.icon} icon={item.icon}
caption={item.caption} caption={item.caption}
url={item.url} url={item.url}
highlight={selected} highlight={index === this.props.select}
/ >); / >);
++index;
} }
} }
let viewOffset = this.state.viewOffset;
eles = eles.slice(viewOffset, viewOffset + this.props.size);
return ( return (
<ul className='vimvixen-console-completion'> <ul className='vimvixen-console-completion'>
{ eles } { eles }
@ -52,5 +86,4 @@ class CompletionComponent extends Component {
} }
} }
const mapStateToProps = state => state; export default CompletionComponent;
export default connect(mapStateToProps)(CompletionComponent);

@ -6,52 +6,43 @@ const defaultState = {
consoleText: '', consoleText: '',
completionSource: '', completionSource: '',
completions: [], completions: [],
groupSelection: -1, select: -1,
itemSelection: -1, viewIndex: 0,
}; };
const nextSelection = (state) => { const nextSelection = (state) => {
if (state.completions.length === 0) { if (state.completions.length === 0) {
return [-1, -1]; return -1;
} }
if (state.groupSelection < 0) { if (state.select < 0) {
return [0, 0]; return 0;
} }
let group = state.completions[state.groupSelection]; let length = state.completions
if (state.groupSelection + 1 >= state.completions.length && .map(g => g.items.length)
state.itemSelection + 1 >= group.items.length) { .reduce((x, y) => x + y);
return [-1, -1]; if (state.select + 1 < length) {
return state.select + 1;
} }
if (state.itemSelection + 1 >= group.items.length) { return -1;
return [state.groupSelection + 1, 0];
}
return [state.groupSelection, state.itemSelection + 1];
}; };
const prevSelection = (state) => { const prevSelection = (state) => {
if (state.groupSelection < 0) { let length = state.completions
return [ .map(g => g.items.length)
state.completions.length - 1, .reduce((x, y) => x + y);
state.completions[state.completions.length - 1].items.length - 1 if (state.select < 0) {
]; return length - 1;
}
if (state.groupSelection === 0 && state.itemSelection === 0) {
return [-1, -1];
} else if (state.itemSelection === 0) {
return [
state.groupSelection - 1,
state.completions[state.groupSelection - 1].items.length - 1
];
} }
return [state.groupSelection, state.itemSelection - 1]; return state.select - 1;
}; };
const nextConsoleText = (completions, group, item, defaults) => { const nextConsoleText = (completions, select, defaults) => {
if (group < 0 || item < 0) { if (select < 0) {
return defaults; return defaults;
} }
return completions[group].items[item].content; let items = completions.map(g => g.items).reduce((g1, g2) => g1.concat(g2));
return items[select].content;
}; };
// eslint-disable-next-line max-lines-per-function // eslint-disable-next-line max-lines-per-function
@ -90,25 +81,20 @@ export default function reducer(state = defaultState, action = {}) {
return { ...state, return { ...state,
completions: action.completions, completions: action.completions,
completionSource: action.completionSource, completionSource: action.completionSource,
groupSelection: -1, select: -1 };
itemSelection: -1, };
case actions.CONSOLE_COMPLETION_NEXT: { case actions.CONSOLE_COMPLETION_NEXT: {
let next = nextSelection(state); let select = nextSelection(state);
return { ...state, return { ...state,
groupSelection: next[0], select: select,
itemSelection: next[1],
consoleText: nextConsoleText( consoleText: nextConsoleText(
state.completions, next[0], next[1], state.completions, select, state.completionSource) };
state.completionSource), };
} }
case actions.CONSOLE_COMPLETION_PREV: { case actions.CONSOLE_COMPLETION_PREV: {
let next = prevSelection(state); let select = prevSelection(state);
return { ...state, return { ...state,
groupSelection: next[0], select: select,
itemSelection: next[1],
consoleText: nextConsoleText( consoleText: nextConsoleText(
state.completions, next[0], next[1], state.completions, select, state.completionSource) };
state.completionSource), };
} }
default: default:
return state; return state;

@ -0,0 +1,138 @@
import { h, render } from 'preact';
import Completion from 'console/components/console/completion'
describe("console/components/console/completion", () => {
let completions = [{
name: "Fruit",
items: [{ caption: "apple" }, { caption: "banana" }, { caption: "cherry" }],
}, {
name: "Element",
items: [{ caption: "argon" }, { caption: "boron" }, { caption: "carbon" }],
}];
beforeEach(() => {
document.body.innerHTML = '';
});
it('renders Completion component', () => {
let ul = render(<Completion
completions={completions}
size={30}
/>, document.body);
expect(ul.children).to.have.lengthOf(8);
expect(ul.children[0].textContent).to.equal('Fruit');
expect(ul.children[1].textContent).to.equal('apple');
expect(ul.children[2].textContent).to.equal('banana');
expect(ul.children[3].textContent).to.equal('cherry');
expect(ul.children[4].textContent).to.equal('Element');
expect(ul.children[5].textContent).to.equal('argon');
expect(ul.children[6].textContent).to.equal('boron');
expect(ul.children[7].textContent).to.equal('carbon');
});
it('highlight current item', () => {
let ul = render(<Completion
completions={completions}
size={30}
select={3}
/>, document.body);
expect(ul.children[5].className.split(' ')).to.include('vimvixen-completion-selected');
});
it('does not highlight any items', () => {
let ul = render(<Completion
completions={completions}
size={30}
select={-1}
/>, document.body);
for (let li of ul.children) {
expect(li.className.split(' ')).not.to.include('vimvixen-completion-selected');
}
});
it('limits completion items', () => {
let ul = render(<Completion
completions={completions}
size={3}
select={-1}
/>, document.body);
expect(Array.from(ul.children).map(e => e.textContent)).to.deep.equal(['Fruit', 'apple', 'banana']);
ul = render(<Completion
completions={completions}
size={3} select={0}
/>, document.body, ul);
expect(Array.from(ul.children).map(e => e.textContent)).to.deep.equal(['Fruit', 'apple', 'banana']);
expect(ul.children[1].className.split(' ')).to.include('vimvixen-completion-selected');
})
it('scrolls up to down with select', () => {
let ul = render(<Completion
completions={completions}
size={3}
select={1}
/>, document.body);
expect(Array.from(ul.children).map(e => e.textContent)).to.deep.equal(['Fruit', 'apple', 'banana']);
expect(ul.children[2].className.split(' ')).to.include('vimvixen-completion-selected');
ul = render(<Completion
completions={completions}
size={3}
select={2}
/>, document.body, ul);
expect(Array.from(ul.children).map(e => e.textContent)).to.deep.equal(['apple', 'banana', 'cherry']);
expect(ul.children[2].className.split(' ')).to.include('vimvixen-completion-selected');
ul = render(<Completion
completions={completions}
size={3}
select={3}
/>, document.body, ul);
expect(Array.from(ul.children).map(e => e.textContent)).to.deep.equal(['cherry', 'Element', 'argon']);
expect(ul.children[2].className.split(' ')).to.include('vimvixen-completion-selected');
});
it('scrolls up to down with select', () => {
let ul = render(<Completion
completions={completions}
size={3}
select={5}
/>, document.body);
expect(Array.from(ul.children).map(e => e.textContent)).to.deep.equal(['argon', 'boron', 'carbon']);
expect(ul.children[2].className.split(' ')).to.include('vimvixen-completion-selected');
ul = render(<Completion
completions={completions}
size={3}
select={4}
/>, document.body, ul);
expect(Array.from(ul.children).map(e => e.textContent)).to.deep.equal(['argon', 'boron', 'carbon']);
expect(ul.children[1].className.split(' ')).to.include('vimvixen-completion-selected');
ul = render(<Completion
completions={completions}
size={3}
select={3}
/>, document.body, ul);
expect(Array.from(ul.children).map(e => e.textContent)).to.deep.equal(['argon', 'boron', 'carbon']);
expect(ul.children[0].className.split(' ')).to.include('vimvixen-completion-selected');
ul = render(<Completion
completions={completions}
size={3}
select={2}
/>, document.body, ul);
expect(Array.from(ul.children).map(e => e.textContent)).to.deep.equal(['cherry', 'Element', 'argon']);
expect(ul.children[0].className.split(' ')).to.include('vimvixen-completion-selected');
});
});

@ -8,8 +8,7 @@ describe("console reducer", () => {
expect(state).to.have.property('messageText', ''); expect(state).to.have.property('messageText', '');
expect(state).to.have.property('consoleText', ''); expect(state).to.have.property('consoleText', '');
expect(state).to.have.deep.property('completions', []); expect(state).to.have.deep.property('completions', []);
expect(state).to.have.property('groupSelection', -1); expect(state).to.have.property('select', -1);
expect(state).to.have.property('itemSelection', -1);
}); });
it('return next state for CONSOLE_HIDE', () => { it('return next state for CONSOLE_HIDE', () => {
@ -60,8 +59,7 @@ describe("console reducer", () => {
it ('return next state for CONSOLE_SET_COMPLETIONS', () => { it ('return next state for CONSOLE_SET_COMPLETIONS', () => {
let state = { let state = {
groupSelection: 0, select: 0,
itemSelection: 0,
completions: [], completions: [],
} }
let action = { let action = {
@ -76,15 +74,13 @@ describe("console reducer", () => {
} }
state = reducer(state, action); state = reducer(state, action);
expect(state).to.have.property('completions', action.completions); expect(state).to.have.property('completions', action.completions);
expect(state).to.have.property('groupSelection', -1); expect(state).to.have.property('select', -1);
expect(state).to.have.property('itemSelection', -1);
}); });
it ('return next state for CONSOLE_COMPLETION_NEXT', () => { it ('return next state for CONSOLE_COMPLETION_NEXT', () => {
let action = { type: actions.CONSOLE_COMPLETION_NEXT }; let action = { type: actions.CONSOLE_COMPLETION_NEXT };
let state = { let state = {
groupSelection: -1, select: -1,
itemSelection: -1,
completions: [{ completions: [{
name: 'Apple', name: 'Apple',
items: [1, 2] items: [1, 2]
@ -95,24 +91,22 @@ describe("console reducer", () => {
}; };
state = reducer(state, action); state = reducer(state, action);
expect(state).to.have.property('groupSelection', 0); expect(state).to.have.property('select', 0);
expect(state).to.have.property('itemSelection', 0);
state = reducer(state, action); state = reducer(state, action);
expect(state).to.have.property('groupSelection', 0); expect(state).to.have.property('select', 1);
expect(state).to.have.property('itemSelection', 1);
state = reducer(state, action); state = reducer(state, action);
expect(state).to.have.property('select', 2);
state = reducer(state, action); state = reducer(state, action);
expect(state).to.have.property('groupSelection', -1); expect(state).to.have.property('select', -1);
expect(state).to.have.property('itemSelection', -1);
}); });
it ('return next state for CONSOLE_COMPLETION_PREV', () => { it ('return next state for CONSOLE_COMPLETION_PREV', () => {
let action = { type: actions.CONSOLE_COMPLETION_PREV }; let action = { type: actions.CONSOLE_COMPLETION_PREV };
let state = { let state = {
groupSelection: -1, select: -1,
itemSelection: -1,
completions: [{ completions: [{
name: 'Apple', name: 'Apple',
items: [1, 2] items: [1, 2]
@ -123,17 +117,15 @@ describe("console reducer", () => {
}; };
state = reducer(state, action); state = reducer(state, action);
expect(state).to.have.property('groupSelection', 1); expect(state).to.have.property('select', 2);
expect(state).to.have.property('itemSelection', 0);
state = reducer(state, action); state = reducer(state, action);
expect(state).to.have.property('groupSelection', 0); expect(state).to.have.property('select', 1);
expect(state).to.have.property('itemSelection', 1);
state = reducer(state, action); state = reducer(state, action);
expect(state).to.have.property('select', 0);
state = reducer(state, action); state = reducer(state, action);
expect(state).to.have.property('groupSelection', -1); expect(state).to.have.property('select', -1);
expect(state).to.have.property('itemSelection', -1);
}); });
}); });