passbolt-styleguide
Version:
Passbolt styleguide contains common styling assets used by the different sites, plugin, etc.
255 lines (236 loc) • 7.13 kB
JavaScript
/**
* Passbolt ~ Open source password manager for teams
* Copyright (c) 2020 Passbolt SA (https://www.passbolt.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) 2020 Passbolt SA (https://www.passbolt.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.passbolt.com Passbolt(tm)
* @since 2.13.0
*/
import React, { Component } from "react";
import PropTypes from "prop-types";
import AutocompleteItem from "./AutocompleteItem";
import SpinnerSVG from "../../../../img/svg/spinner.svg";
class Autocomplete extends Component {
/**
* Constructor
* @param {Object} props
*/
constructor(props) {
super(props);
this.bindEventHandlers();
this.createInputRefs();
this.state = this.getDefaultState();
}
/**
* getDefaultState
* @return {object}
*/
getDefaultState() {
return {
// autocomplete
selected: -1,
};
}
/**
* ComponentDidMount
* Invoked immediately after component is inserted into the tree
* @return {void}
*/
componentDidMount() {
document.addEventListener("keydown", this.handleKeyDown, { capture: true });
}
componentWillUnmount() {
document.removeEventListener("keydown", this.handleKeyDown, { capture: true });
}
createInputRefs() {
this.listRef = React.createRef();
}
bindEventHandlers() {
this.handleKeyDown = this.handleKeyDown.bind(this);
this.selectNext = this.selectNext.bind(this);
this.selectPrevious = this.selectPrevious.bind(this);
this.handleSelect = this.handleSelect.bind(this);
}
/**
* Handle key down to navigate and select the item
* @param event
*/
handleKeyDown(event) {
if (!this.props.autocompleteItems) {
return;
}
if (event.keyCode === 40) {
// key down
event.preventDefault();
this.selectNext();
return;
}
if (event.keyCode === 38) {
// key up
event.preventDefault();
this.selectPrevious();
return;
}
if (event.keyCode === 8) {
// backspace key
this.setState({ selected: -1 });
return;
}
if (event.keyCode === 13 || event.keyCode === 9) {
// enter key or tab
if (this.state.selected === null || this.state.selected === -1) {
return;
}
event.preventDefault();
const obj = this.props.autocompleteItems[this.state.selected];
this.props.onSelect(obj);
}
}
/**
* Handle when an item is selected
* @param event
* @param selected
*/
handleSelect(event, selected) {
const obj = this.props.autocompleteItems[selected];
this.props.onSelect(obj);
/*
* When click on the autocomplete word
* the click is detected out of the element and the editor close.
* To fix that an immediate stop propagation enable to avoid the editor close.
* Need absolutely an immediate propagation to stop other listeners.
*/
event.nativeEvent.stopImmediatePropagation();
}
/**
* Handle when item is selected by arrows
* @param {number} selected
*/
handleArrowFocus(selected) {
if (selected === -1) {
this.props.onArrowFocus(this.props.value);
} else {
const slug = this.props.autocompleteItems[selected].slug;
this.props.onArrowFocus(slug);
}
}
/**
* Navigate to select the previous item
*/
selectPrevious() {
let selected = this.state.selected;
if (selected === -1) {
selected = this.props.autocompleteItems.length - 1;
} else {
selected = selected - 1;
}
this.scrollToSelectedItem(selected);
this.handleArrowFocus(selected);
this.setState({ selected });
}
/**
* Navigate to select the next item
*/
selectNext() {
let selected = this.state.selected;
if (selected === this.props.autocompleteItems.length - 1) {
selected = -1;
} else {
selected = selected + 1;
}
this.scrollToSelectedItem(selected);
this.handleArrowFocus(selected);
this.setState({ selected });
}
/**
* check if an item is selected
* @param key
* @returns {boolean}
*/
isItemSelected(key) {
if (this.state.selected === null) {
return false;
} else {
return key === this.state.selected;
}
}
/**
* Get the autocomplete style.
*/
getStyle() {
// calculate the max width according to the position of the autocomplete to avoid horizontally scroll
const maxWidth = this.props.width - this.props.left;
return {
left: this.props.left,
top: this.props.top,
maxWidth,
};
}
/**
* Scroll to the selected item
* @param {number} selected
*/
scrollToSelectedItem(selected) {
if (!this.props.autocompleteItems || this.props.autocompleteItems.length === 0 || selected === -1) {
this.listRef.current.scrollTop = 0;
} else {
const totalHeight = this.listRef.current.scrollHeight;
const itemHeight = totalHeight / this.props.autocompleteItems.length;
const visibleHeight = this.listRef.current.clientHeight;
const howManyFits = Math.round(visibleHeight / itemHeight);
const fitOffset = visibleHeight - itemHeight * howManyFits;
const currentItemPosition = itemHeight * selected;
const currentScroll = this.listRef.current.scrollTop;
if (currentItemPosition - fitOffset < currentScroll) {
this.listRef.current.scrollTop = this.listRef.current.scrollTop - visibleHeight;
return;
}
if (currentItemPosition > currentScroll + visibleHeight) {
this.listRef.current.scrollTop = currentItemPosition;
return;
}
}
}
render() {
return (
<div className="autocomplete-suggestions" style={this.getStyle()}>
<div className="autocomplete-content scroll" ref={this.listRef}>
<ul>
{!this.state.processing &&
this.props.autocompleteItems &&
this.props.autocompleteItems.map((tag, key) => (
<AutocompleteItem
key={key}
id={key}
slug={tag.slug}
selected={this.isItemSelected(key)}
onClick={this.handleSelect}
/>
))}
{this.state.processing && (
<li className="autocomplete-suggestion row">
<SpinnerSVG />
</li>
)}
</ul>
</div>
</div>
);
}
}
Autocomplete.propTypes = {
id: PropTypes.string, // id of the item
autocompleteItems: PropTypes.array, // item of the autocomplete
top: PropTypes.number, // top position for autocomplete
left: PropTypes.number, // left position for autocomplete
width: PropTypes.number, // width in px of the parent element
onSelect: PropTypes.func,
onArrowFocus: PropTypes.func,
value: PropTypes.string,
};
export default Autocomplete;