You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Android: Plugins: Autohide the plugin panel toggle in toolbar to increase size for notebook dropdown (#10212)
This commit is contained in:
		| @@ -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( | ||||
| 			<Dropdown | ||||
| 				items={[{ label: 'Test1', value: '1' }, { label: 'Test2', value: '2' }, { label: 'Test3', value: '3' }]} | ||||
| 				selectedValue={'1'} | ||||
| 				onValueChange={()=>{}} | ||||
| 				coverableChildrenRight={<Text>Elem Right</Text>} | ||||
| 			/>, | ||||
| 		); | ||||
|  | ||||
|  | ||||
| 		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(); | ||||
| 	}); | ||||
| }); | ||||
|   | ||||
| @@ -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<DropdownProps, DropdownState> { | ||||
| 	private headerRef: TouchableOpacity; | ||||
| 	private headerRef: View; | ||||
|  | ||||
| 	public constructor(props: DropdownProps) { | ||||
| 		super(props); | ||||
| @@ -49,14 +54,35 @@ class Dropdown extends Component<DropdownProps, DropdownState> { | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	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<DropdownProps, DropdownState> { | ||||
| 			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<DropdownProps, DropdownState> { | ||||
| 			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<DropdownProps, DropdownState> { | ||||
| 					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<DropdownProps, DropdownState> { | ||||
| 		const screenReaderCloseMenuButton = ( | ||||
| 			<TouchableWithoutFeedback | ||||
| 				accessibilityRole='button' | ||||
| 				onPress={()=> closeList()} | ||||
| 				onPress={this.onCloseList} | ||||
| 			> | ||||
| 				<Text style={{ | ||||
| 					opacity: 0, | ||||
| @@ -168,34 +193,34 @@ class Dropdown extends Component<DropdownProps, DropdownState> { | ||||
|  | ||||
| 		return ( | ||||
| 			<View style={{ flex: 1, flexDirection: 'column' }}> | ||||
| 				<TouchableOpacity | ||||
| 					style={headerWrapperStyle as any} | ||||
| 				<View | ||||
| 					style={{ flexDirection: 'row', flex: 1, alignItems: 'center' }} | ||||
| 					onLayout={this.updateHeaderCoordinates} | ||||
| 					ref={ref => (this.headerRef = ref)} | ||||
| 					disabled={this.props.disabled} | ||||
| 					onPress={() => { | ||||
| 						this.updateHeaderCoordinates(); | ||||
| 						this.setState({ listVisible: true }); | ||||
| 					}} | ||||
| 				> | ||||
| 					<Text ellipsizeMode="tail" numberOfLines={1} style={headerStyle}> | ||||
| 						{headerLabel} | ||||
| 					</Text> | ||||
| 					<Text style={headerArrowStyle}>{'▼'}</Text> | ||||
| 				</TouchableOpacity> | ||||
| 					<TouchableOpacity | ||||
| 						style={headerWrapperStyle} | ||||
| 						disabled={this.props.disabled} | ||||
| 						onPress={this.onOpenList} | ||||
| 					> | ||||
| 						<Text ellipsizeMode="tail" numberOfLines={1} style={headerStyle}> | ||||
| 							{headerLabel} | ||||
| 						</Text> | ||||
| 						<Text style={headerArrowStyle}>{'▼'}</Text> | ||||
| 					</TouchableOpacity> | ||||
| 					{this.state.listVisible ? null : this.props.coverableChildrenRight} | ||||
| 				</View> | ||||
| 				<Modal | ||||
| 					transparent={true} | ||||
| 					animationType='fade' | ||||
| 					visible={this.state.listVisible} | ||||
| 					onRequestClose={() => { | ||||
| 						closeList(); | ||||
| 					}} | ||||
| 					onRequestClose={this.onCloseList} | ||||
| 					supportedOrientations={['landscape', 'portrait']} | ||||
| 				> | ||||
| 					<TouchableWithoutFeedback | ||||
| 						accessibilityElementsHidden={true} | ||||
| 						importantForAccessibility='no-hide-descendants' | ||||
| 						onPress={() => { | ||||
| 							closeList(); | ||||
| 						}} | ||||
| 						onPress={this.onCloseList} | ||||
| 						style={backgroundCloseButtonStyle} | ||||
| 					> | ||||
| 						<View style={{ flex: 1 }}/> | ||||
|   | ||||
| @@ -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<FolderPickerProps> = ({ | ||||
| 	folders, | ||||
| 	placeholder, | ||||
| 	darkText, | ||||
| 	coverableChildrenRight, | ||||
| 	themeId, | ||||
| }) => { | ||||
| 	const theme = themeStyle(themeId); | ||||
| @@ -66,6 +68,7 @@ const FolderPicker: FunctionComponent<FolderPickerProps> = ({ | ||||
| 			disabled={disabled} | ||||
| 			labelTransform="trim" | ||||
| 			selectedValue={selectedFolderId || ''} | ||||
| 			coverableChildrenRight={coverableChildrenRight} | ||||
| 			itemListStyle={{ | ||||
| 				backgroundColor: theme.backgroundColor, | ||||
| 			}} | ||||
|   | ||||
| @@ -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<ScreenHeaderProps, ScreenHeade | ||||
| 			); | ||||
| 		} | ||||
|  | ||||
| 		const createTitleComponent = (disabled: boolean) => { | ||||
| 		const createTitleComponent = (disabled: boolean, hideableAfterTitleComponents: ReactElement) => { | ||||
| 			const folderPickerOptions = this.props.folderPickerOptions; | ||||
|  | ||||
| 			if (folderPickerOptions && folderPickerOptions.enabled) { | ||||
| @@ -613,11 +613,17 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade | ||||
| 						}} | ||||
| 						mustSelect={!!folderPickerOptions.mustSelect} | ||||
| 						folders={Folder.getRealFolders(this.props.folders)} | ||||
| 						coverableChildrenRight={hideableAfterTitleComponents} | ||||
| 					/> | ||||
| 				); | ||||
| 			} else { | ||||
| 				const title = 'title' in this.props && this.props.title !== null ? this.props.title : ''; | ||||
| 				return <Text ellipsizeMode={'tail'} numberOfLines={1} style={this.styles().titleText}>{title}</Text>; | ||||
| 				return ( | ||||
| 					<> | ||||
| 						<Text ellipsizeMode={'tail'} numberOfLines={1} style={this.styles().titleText}>{title}</Text> | ||||
| 						{hideableAfterTitleComponents} | ||||
| 					</> | ||||
| 				); | ||||
| 			} | ||||
| 		}; | ||||
|  | ||||
| @@ -642,16 +648,21 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade | ||||
| 		if (this.props.noteSelectionEnabled) backButtonDisabled = false; | ||||
| 		const headerItemDisabled = !(this.props.selectedNoteIds.length > 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<ScreenHeaderProps, ScreenHeade | ||||
| 						this.props.showSaveButton === true, | ||||
| 					)} | ||||
| 					{titleComp} | ||||
| 					{pluginPanelsComp} | ||||
| 					{selectAllButtonComp} | ||||
| 					{searchButtonComp} | ||||
| 					{deleteButtonComp} | ||||
|   | ||||
| @@ -96,4 +96,5 @@ unresponded | ||||
| activeline | ||||
| Prec | ||||
| ellipsized | ||||
| Trashable | ||||
| Trashable | ||||
| hideable | ||||
		Reference in New Issue
	
	Block a user