From aa74657f6999af1a9c166ce2f468d9fb74fdb2cb Mon Sep 17 00:00:00 2001 From: Jeremiah <42397676+jlsnow301@users.noreply.github.com> Date: Tue, 16 May 2023 19:19:04 -0700 Subject: [PATCH] Tgui say v1.1 (#75431) ## About The Pull Request "It's better! I promise!" When I wrote it, I was inexperienced and pretty angry. Not that I'm any better of a person now, but the code should be. I consolidated instead of relying on heavy abstractions. I simplified logic and wrote more tests. The result should look and feel much more like intended. The bundle size is reduced by ~43%. Types are much stricter. The logic and css classes are much more precise. No major style changes yet ![Screenshot 2023-05-15 003339](https://github.com/tgstation/tgstation/assets/42397676/edeabdcf-5cc6-44ba-9e98-9015bb863547) ## Why It's Good For The Game Less javascript is better, and being even a few fractions of a second faster might make better gameplay ## Changelog :cl: refactor: Tgui Say is rewritten, becoming "much more performant". Hey, that's what it says on the tin! I'm not from marketing! fix: Tguisay drag zones are now ever so slightly larger around the corner of the window fix: Pressing one of the chat open keys (T/Y/M/O) will no longer change channels if it's already open /:cl: --- code/modules/tgui_input/say_modal/modal.dm | 4 +- tgui/packages/common/keys.ts | 39 ++ tgui/packages/common/{timer.js => timer.ts} | 32 +- .../packages/tgui-say/ChannelIterator.test.ts | 47 +++ tgui/packages/tgui-say/ChannelIterator.ts | 50 +++ tgui/packages/tgui-say/ChatHistory.test.ts | 50 +++ tgui/packages/tgui-say/ChatHistory.ts | 59 +++ tgui/packages/tgui-say/TguiSay.tsx | 354 ++++++++++++++++++ .../packages/tgui-say/components/dragzone.tsx | 20 - tgui/packages/tgui-say/constants.ts | 32 ++ tgui/packages/tgui-say/constants/index.tsx | 73 ---- .../packages/tgui-say/fonts/VT323-Regular.ttf | Bin 0 -> 147320 bytes tgui/packages/tgui-say/handlers/arrowKeys.tsx | 22 -- .../tgui-say/handlers/backspaceDelete.tsx | 22 -- tgui/packages/tgui-say/handlers/click.tsx | 9 - .../tgui-say/handlers/componentMount.tsx | 23 -- .../tgui-say/handlers/componentUpdate.tsx | 6 - tgui/packages/tgui-say/handlers/enter.tsx | 23 -- tgui/packages/tgui-say/handlers/escape.tsx | 8 - tgui/packages/tgui-say/handlers/force.tsx | 19 - .../tgui-say/handlers/incrementChannel.tsx | 49 --- tgui/packages/tgui-say/handlers/index.tsx | 41 -- tgui/packages/tgui-say/handlers/input.tsx | 11 - tgui/packages/tgui-say/handlers/keyDown.tsx | 43 --- .../tgui-say/handlers/radioPrefix.tsx | 34 -- tgui/packages/tgui-say/handlers/reset.tsx | 22 -- tgui/packages/tgui-say/handlers/setSize.tsx | 22 -- .../tgui-say/handlers/viewHistory.tsx | 26 -- tgui/packages/tgui-say/helpers.ts | 56 +++ tgui/packages/tgui-say/helpers/index.tsx | 157 -------- tgui/packages/tgui-say/index.tsx | 2 +- tgui/packages/tgui-say/interfaces/TguiSay.tsx | 76 ---- tgui/packages/tgui-say/package.json | 2 - tgui/packages/tgui-say/styles/button.scss | 33 +- tgui/packages/tgui-say/styles/colors.scss | 58 +-- tgui/packages/tgui-say/styles/content.scss | 14 + tgui/packages/tgui-say/styles/dragzone.scss | 75 ++-- tgui/packages/tgui-say/styles/main.scss | 59 ++- tgui/packages/tgui-say/styles/modal.scss | 71 ---- tgui/packages/tgui-say/styles/textarea.scss | 26 +- tgui/packages/tgui-say/styles/window.scss | 29 ++ tgui/packages/tgui-say/timers.ts | 19 + tgui/packages/tgui-say/types/index.tsx | 67 ---- tgui/packages/tgui/interfaces/MafiaPanel.tsx | 2 +- tgui/yarn.lock | 2 - 45 files changed, 885 insertions(+), 1003 deletions(-) create mode 100644 tgui/packages/common/keys.ts rename tgui/packages/common/{timer.js => timer.ts} (59%) create mode 100644 tgui/packages/tgui-say/ChannelIterator.test.ts create mode 100644 tgui/packages/tgui-say/ChannelIterator.ts create mode 100644 tgui/packages/tgui-say/ChatHistory.test.ts create mode 100644 tgui/packages/tgui-say/ChatHistory.ts create mode 100644 tgui/packages/tgui-say/TguiSay.tsx delete mode 100644 tgui/packages/tgui-say/components/dragzone.tsx create mode 100644 tgui/packages/tgui-say/constants.ts delete mode 100644 tgui/packages/tgui-say/constants/index.tsx create mode 100644 tgui/packages/tgui-say/fonts/VT323-Regular.ttf delete mode 100644 tgui/packages/tgui-say/handlers/arrowKeys.tsx delete mode 100644 tgui/packages/tgui-say/handlers/backspaceDelete.tsx delete mode 100644 tgui/packages/tgui-say/handlers/click.tsx delete mode 100644 tgui/packages/tgui-say/handlers/componentMount.tsx delete mode 100644 tgui/packages/tgui-say/handlers/componentUpdate.tsx delete mode 100644 tgui/packages/tgui-say/handlers/enter.tsx delete mode 100644 tgui/packages/tgui-say/handlers/escape.tsx delete mode 100644 tgui/packages/tgui-say/handlers/force.tsx delete mode 100644 tgui/packages/tgui-say/handlers/incrementChannel.tsx delete mode 100644 tgui/packages/tgui-say/handlers/index.tsx delete mode 100644 tgui/packages/tgui-say/handlers/input.tsx delete mode 100644 tgui/packages/tgui-say/handlers/keyDown.tsx delete mode 100644 tgui/packages/tgui-say/handlers/radioPrefix.tsx delete mode 100644 tgui/packages/tgui-say/handlers/reset.tsx delete mode 100644 tgui/packages/tgui-say/handlers/setSize.tsx delete mode 100644 tgui/packages/tgui-say/handlers/viewHistory.tsx create mode 100644 tgui/packages/tgui-say/helpers.ts delete mode 100644 tgui/packages/tgui-say/helpers/index.tsx delete mode 100644 tgui/packages/tgui-say/interfaces/TguiSay.tsx create mode 100644 tgui/packages/tgui-say/styles/content.scss delete mode 100644 tgui/packages/tgui-say/styles/modal.scss create mode 100644 tgui/packages/tgui-say/styles/window.scss create mode 100644 tgui/packages/tgui-say/timers.ts delete mode 100644 tgui/packages/tgui-say/types/index.tsx diff --git a/code/modules/tgui_input/say_modal/modal.dm b/code/modules/tgui_input/say_modal/modal.dm index 4acb4069ed048..8afba448762aa 100644 --- a/code/modules/tgui_input/say_modal/modal.dm +++ b/code/modules/tgui_input/say_modal/modal.dm @@ -117,10 +117,10 @@ close() return TRUE if (type == "thinking") - if(payload["mode"] == TRUE) + if(payload["visible"] == TRUE) start_thinking() return TRUE - if(payload["mode"] == FALSE) + if(payload["visible"] == FALSE) stop_thinking() return TRUE return FALSE diff --git a/tgui/packages/common/keys.ts b/tgui/packages/common/keys.ts new file mode 100644 index 0000000000000..61b79992b486b --- /dev/null +++ b/tgui/packages/common/keys.ts @@ -0,0 +1,39 @@ +/** + * ### Key codes. + * event.keyCode is deprecated, use this reference instead. + * + * Handles modifier keys (Shift, Alt, Control) and arrow keys. + * + * For alphabetical keys, use the actual character (e.g. 'a') instead of the key code. + * + * Something isn't here that you want? Just add it: + * @url https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values + * @usage + * ```ts + * import { KEY } from 'tgui/common/keys'; + * + * if (event.key === KEY.Enter) { + * // do something + * } + * ``` + */ +export enum KEY { + Alt = 'Alt', + Backspace = 'Backspace', + Control = 'Control', + Delete = 'Delete', + Down = 'Down', + End = 'End', + Enter = 'Enter', + Escape = 'Esc', + Home = 'Home', + Insert = 'Insert', + Left = 'Left', + PageDown = 'PageDown', + PageUp = 'PageUp', + Right = 'Right', + Shift = 'Shift', + Space = ' ', + Tab = 'Tab', + Up = 'Up', +} diff --git a/tgui/packages/common/timer.js b/tgui/packages/common/timer.ts similarity index 59% rename from tgui/packages/common/timer.js rename to tgui/packages/common/timer.ts index 7d89e935b9b57..49d36484200b3 100644 --- a/tgui/packages/common/timer.js +++ b/tgui/packages/common/timer.ts @@ -10,9 +10,13 @@ * called for N milliseconds. If `immediate` is passed, trigger the * function on the leading edge, instead of the trailing. */ -export const debounce = (fn, time, immediate = false) => { - let timeout; - return (...args) => { +export const debounce = any>( + fn: F, + time: number, + immediate = false +): ((...args: Parameters) => void) => { + let timeout: ReturnType | null; + return (...args: Parameters) => { const later = () => { timeout = null; if (!immediate) { @@ -20,7 +24,7 @@ export const debounce = (fn, time, immediate = false) => { } }; const callNow = immediate && !timeout; - clearTimeout(timeout); + clearTimeout(timeout!); timeout = setTimeout(later, time); if (callNow) { fn(...args); @@ -32,18 +36,24 @@ export const debounce = (fn, time, immediate = false) => { * Returns a function, that, when invoked, will only be triggered at most once * during a given window of time. */ -export const throttle = (fn, time) => { - let previouslyRun, queuedToRun; - return function invokeFn(...args) { +export const throttle = any>( + fn: F, + time: number +): ((...args: Parameters) => void) => { + let previouslyRun: number | null, + queuedToRun: ReturnType | null; + return function invokeFn(...args: Parameters) { const now = Date.now(); - queuedToRun = clearTimeout(queuedToRun); + if (queuedToRun) { + clearTimeout(queuedToRun); + } if (!previouslyRun || now - previouslyRun >= time) { fn.apply(null, args); previouslyRun = now; } else { queuedToRun = setTimeout( - invokeFn.bind(null, ...args), - time - (now - previouslyRun) + () => invokeFn(...args), + time - (now - (previouslyRun ?? 0)) ); } }; @@ -54,5 +64,5 @@ export const throttle = (fn, time) => { * * @param {number} time */ -export const sleep = (time) => +export const sleep = (time: number): Promise => new Promise((resolve) => setTimeout(resolve, time)); diff --git a/tgui/packages/tgui-say/ChannelIterator.test.ts b/tgui/packages/tgui-say/ChannelIterator.test.ts new file mode 100644 index 0000000000000..15e9812e702ec --- /dev/null +++ b/tgui/packages/tgui-say/ChannelIterator.test.ts @@ -0,0 +1,47 @@ +import { ChannelIterator } from './ChannelIterator'; + +describe('ChannelIterator', () => { + let channelIterator: ChannelIterator; + + beforeEach(() => { + channelIterator = new ChannelIterator(); + }); + + it('should cycle through channels properly', () => { + expect(channelIterator.current()).toBe('Say'); + expect(channelIterator.next()).toBe('Radio'); + expect(channelIterator.next()).toBe('Me'); + expect(channelIterator.next()).toBe('OOC'); + expect(channelIterator.next()).toBe('Say'); // Admin is blacklisted so it should be skipped + }); + + it('should set a channel properly', () => { + channelIterator.set('OOC'); + expect(channelIterator.current()).toBe('OOC'); + }); + + it('should return true when current channel is "Say"', () => { + channelIterator.set('Say'); + expect(channelIterator.isSay()).toBe(true); + }); + + it('should return false when current channel is not "Say"', () => { + channelIterator.set('Radio'); + expect(channelIterator.isSay()).toBe(false); + }); + + it('should return true when current channel is visible', () => { + channelIterator.set('Say'); + expect(channelIterator.isVisible()).toBe(true); + }); + + it('should return false when current channel is not visible', () => { + channelIterator.set('OOC'); + expect(channelIterator.isVisible()).toBe(false); + }); + + it('should not leak a message from a blacklisted channel', () => { + channelIterator.set('Admin'); + expect(channelIterator.next()).toBe('Admin'); + }); +}); diff --git a/tgui/packages/tgui-say/ChannelIterator.ts b/tgui/packages/tgui-say/ChannelIterator.ts new file mode 100644 index 0000000000000..136806927e95e --- /dev/null +++ b/tgui/packages/tgui-say/ChannelIterator.ts @@ -0,0 +1,50 @@ +export type Channel = 'Say' | 'Radio' | 'Me' | 'OOC' | 'Admin'; + +/** + * ### ChannelIterator + * Cycles a predefined list of channels, + * skipping over blacklisted ones, + * and providing methods to manage and query the current channel. + */ +export class ChannelIterator { + private index: number = 0; + private readonly channels: Channel[] = ['Say', 'Radio', 'Me', 'OOC', 'Admin']; + private readonly blacklist: Channel[] = ['Admin']; + private readonly quiet: Channel[] = ['OOC', 'Admin']; + + public next(): Channel { + if (this.blacklist.includes(this.channels[this.index])) { + return this.channels[this.index]; + } + + for (let index = 1; index <= this.channels.length; index++) { + let nextIndex = (this.index + index) % this.channels.length; + if (!this.blacklist.includes(this.channels[nextIndex])) { + this.index = nextIndex; + break; + } + } + + return this.channels[this.index]; + } + + public set(channel: Channel): void { + this.index = this.channels.indexOf(channel) || 0; + } + + public current(): Channel { + return this.channels[this.index]; + } + + public isSay(): boolean { + return this.channels[this.index] === 'Say'; + } + + public isVisible(): boolean { + return !this.quiet.includes(this.channels[this.index]); + } + + public reset(): void { + this.index = 0; + } +} diff --git a/tgui/packages/tgui-say/ChatHistory.test.ts b/tgui/packages/tgui-say/ChatHistory.test.ts new file mode 100644 index 0000000000000..c6d8c1c2e27c0 --- /dev/null +++ b/tgui/packages/tgui-say/ChatHistory.test.ts @@ -0,0 +1,50 @@ +import { ChatHistory } from './ChatHistory'; + +describe('ChatHistory', () => { + let chatHistory: ChatHistory; + + beforeEach(() => { + chatHistory = new ChatHistory(); + }); + + it('should add a message to the history', () => { + chatHistory.add('Hello'); + expect(chatHistory.getOlderMessage()).toEqual('Hello'); + }); + + it('should retrieve older and newer messages', () => { + chatHistory.add('Hello'); + chatHistory.add('World'); + expect(chatHistory.getOlderMessage()).toEqual('World'); + expect(chatHistory.getOlderMessage()).toEqual('Hello'); + expect(chatHistory.getNewerMessage()).toEqual('World'); + expect(chatHistory.getNewerMessage()).toBeNull(); + expect(chatHistory.getOlderMessage()).toEqual('World'); + }); + + it('should limit the history to 5 messages', () => { + for (let i = 1; i <= 6; i++) { + chatHistory.add(`Message ${i}`); + } + + expect(chatHistory.getOlderMessage()).toEqual('Message 6'); + for (let i = 5; i >= 2; i--) { + expect(chatHistory.getOlderMessage()).toEqual(`Message ${i}`); + } + expect(chatHistory.getOlderMessage()).toBeNull(); + }); + + it('should handle temp message correctly', () => { + chatHistory.saveTemp('Temp message'); + expect(chatHistory.getTemp()).toEqual('Temp message'); + expect(chatHistory.getTemp()).toBeNull(); + }); + + it('should reset correctly', () => { + chatHistory.add('Hello'); + chatHistory.getOlderMessage(); + chatHistory.reset(); + expect(chatHistory.isAtLatest()).toBe(true); + expect(chatHistory.getOlderMessage()).toEqual('Hello'); + }); +}); diff --git a/tgui/packages/tgui-say/ChatHistory.ts b/tgui/packages/tgui-say/ChatHistory.ts new file mode 100644 index 0000000000000..b5490b1887f41 --- /dev/null +++ b/tgui/packages/tgui-say/ChatHistory.ts @@ -0,0 +1,59 @@ +/** + * ### ChatHistory + * A class to manage a chat history, + * maintaining a maximum of five messages and supporting navigation, + * temporary message storage, and query operations. + */ +export class ChatHistory { + private messages: string[] = []; + private index: number = -1; // Initialize index at -1 + private temp: string | null = null; + + public add(message: string): void { + this.messages.unshift(message); + this.index = -1; // Reset index + if (this.messages.length > 5) { + this.messages.pop(); + } + } + + public getIndex(): number { + return this.index + 1; + } + + public getOlderMessage(): string | null { + if (this.messages.length === 0 || this.index >= this.messages.length - 1) { + return null; + } + this.index++; + return this.messages[this.index]; + } + + public getNewerMessage(): string | null { + if (this.index <= 0) { + this.index = -1; + return null; + } + this.index--; + return this.messages[this.index]; + } + + public isAtLatest(): boolean { + return this.index === -1; + } + + public saveTemp(message: string): void { + this.temp = message; + } + + public getTemp(): string | null { + const temp = this.temp; + this.temp = null; + return temp; + } + + public reset(): void { + this.index = -1; + this.temp = null; + } +} diff --git a/tgui/packages/tgui-say/TguiSay.tsx b/tgui/packages/tgui-say/TguiSay.tsx new file mode 100644 index 0000000000000..d832a8e5147c2 --- /dev/null +++ b/tgui/packages/tgui-say/TguiSay.tsx @@ -0,0 +1,354 @@ +import { Channel, ChannelIterator } from './ChannelIterator'; +import { ChatHistory } from './ChatHistory'; +import { Component, createRef, InfernoKeyboardEvent, RefObject } from 'inferno'; +import { LINE_LENGTHS, RADIO_PREFIXES, WINDOW_SIZES } from './constants'; +import { byondMessages } from './timers'; +import { dragStartHandler } from 'tgui/drag'; +import { windowOpen, windowLoad, windowClose, windowSet } from './helpers'; +import { BooleanLike } from 'common/react'; +import { KEY } from 'common/keys'; + +type ByondOpen = { + channel: Channel; +}; + +type ByondProps = { + maxLength: number; + lightMode: BooleanLike; +}; + +type State = { + buttonContent: string | number; + size: WINDOW_SIZES; +}; + +const CHANNEL_REGEX = /^:\w\s/; + +export class TguiSay extends Component<{}, State> { + private channelIterator: ChannelIterator; + private chatHistory: ChatHistory; + private currentPrefix: keyof typeof RADIO_PREFIXES | null; + private innerRef: RefObject; + private lightMode: boolean; + private maxLength: number; + private messages: typeof byondMessages; + state: State; + + constructor(props: never) { + super(props); + + this.channelIterator = new ChannelIterator(); + this.chatHistory = new ChatHistory(); + this.currentPrefix = null; + this.innerRef = createRef(); + this.lightMode = false; + this.maxLength = 1024; + this.messages = byondMessages; + this.state = { + buttonContent: '', + size: WINDOW_SIZES.small, + }; + + this.handleArrowKeys = this.handleArrowKeys.bind(this); + this.handleBackspaceDelete = this.handleBackspaceDelete.bind(this); + this.handleClose = this.handleClose.bind(this); + this.handleEnter = this.handleEnter.bind(this); + this.handleForceSay = this.handleForceSay.bind(this); + this.handleIncrementChannel = this.handleIncrementChannel.bind(this); + this.handleInput = this.handleInput.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); + this.handleOpen = this.handleOpen.bind(this); + this.handleProps = this.handleProps.bind(this); + this.reset = this.reset.bind(this); + this.setSize = this.setSize.bind(this); + this.setValue = this.setValue.bind(this); + } + + componentDidMount() { + Byond.subscribeTo('props', this.handleProps); + Byond.subscribeTo('force', this.handleForceSay); + Byond.subscribeTo('open', this.handleOpen); + windowLoad(); + } + + handleArrowKeys(direction: KEY.Up | KEY.Down) { + const currentValue = this.innerRef.current?.value; + + if (direction === KEY.Up) { + if (this.chatHistory.isAtLatest() && currentValue) { + // Save current message to temp history if at the most recent message + this.chatHistory.saveTemp(currentValue); + } + // Try to get the previous message, fall back to the current value if none + const prevMessage = this.chatHistory.getOlderMessage(); + + if (prevMessage) { + this.setState({ buttonContent: this.chatHistory.getIndex() }); + this.setSize(prevMessage.length); + this.setValue(prevMessage); + } + } else { + const nextMessage = + this.chatHistory.getNewerMessage() || this.chatHistory.getTemp() || ''; + + const buttonContent = this.chatHistory.isAtLatest() + ? this.channelIterator.current() + : this.chatHistory.getIndex(); + + this.setState({ buttonContent }); + this.setSize(nextMessage.length); + this.setValue(nextMessage); + } + } + + handleBackspaceDelete() { + const typed = this.innerRef.current?.value; + + // User is on a chat history message + if (!this.chatHistory.isAtLatest()) { + this.chatHistory.reset(); + this.setState({ + buttonContent: this.currentPrefix ?? this.channelIterator.current(), + }); + // Empty input, resets the channel + } else if ( + !!this.currentPrefix && + this.channelIterator.isSay() && + typed?.length === 0 + ) { + this.currentPrefix = null; + this.setState({ buttonContent: this.channelIterator.current() }); + } + + this.setSize(typed?.length); + } + + handleClose() { + const current = this.innerRef.current; + + if (current) { + current.blur(); + } + + this.reset(); + this.chatHistory.reset(); + this.channelIterator.reset(); + this.currentPrefix = null; + windowClose(); + } + + handleEnter() { + const prefix = this.currentPrefix ?? ''; + const value = this.innerRef.current?.value; + + if (value?.length && value.length < this.maxLength) { + this.chatHistory.add(value); + Byond.sendMessage('entry', { + channel: this.channelIterator.current(), + entry: this.channelIterator.isSay() ? prefix + value : value, + }); + } + + this.handleClose(); + } + + handleForceSay() { + const currentValue = this.innerRef.current?.value; + // Only force say if we're on a visible channel and have typed something + if (!currentValue || !this.channelIterator.isVisible()) return; + + const prefix = this.currentPrefix ?? ''; + const grunt = this.channelIterator.isSay() + ? prefix + currentValue + : currentValue; + + this.messages.forceSayMsg(grunt); + this.reset(); + } + + handleIncrementChannel() { + // Binary talk is a special case, tell byond to show thinking indicators + if (this.channelIterator.isSay() && this.currentPrefix === ':b ') { + this.messages.channelIncrementMsg(true); + } + + this.currentPrefix = null; + + this.channelIterator.next(); + + // If we've looped onto a quiet channel, tell byond to hide thinking indicators + if (!this.channelIterator.isVisible()) { + this.messages.channelIncrementMsg(false); + } + + this.setState({ buttonContent: this.channelIterator.current() }); + } + + handleInput() { + const typed = this.innerRef.current?.value; + + // If we're typing, send the message + if (this.channelIterator.isVisible() && this.currentPrefix !== ':b ') { + this.messages.typingMsg(); + } + + this.setSize(typed?.length); + + // Is there a value? Is it long enough to be a prefix? + if (!typed || typed.length < 3) { + return; + } + + if (!CHANNEL_REGEX.test(typed)) { + return; + } + + // Is it a valid prefix? + const prefix = typed + .slice(0, 3) + ?.toLowerCase() as keyof typeof RADIO_PREFIXES; + if (!RADIO_PREFIXES[prefix] || prefix === this.currentPrefix) { + return; + } + + // If we're in binary, hide the thinking indicator + if (prefix === ':b ') { + Byond.sendMessage('thinking', { visible: false }); + } + + this.channelIterator.set('Say'); + this.currentPrefix = prefix; + this.setState({ buttonContent: RADIO_PREFIXES[prefix] }); + this.setValue(typed.slice(3)); + } + + handleKeyDown(event: InfernoKeyboardEvent) { + switch (event.key) { + case KEY.Up: + case KEY.Down: + event.preventDefault(); + this.handleArrowKeys(event.key); + break; + + case KEY.Delete: + case KEY.Backspace: + this.handleBackspaceDelete(); + break; + + case KEY.Enter: + event.preventDefault(); + this.handleEnter(); + break; + + case KEY.Tab: + event.preventDefault(); + this.handleIncrementChannel(); + break; + + case KEY.Escape: + this.handleClose(); + break; + } + } + + handleOpen = (data: ByondOpen) => { + const { channel } = data; + // Catches the case where the modal is already open + if (this.channelIterator.isSay()) { + this.channelIterator.set(channel); + } + this.setState({ buttonContent: this.channelIterator.current() }); + + setTimeout(() => { + this.innerRef.current?.focus(); + }, 1); + + windowOpen(this.channelIterator.current()); + }; + + handleProps = (data: ByondProps) => { + const { maxLength, lightMode } = data; + this.maxLength = maxLength; + this.lightMode = !!lightMode; + }; + + reset() { + this.setValue(''); + this.setSize(); + this.setState({ + buttonContent: this.channelIterator.current(), + }); + } + + setSize(length = 0) { + let newSize: WINDOW_SIZES; + + if (length > LINE_LENGTHS.medium) { + newSize = WINDOW_SIZES.large; + } else if (length <= LINE_LENGTHS.medium && length > LINE_LENGTHS.small) { + newSize = WINDOW_SIZES.medium; + } else { + newSize = WINDOW_SIZES.small; + } + + if (this.state.size !== newSize) { + this.setState({ size: newSize }); + windowSet(newSize); + } + } + + setValue(value: string) { + const textArea = this.innerRef.current; + if (textArea) { + textArea.value = value; + } + } + + render() { + const theme = + (this.lightMode && 'lightMode') || + (this.currentPrefix && RADIO_PREFIXES[this.currentPrefix]) || + this.channelIterator.current(); + + return ( +
+ +
+ +
+ +