gamja/components/composer.js
Simon Ser 724d7318cf Focus buffer scrollview instead of composer
Instead of focusing the composer, focus the buffer scrollview when
switching to a buffer. This allows keyboard navigation to work as
expected, with arrow up/down and page up/down scrolling the buffer
instead of doing nothing.

Focus back the composer when a KeyboardEvent produces text. This
allows users to start typing a message right after switching to a
buffer.

Closes: https://todo.sr.ht/~emersion/gamja/64
2021-06-22 14:47:39 +02:00

134 lines
3 KiB
JavaScript

import { html, Component, createRef } from "../lib/index.js";
export default class Composer extends Component {
state = {
text: "",
};
textInput = createRef();
constructor(props) {
super(props);
this.handleInput = this.handleInput.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleInputKeyDown = this.handleInputKeyDown.bind(this);
this.handleWindowKeyDown = this.handleWindowKeyDown.bind(this);
}
handleInput(event) {
this.setState({ [event.target.name]: event.target.value });
if (this.props.readOnly && event.target.name === "text" && !event.target.value) {
event.target.blur();
}
}
handleSubmit(event) {
event.preventDefault();
this.props.onSubmit(this.state.text);
this.setState({ text: "" });
}
handleInputKeyDown(event) {
if (!this.props.autocomplete || event.key !== "Tab") {
return;
}
let text = this.state.text;
let i;
for (i = text.length - 1; i >= 0; i--) {
if (text[i] === " ") {
break;
}
}
let prefix = text.slice(i + 1);
if (!prefix) {
return;
}
event.preventDefault();
let repl = this.props.autocomplete(prefix);
if (!repl) {
return;
}
text = text.slice(0, i + 1) + repl;
this.setState({ text });
}
handleWindowKeyDown(event) {
// If an <input> or <button> is focused, ignore.
if (document.activeElement !== document.body && document.activeElement.tagName !== "SECTION") {
return;
}
// Ignore events that don't produce a Unicode string. If the key event
// result in a character being typed by the user, KeyboardEvent.key
// will contain the typed string. The key string may contain one
// Unicode non-control character and multiple Unicode combining
// characters. String.prototype.length cannot be used since it would
// return the number of Unicode code-points. Instead, the spread
// operator is used to count the number of non-combining Unicode
// characters.
if ([...event.key].length !== 1) {
return;
}
if (this.state.text) {
return;
}
if (this.props.readOnly && event.key !== "/") {
return;
}
event.preventDefault();
this.setState({ text: event.key }, () => {
this.focus();
});
}
componentDidMount() {
window.addEventListener("keydown", this.handleWindowKeyDown);
}
componentWillUnmount() {
window.removeEventListener("keydown", this.handleWindowKeyDown);
}
focus() {
if (!this.textInput.current) {
return;
}
document.activeElement.blur(); // in case we're read-only
this.textInput.current.focus();
}
render() {
let className = "";
if (this.props.readOnly && !this.state.text) {
className = "read-only";
}
return html`
<form
id="composer"
class=${className}
onInput=${this.handleInput}
onSubmit=${this.handleSubmit}
>
<input
type="text"
name="text"
ref=${this.textInput}
value=${this.state.text}
autocomplete="off"
placeholder="Type a message"
enterkeyhint="send"
onKeyDown=${this.handleInputKeyDown}
/>
</form>
`;
}
}