From d3e2d3fc4a8054420fa797f0adb826b400262119 Mon Sep 17 00:00:00 2001 From: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com> Date: Tue, 26 Mar 2024 04:35:15 -0700 Subject: [PATCH] Android: Plugins: Autohide the plugin panel toggle in toolbar to increase size for notebook dropdown (#10212) --- .../app-mobile/components/Dropdown.test.tsx | 33 +++++++ packages/app-mobile/components/Dropdown.tsx | 95 ++++++++++++------- .../app-mobile/components/FolderPicker.tsx | 5 +- .../app-mobile/components/ScreenHeader.tsx | 22 +++-- packages/tools/cspell/dictionary4.txt | 3 +- 5 files changed, 115 insertions(+), 43 deletions(-) diff --git a/packages/app-mobile/components/Dropdown.test.tsx b/packages/app-mobile/components/Dropdown.test.tsx index e349c52ef..c6acf685e 100644 --- a/packages/app-mobile/components/Dropdown.test.tsx +++ b/packages/app-mobile/components/Dropdown.test.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { Text } from 'react-native'; import { describe, it, expect, jest } from '@jest/globals'; import { fireEvent, render, screen, waitFor } from '@testing-library/react-native'; @@ -53,4 +54,36 @@ describe('Dropdown', () => { expect(screen.queryByText('Item 2')).not.toBeNull(); }); }); + + it('should hide coverableChildren to increase space', async () => { + render( + {}} + coverableChildrenRight={Elem Right} + />, + ); + + + expect(screen.queryByText('Test2')).toBeNull(); + expect(screen.getByText('Elem Right')).not.toBeNull(); + + // Open the dropdown + fireEvent.press(screen.getByText('Test1')); + + // Should show the dropdown and hide the right content. + await waitFor(() => { + expect(screen.queryByText('Test2')).not.toBeNull(); + }); + expect(screen.queryByText('Elem Right')).toBeNull(); + + + // Should hide the dropdown and show the right content. + fireEvent.press(screen.getByText('Test3')); + await waitFor(() => { + expect(screen.queryByText('Test2')).toBeNull(); + }); + expect(screen.queryByText('Elem Right')).not.toBeNull(); + }); }); diff --git a/packages/app-mobile/components/Dropdown.tsx b/packages/app-mobile/components/Dropdown.tsx index d6ac6c8d1..a1db3fbb2 100644 --- a/packages/app-mobile/components/Dropdown.tsx +++ b/packages/app-mobile/components/Dropdown.tsx @@ -1,6 +1,6 @@ -const React = require('react'); -import { TouchableOpacity, TouchableWithoutFeedback, Dimensions, Text, Modal, View, LayoutRectangle, ViewStyle, TextStyle, FlatList } from 'react-native'; -import { Component } from 'react'; +import * as React from 'react'; +import { TouchableOpacity, TouchableWithoutFeedback, Dimensions, Text, Modal, View, LayoutRectangle, ViewStyle, TextStyle, FlatList, LayoutChangeEvent } from 'react-native'; +import { Component, ReactElement } from 'react'; import { _ } from '@joplin/lib/locale'; type ValueType = string; @@ -29,6 +29,11 @@ interface DropdownProps { selectedValue: ValueType|null; onValueChange?: OnValueChangedListener; + + // Shown to the right of the dropdown when closed, hidden when opened. + // Avoids abrupt size transitions that would be caused by externally resizing the space + // available for the dropdown on open/close. + coverableChildrenRight?: ReactElement[]|ReactElement; } interface DropdownState { @@ -37,7 +42,7 @@ interface DropdownState { } class Dropdown extends Component { - private headerRef: TouchableOpacity; + private headerRef: View; public constructor(props: DropdownProps) { super(props); @@ -49,14 +54,35 @@ class Dropdown extends Component { }; } - private updateHeaderCoordinates() { + private updateHeaderCoordinates = (event: LayoutChangeEvent) => { + if (!this.headerRef) return; + + const { width, height } = event.nativeEvent.layout; + + const lastLayout = this.state.headerSize; + if (width !== lastLayout.width || height !== lastLayout.height) { + this.setState({ + headerSize: { x: lastLayout.x, y: lastLayout.y, width, height }, + }); + } + // https://stackoverflow.com/questions/30096038/react-native-getting-the-position-of-an-element this.headerRef.measure((_fx, _fy, width, height, px, py) => { - this.setState({ - headerSize: { x: px, y: py, width: width, height: height }, - }); + const lastLayout = this.state.headerSize; + if (px !== lastLayout.x || py !== lastLayout.y || width !== lastLayout.width || height !== lastLayout.height) { + this.setState({ + headerSize: { x: px, y: py, width: width, height: height }, + }); + } }); - } + }; + + private onOpenList = () => { + this.setState({ listVisible: true }); + }; + private onCloseList = () => { + this.setState({ listVisible: false }); + }; public render() { const items = this.props.items; @@ -100,10 +126,13 @@ class Dropdown extends Component { paddingRight: 10, }; - const headerWrapperStyle = { ...(this.props.headerWrapperStyle ? this.props.headerWrapperStyle : {}), height: 35, + const headerWrapperStyle: ViewStyle = { + ...(this.props.headerWrapperStyle ? this.props.headerWrapperStyle : {}), + height: 35, flex: 1, flexDirection: 'row', - alignItems: 'center' }; + alignItems: 'center', + }; const headerStyle = { ...(this.props.headerStyle ? this.props.headerStyle : {}), flex: 1 }; @@ -125,10 +154,6 @@ class Dropdown extends Component { headerLabel = headerLabel.trim(); } - const closeList = () => { - this.setState({ listVisible: false }); - }; - const itemRenderer = ({ item }: { item: DropdownListItem }) => { const key = item.value ? item.value.toString() : '__null'; // The top item ("Move item to notebook...") has a null value. const indentWidth = Math.min((item.depth ?? 0) * 32, dropdownWidth * 2 / 3); @@ -139,7 +164,7 @@ class Dropdown extends Component { accessibilityRole="menuitem" key={key} onPress={() => { - closeList(); + this.onCloseList(); if (this.props.onValueChange) this.props.onValueChange(item.value); }} > @@ -157,7 +182,7 @@ class Dropdown extends Component { const screenReaderCloseMenuButton = ( closeList()} + onPress={this.onCloseList} > { return ( - (this.headerRef = ref)} - disabled={this.props.disabled} - onPress={() => { - this.updateHeaderCoordinates(); - this.setState({ listVisible: true }); - }} > - - {headerLabel} - - {'▼'} - + + + {headerLabel} + + {'▼'} + + {this.state.listVisible ? null : this.props.coverableChildrenRight} + { - closeList(); - }} + onRequestClose={this.onCloseList} supportedOrientations={['landscape', 'portrait']} > { - closeList(); - }} + onPress={this.onCloseList} style={backgroundCloseButtonStyle} > diff --git a/packages/app-mobile/components/FolderPicker.tsx b/packages/app-mobile/components/FolderPicker.tsx index a93c6f2eb..ce02178b2 100644 --- a/packages/app-mobile/components/FolderPicker.tsx +++ b/packages/app-mobile/components/FolderPicker.tsx @@ -1,6 +1,6 @@ const React = require('react'); -import { FunctionComponent } from 'react'; +import { FunctionComponent, ReactElement } from 'react'; import { _ } from '@joplin/lib/locale'; import Folder, { FolderEntityWithChildren } from '@joplin/lib/models/Folder'; import { themeStyle } from './global-style'; @@ -16,6 +16,7 @@ interface FolderPickerProps { placeholder?: string; darkText?: boolean; themeId?: number; + coverableChildrenRight?: ReactElement|ReactElement[]; } @@ -27,6 +28,7 @@ const FolderPicker: FunctionComponent = ({ folders, placeholder, darkText, + coverableChildrenRight, themeId, }) => { const theme = themeStyle(themeId); @@ -66,6 +68,7 @@ const FolderPicker: FunctionComponent = ({ disabled={disabled} labelTransform="trim" selectedValue={selectedFolderId || ''} + coverableChildrenRight={coverableChildrenRight} itemListStyle={{ backgroundColor: theme.backgroundColor, }} diff --git a/packages/app-mobile/components/ScreenHeader.tsx b/packages/app-mobile/components/ScreenHeader.tsx index 021f79f83..f7717a4cc 100644 --- a/packages/app-mobile/components/ScreenHeader.tsx +++ b/packages/app-mobile/components/ScreenHeader.tsx @@ -1,7 +1,7 @@ const React = require('react'); import { connect } from 'react-redux'; -import { PureComponent } from 'react'; +import { PureComponent, ReactElement } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, Image, ScrollView, Dimensions, ViewStyle } from 'react-native'; const Icon = require('react-native-vector-icons/Ionicons').default; const { BackButtonService } = require('../services/back-button.js'); @@ -573,7 +573,7 @@ class ScreenHeaderComponent extends PureComponent { + const createTitleComponent = (disabled: boolean, hideableAfterTitleComponents: ReactElement) => { const folderPickerOptions = this.props.folderPickerOptions; if (folderPickerOptions && folderPickerOptions.enabled) { @@ -613,11 +613,17 @@ class ScreenHeaderComponent extends PureComponent ); } else { const title = 'title' in this.props && this.props.title !== null ? this.props.title : ''; - return {title}; + return ( + <> + {title} + {hideableAfterTitleComponents} + + ); } }; @@ -642,16 +648,21 @@ class ScreenHeaderComponent extends PureComponent 0); - const titleComp = createTitleComponent(headerItemDisabled); const sideMenuComp = !showSideMenuButton ? null : sideMenuButton(this.styles(), () => this.sideMenuButton_press()); const backButtonComp = !showBackButton ? null : backButton(this.styles(), () => this.backButton_press(), backButtonDisabled); + const pluginPanelsComp = pluginPanelToggleButton(this.styles(), () => this.pluginPanelToggleButton_press()); const selectAllButtonComp = !showSelectAllButton ? null : selectAllButton(this.styles(), () => this.selectAllButton_press()); const searchButtonComp = !showSearchButton ? null : searchButton(this.styles(), () => this.searchButton_press()); - const pluginPanelsComp = pluginPanelToggleButton(this.styles(), () => this.pluginPanelToggleButton_press()); const deleteButtonComp = !selectedFolderInTrash && this.props.noteSelectionEnabled ? deleteButton(this.styles(), () => this.deleteButton_press(), headerItemDisabled) : null; const restoreButtonComp = selectedFolderInTrash && this.props.noteSelectionEnabled ? restoreButton(this.styles(), () => this.restoreButton_press(), headerItemDisabled) : null; const duplicateButtonComp = !selectedFolderInTrash && this.props.noteSelectionEnabled ? duplicateButton(this.styles(), () => this.duplicateButton_press(), headerItemDisabled) : null; const sortButtonComp = !this.props.noteSelectionEnabled && this.props.sortButton_press ? sortButton(this.styles(), () => this.props.sortButton_press()) : null; + + // To allow the notebook dropdown (and perhaps other components) to have sufficient + // space while in use, we allow certain buttons to be hidden. + const hideableRightComponents = pluginPanelsComp; + + const titleComp = createTitleComponent(headerItemDisabled, hideableRightComponents); const windowHeight = Dimensions.get('window').height - 50; const contextMenuStyle: ViewStyle = { @@ -692,7 +703,6 @@ class ScreenHeaderComponent extends PureComponent