From 81c43ecc3d17e0dbf7ad1d949b6d977f2c65bd48 Mon Sep 17 00:00:00 2001 From: muhamedsalih-tw <104364298+muhamedsalih-tw@users.noreply.github.com> Date: Thu, 27 Oct 2022 07:13:47 +0530 Subject: fix: 'failed prop' warning in QuickSwitchModal, SettingsNavigation, SettingsWindow and Recipe component tree (#713) * chore: turn off eslint rule @typescript-eslint/no-useless-constructor to initialize dynamic props & state Co-authored-by: Muhamed <> Co-authored-by: Vijay A --- src/features/quickSwitch/Component.tsx | 349 +++++++++++++++++++++++++++++++++ 1 file changed, 349 insertions(+) create mode 100644 src/features/quickSwitch/Component.tsx (limited to 'src/features/quickSwitch/Component.tsx') diff --git a/src/features/quickSwitch/Component.tsx b/src/features/quickSwitch/Component.tsx new file mode 100644 index 000000000..fb85d61e1 --- /dev/null +++ b/src/features/quickSwitch/Component.tsx @@ -0,0 +1,349 @@ +import { ChangeEvent, Component, createRef, ReactElement } from 'react'; +import { getCurrentWindow } from '@electron/remote'; +import { observer, inject } from 'mobx-react'; +import { reaction } from 'mobx'; +import withStyles, { WithStylesProps } from 'react-jss'; +import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; +import { compact, invoke, noop } from 'lodash'; +import { StoresProps } from '../../@types/ferdium-components.types'; +import Service from '../../models/Service'; +import Input from '../../components/ui/input/index'; +import { H1 } from '../../components/ui/headline'; +import Modal from '../../components/ui/Modal'; +import { state as ModalState } from './store'; + +const messages = defineMessages({ + title: { + id: 'feature.quickSwitch.title', + defaultMessage: 'QuickSwitch', + }, + search: { + id: 'feature.quickSwitch.search', + defaultMessage: 'Search...', + }, + info: { + id: 'feature.quickSwitch.info', + defaultMessage: + 'Select a service with TAB, ↑ and ↓. Open a service with ENTER.', + }, +}); + +const styles = theme => ({ + modal: { + width: '80%', + maxWidth: 600, + background: theme.styleTypes.primary.contrast, + paddingTop: 30, + }, + headline: { + fontSize: 20, + marginBottom: 20, + marginTop: -27, + }, + services: { + width: '100%', + maxHeight: '50vh', + overflowX: 'hidden', + overflowY: 'auto', + }, + service: { + background: theme.styleTypes.primary.contrast, + color: theme.colorText, + borderRadius: 6, + padding: '3px 25px', + marginBottom: 10, + display: 'flex', + alignItems: 'center', + '&:last-child': { + marginBottom: 0, + }, + '&:hover': { + cursor: 'pointer', + }, + }, + activeService: { + background: `${theme.styleTypes.primary.accent} !important`, + color: theme.styleTypes.primary.contrast, + cursor: 'pointer', + }, + serviceIcon: { + width: 50, + height: 50, + paddingRight: 20, + objectFit: 'contain', + }, +}); + +interface IProps + extends WithStylesProps, + Partial, + WrappedComponentProps {} + +interface IState { + selected: number; + search: string; + wasPrevVisible: boolean; +} + +@inject('stores', 'actions') +@observer +class QuickSwitchModal extends Component { + ARROW_DOWN = 40; + + ARROW_UP = 38; + + ENTER = 13; + + TAB = 9; + + inputRef = createRef(); + + serviceElements = {}; + + constructor(props) { + super(props); + + this.state = { + selected: 0, + search: '', + wasPrevVisible: false, + }; + + this._handleKeyDown = this._handleKeyDown.bind(this); + this._handleSearchUpdate = this._handleSearchUpdate.bind(this); + this._handleVisibilityChange = this._handleVisibilityChange.bind(this); + this.openService = this.openService.bind(this); + + reaction( + () => ModalState.isModalVisible, + () => { + this._handleVisibilityChange(); + }, + ); + } + + // Add global keydown listener when component mounts + componentDidMount(): void { + document.addEventListener('keydown', this._handleKeyDown); + } + + // Remove global keydown listener when component unmounts + componentWillUnmount(): void { + document.removeEventListener('keydown', this._handleKeyDown); + } + + // Get currently shown services + services(): Service[] { + let services: Service[] = []; + if ( + this.state.search && + compact(invoke(this.state.search, 'match', /^[\da-z]/i)).length > 0 + ) { + // Apply simple search algorythm to list of all services + services = this.props.stores!.services.allDisplayed; + services = services.filter( + service => + service.name.toLowerCase().search(this.state.search.toLowerCase()) !== + -1, + ); + } else if (this.props.stores!.services.allDisplayed.length > 0) { + // Add the currently active service first + const currentService = this.props.stores!.services.active; + if (currentService) { + services.push(currentService); + } + + // Add last used services to services array + for (const service of this.props.stores!.services.lastUsedServices) { + const tempService = this.props.stores!.services.one(service); + if (tempService && !services.includes(tempService)) { + services.push(tempService); + } + } + + // Add all other services in the default order + for (const service of this.props.stores!.services.allDisplayed) { + if (!services.includes(service)) { + services.push(service); + } + } + } + + return services; + } + + openService(index): void { + // Open service + const service = this.services()[index]; + this.props.actions!.service.setActive({ serviceId: service.id }); + + // Reset and close modal + this.setState({ + selected: 0, + search: '', + }); + this.close(); + } + + // Change the selected service + // factor should be -1 or 1 + changeSelected(factor: number): any { + this.setState(state => { + let newSelected = state.selected + factor; + const services = this.services().length; + + // Roll around when on edge of list + if (state.selected < 1 && factor === -1) { + newSelected = services - 1; + } else if (state.selected >= services - 1 && factor === 1) { + newSelected = 0; + } + + // Make sure new selection is visible + const serviceElement = this.serviceElements[newSelected]; + if (serviceElement) { + serviceElement.scrollIntoViewIfNeeded(false); + } + + return { + selected: newSelected, + }; + }); + } + + // Handle global key presses to change the selection + _handleKeyDown(event: KeyboardEvent): void { + if (ModalState.isModalVisible) { + switch (event.keyCode) { + case this.ARROW_DOWN: + this.changeSelected(1); + break; + case this.TAB: + if (event.shiftKey) { + this.changeSelected(-1); + } else { + this.changeSelected(1); + } + break; + case this.ARROW_UP: + this.changeSelected(-1); + break; + case this.ENTER: + this.openService(this.state.selected); + break; + default: + break; + } + } + } + + // Handle update of the search query + _handleSearchUpdate(event: ChangeEvent): void { + this.setState({ + search: event.target.value, + }); + } + + _handleVisibilityChange(): void { + const { isModalVisible } = ModalState; + + if (isModalVisible && !this.state.wasPrevVisible) { + // Set focus back on current window if its in a service + // TODO: Find a way to gain back focus + getCurrentWindow().blurWebView(); + getCurrentWindow().webContents.focus(); + + // The input "focus" attribute will only work on first modal open + // Manually add focus to the input element + // Wrapped inside timeout to let the modal render first + setTimeout(() => { + if (this.inputRef.current) { + this.inputRef.current.querySelectorAll('input')[0].focus(); + } + }, 10); + + this.setState({ + wasPrevVisible: true, + }); + } else if (!isModalVisible && this.state.wasPrevVisible) { + // Manually blur focus from the input element to prevent + // search query change when modal not visible + setTimeout(() => { + if (this.inputRef.current) { + this.inputRef.current.querySelectorAll('input')[0].blur(); + } + }, 100); + + this.setState({ + wasPrevVisible: false, + }); + } + } + + // Close this modal + close(): void { + ModalState.isModalVisible = false; + } + + render(): ReactElement { + const { isModalVisible } = ModalState; + const { openService } = this; + const { classes, intl } = this.props; + const services = this.services(); + + return ( + +

+ {intl.formatMessage(messages.title)} +

+
+ +
+ +
+ {services.map((service, index) => ( +
openService(index)} + onKeyDown={noop} + key={service.id} + ref={el => { + this.serviceElements[index] = el; + }} + > + {service.recipe.name} +
{service.name}
+
+ ))} +
+ +

+
+ {intl.formatMessage(messages.info)} +

+
+ ); + } +} + +export default injectIntl( + withStyles(styles, { injectTheme: true })(QuickSwitchModal), +); -- cgit v1.2.3-70-g09d2