From 9126efacd209934bb7a689d52975d54ae5fe7966 Mon Sep 17 00:00:00 2001 From: Aran-Fey Date: Tue, 2 Jul 2024 08:45:23 +0200 Subject: [PATCH] fix MultiLineTextInput --- frontend/code/components/dropdown.ts | 3 +- .../code/components/multiLineTextInput.ts | 73 ++------ frontend/code/components/textInput.ts | 7 +- frontend/code/inputBox.ts | 66 ++++--- frontend/css/style.scss | 170 ++++++++++-------- 5 files changed, 162 insertions(+), 157 deletions(-) diff --git a/frontend/code/components/dropdown.ts b/frontend/code/components/dropdown.ts index 759a2d01..762aca05 100644 --- a/frontend/code/components/dropdown.ts +++ b/frontend/code/components/dropdown.ts @@ -29,7 +29,8 @@ export class DropdownComponent extends ComponentBase { let element = document.createElement('div'); element.classList.add('rio-dropdown'); - this.inputBox = new InputBox(element); + this.inputBox = new InputBox({ labelIsAlwaysSmall: true }); + element.appendChild(this.inputBox.outerElement); // Add an arrow icon let arrowElement = document.createElement('div'); diff --git a/frontend/code/components/multiLineTextInput.ts b/frontend/code/components/multiLineTextInput.ts index 8e2d0cbf..0c4afe28 100644 --- a/frontend/code/components/multiLineTextInput.ts +++ b/frontend/code/components/multiLineTextInput.ts @@ -1,4 +1,5 @@ import { markEventAsHandled } from '../eventHandling'; +import { InputBox } from '../inputBox'; import { ComponentBase, ComponentState } from './componentBase'; export type MultiLineTextInputState = ComponentState & { @@ -12,33 +13,18 @@ export type MultiLineTextInputState = ComponentState & { export class MultiLineTextInputComponent extends ComponentBase { state: Required; - private labelElement: HTMLElement; - private inputElement: HTMLTextAreaElement; + private inputBox: InputBox; createElement(): HTMLElement { - // Create the element - let element = document.createElement('div'); - element.classList.add('rio-text-input', 'rio-input-box'); + let textarea = document.createElement('textarea'); + this.inputBox = new InputBox({ inputElement: textarea }); - element.innerHTML = ` - -
-
-
- `; + let element = this.inputBox.outerElement; + element.classList.add('rio-multi-line-text-input'); - this.labelElement = element.querySelector( - '.rio-input-box-label' - ) as HTMLElement; - - // Detect value changes and send them to the backend - this.inputElement = element.querySelector( - 'textarea' - ) as HTMLTextAreaElement; - - this.inputElement.addEventListener('blur', () => { + this.inputBox.inputElement.addEventListener('blur', () => { this.setStateAndNotifyBackend({ - text: this.inputElement.value, + text: this.inputBox.value, }); }); @@ -47,29 +33,15 @@ export class MultiLineTextInputComponent extends ComponentBase { // In addition to notifying the backend, also include the input's // current value. This ensures any event handlers actually use the up-to // date value. - this.inputElement.addEventListener('keydown', (event) => { + this.inputBox.inputElement.addEventListener('keydown', (event) => { if (event.key === 'Enter' && event.shiftKey) { - this.state.text = this.inputElement.value; + this.state.text = this.inputBox.value; this.sendMessageToBackend({ text: this.state.text, }); - event.preventDefault(); + markEventAsHandled(event); } - - markEventAsHandled(event); - }); - - // Eat the event so other components don't get it - this.inputElement.addEventListener('mousedown', (event) => { - markEventAsHandled(event); - }); - - // The input element doesn't take up the full height of the component. - // Catch clicks above and also make them focus the input element. - element.addEventListener('click', (event) => { - this.inputElement.focus(); - markEventAsHandled(event); }); return element; @@ -82,32 +54,23 @@ export class MultiLineTextInputComponent extends ComponentBase { super.updateElement(deltaState, latentComponents); if (deltaState.text !== undefined) { - this.inputElement.value = deltaState.text; + this.inputBox.value = deltaState.text; } if (deltaState.label !== undefined) { - this.labelElement.textContent = deltaState.label; + this.inputBox.label = deltaState.label; } - if (deltaState.is_sensitive === true) { - this.inputElement.disabled = false; - this.element.classList.remove('rio-disabled-input'); - } else if (deltaState.is_sensitive === false) { - this.inputElement.disabled = true; - this.element.classList.add('rio-disabled-input'); + if (deltaState.is_sensitive !== undefined) { + this.inputBox.isSensitive = deltaState.is_sensitive; } - if (deltaState.is_valid === false) { - this.element.style.setProperty( - '--rio-local-text-color', - 'var(--rio-global-danger-bg)' - ); - } else if (deltaState.is_valid === true) { - this.element.style.removeProperty('--rio-local-text-color'); + if (deltaState.is_valid !== undefined) { + this.inputBox.isValid = deltaState.is_valid; } } grabKeyboardFocus(): void { - this.inputElement.focus(); + this.inputBox.focus(); } } diff --git a/frontend/code/components/textInput.ts b/frontend/code/components/textInput.ts index 24850680..6fa4668c 100644 --- a/frontend/code/components/textInput.ts +++ b/frontend/code/components/textInput.ts @@ -21,11 +21,10 @@ export class TextInputComponent extends ComponentBase { private onChangeLimiter: Debouncer; createElement(): HTMLElement { - // Create the element - let element = document.createElement('div'); - element.classList.add('rio-text-input'); + this.inputBox = new InputBox(); - this.inputBox = new InputBox(element); + let element = this.inputBox.outerElement; + element.classList.add('rio-text-input'); // Create a rate-limited function for notifying the backend of changes. // This allows reporting changes to the backend in real-time, rather diff --git a/frontend/code/inputBox.ts b/frontend/code/inputBox.ts index 3ff2f5d7..0e605a3e 100644 --- a/frontend/code/inputBox.ts +++ b/frontend/code/inputBox.ts @@ -6,7 +6,7 @@ import { markEventAsHandled, stopPropagation } from './eventHandling'; /// - prefix text /// - suffix text export class InputBox { - private element: HTMLElement; + public outerElement: HTMLElement; private prefixTextElement: HTMLElement; private suffixElementContainer: HTMLElement; @@ -14,14 +14,23 @@ export class InputBox { private labelWidthReserverElement: HTMLElement; // Ensures enough width for the label private labelElement: HTMLElement; + + // NOTE: The input element can also be a textarea, but for some reason the + // typing gets really wonky if this is a union. I don't like it, but I think + // lying about the type is our best option. private _inputElement: HTMLInputElement; - constructor(parentElement: Element) { - this.element = document.createElement('div'); - this.element.classList.add('rio-input-box'); - parentElement.appendChild(this.element); + constructor({ + inputElement, + labelIsAlwaysSmall, + }: { + inputElement?: HTMLInputElement | HTMLTextAreaElement; + labelIsAlwaysSmall?: boolean; + } = {}) { + this.outerElement = document.createElement('div'); + this.outerElement.classList.add('rio-input-box'); - this.element.innerHTML = ` + this.outerElement.innerHTML = `
@@ -36,26 +45,39 @@ export class InputBox {
`; - this.prefixTextElement = this.element.querySelector( + this.prefixTextElement = this.outerElement.querySelector( '.rio-input-box-prefix-text' ) as HTMLElement; - this.suffixElementContainer = this.element.querySelector( + this.suffixElementContainer = this.outerElement.querySelector( '.rio-input-box-suffix-element > *' ) as HTMLElement; - this.suffixTextElement = this.element.querySelector( + this.suffixTextElement = this.outerElement.querySelector( '.rio-input-box-suffix-text' ) as HTMLElement; - this.labelWidthReserverElement = this.element.querySelector( + this.labelWidthReserverElement = this.outerElement.querySelector( '.rio-input-box-label-width-reserver' ) as HTMLElement; - this.labelElement = this.element.querySelector( + this.labelElement = this.outerElement.querySelector( '.rio-input-box-label' ) as HTMLElement; - this._inputElement = this.element.querySelector( + this._inputElement = this.outerElement.querySelector( 'input' ) as HTMLInputElement; + if (inputElement !== undefined) { + this._inputElement.parentElement!.insertBefore( + inputElement, + this._inputElement + ); + this._inputElement.remove(); + this._inputElement = inputElement as HTMLInputElement; + } + + if (labelIsAlwaysSmall) { + this.outerElement.classList.add('label-is-always-small'); + } + // Detect clicks on any part of the component and focus the input // // The `mousedown` are needed to prevent any potential drag events from @@ -100,9 +122,9 @@ export class InputBox { // floating label can position itself accordingly this._inputElement.addEventListener('blur', () => { if (this._inputElement.value) { - this.element.classList.add('has-value'); + this.outerElement.classList.add('has-value'); } else { - this.element.classList.remove('has-value'); + this.outerElement.classList.remove('has-value'); } }); @@ -124,9 +146,9 @@ export class InputBox { this._inputElement.value = value; if (value) { - this.element.classList.add('has-value'); + this.outerElement.classList.add('has-value'); } else { - this.element.classList.remove('has-value'); + this.outerElement.classList.remove('has-value'); } } @@ -139,9 +161,9 @@ export class InputBox { this.labelWidthReserverElement.textContent = label; if (label) { - this.element.classList.add('has-label'); + this.outerElement.classList.add('has-label'); } else { - this.element.classList.remove('has-label'); + this.outerElement.classList.remove('has-label'); } } @@ -182,17 +204,17 @@ export class InputBox { this._inputElement.disabled = !isSensitive; if (isSensitive) { - this._inputElement.classList.remove('rio-disabled-input'); + this.outerElement.classList.remove('rio-disabled-input'); } else { - this._inputElement.classList.add('rio-disabled-input'); + this.outerElement.classList.add('rio-disabled-input'); } } set isValid(isValid: boolean) { if (isValid) { - this.element.style.removeProperty('--rio-local-text-color'); + this.outerElement.style.removeProperty('--rio-local-text-color'); } else { - this.element.style.setProperty( + this.outerElement.style.setProperty( '--rio-local-text-color', 'var(--rio-global-danger-bg)' ); diff --git a/frontend/css/style.scss b/frontend/css/style.scss index 95042e51..de31977c 100644 --- a/frontend/css/style.scss +++ b/frontend/css/style.scss @@ -31,6 +31,12 @@ /// Kills the element's size request so it can't make its parent element grow. /// Always takes the size of the nearest *positioned* parent element. +/// +/// Note: I want to avoid using `position: absolute` here because that can have +/// unintended side effects (for example, scroll anchoring excludes elements +/// that are absolute). However, it turns out that this implementation doesn't +/// work with elements. If we discover more problematic elements, we +/// should reconsider using `position: absolute`. @mixin kill-size-request { // Prevent it from making the parent element grow width: 0; @@ -386,9 +392,7 @@ select { background-color: transparent; opacity: 0; - transition: - opacity 0.3s ease-in-out, - background-color 1s ease-in-out; + transition: opacity 0.3s ease-in-out, background-color 1s ease-in-out; & > * { transform: translateY(-5rem); @@ -527,8 +531,10 @@ select { // Input Box: This is a style common to multiple input components, such as // `TextInput` and `Dropdown`. $rio-input-box-height: 2rem; -$rio-input-box-height-with-label: calc($rio-input-box-height + 1rem); -$rio-input-box-text-distance-from-bottom: 0.4rem; // To be aligned with the