1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Android: Plugins: Autohide the plugin panel toggle in toolbar to increase size for notebook dropdown (#10212)

This commit is contained in:
Henry Heino 2024-03-26 04:35:15 -07:00 committed by GitHub
parent 9b5ee63638
commit d3e2d3fc4a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 115 additions and 43 deletions

View File

@ -1,4 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { Text } from 'react-native';
import { describe, it, expect, jest } from '@jest/globals'; import { describe, it, expect, jest } from '@jest/globals';
import { fireEvent, render, screen, waitFor } from '@testing-library/react-native'; import { fireEvent, render, screen, waitFor } from '@testing-library/react-native';
@ -53,4 +54,36 @@ describe('Dropdown', () => {
expect(screen.queryByText('Item 2')).not.toBeNull(); 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();
});
}); });

View File

@ -1,6 +1,6 @@
const React = require('react'); import * as React from 'react';
import { TouchableOpacity, TouchableWithoutFeedback, Dimensions, Text, Modal, View, LayoutRectangle, ViewStyle, TextStyle, FlatList } from 'react-native'; import { TouchableOpacity, TouchableWithoutFeedback, Dimensions, Text, Modal, View, LayoutRectangle, ViewStyle, TextStyle, FlatList, LayoutChangeEvent } from 'react-native';
import { Component } from 'react'; import { Component, ReactElement } from 'react';
import { _ } from '@joplin/lib/locale'; import { _ } from '@joplin/lib/locale';
type ValueType = string; type ValueType = string;
@ -29,6 +29,11 @@ interface DropdownProps {
selectedValue: ValueType|null; selectedValue: ValueType|null;
onValueChange?: OnValueChangedListener; 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 { interface DropdownState {
@ -37,7 +42,7 @@ interface DropdownState {
} }
class Dropdown extends Component<DropdownProps, DropdownState> { class Dropdown extends Component<DropdownProps, DropdownState> {
private headerRef: TouchableOpacity; private headerRef: View;
public constructor(props: DropdownProps) { public constructor(props: DropdownProps) {
super(props); 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 // https://stackoverflow.com/questions/30096038/react-native-getting-the-position-of-an-element
this.headerRef.measure((_fx, _fy, width, height, px, py) => { this.headerRef.measure((_fx, _fy, width, height, px, py) => {
this.setState({ const lastLayout = this.state.headerSize;
headerSize: { x: px, y: py, width: width, height: height }, 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() { public render() {
const items = this.props.items; const items = this.props.items;
@ -100,10 +126,13 @@ class Dropdown extends Component<DropdownProps, DropdownState> {
paddingRight: 10, 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, flex: 1,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center' }; alignItems: 'center',
};
const headerStyle = { ...(this.props.headerStyle ? this.props.headerStyle : {}), flex: 1 }; const headerStyle = { ...(this.props.headerStyle ? this.props.headerStyle : {}), flex: 1 };
@ -125,10 +154,6 @@ class Dropdown extends Component<DropdownProps, DropdownState> {
headerLabel = headerLabel.trim(); headerLabel = headerLabel.trim();
} }
const closeList = () => {
this.setState({ listVisible: false });
};
const itemRenderer = ({ item }: { item: DropdownListItem }) => { 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 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); const indentWidth = Math.min((item.depth ?? 0) * 32, dropdownWidth * 2 / 3);
@ -139,7 +164,7 @@ class Dropdown extends Component<DropdownProps, DropdownState> {
accessibilityRole="menuitem" accessibilityRole="menuitem"
key={key} key={key}
onPress={() => { onPress={() => {
closeList(); this.onCloseList();
if (this.props.onValueChange) this.props.onValueChange(item.value); if (this.props.onValueChange) this.props.onValueChange(item.value);
}} }}
> >
@ -157,7 +182,7 @@ class Dropdown extends Component<DropdownProps, DropdownState> {
const screenReaderCloseMenuButton = ( const screenReaderCloseMenuButton = (
<TouchableWithoutFeedback <TouchableWithoutFeedback
accessibilityRole='button' accessibilityRole='button'
onPress={()=> closeList()} onPress={this.onCloseList}
> >
<Text style={{ <Text style={{
opacity: 0, opacity: 0,
@ -168,34 +193,34 @@ class Dropdown extends Component<DropdownProps, DropdownState> {
return ( return (
<View style={{ flex: 1, flexDirection: 'column' }}> <View style={{ flex: 1, flexDirection: 'column' }}>
<TouchableOpacity <View
style={headerWrapperStyle as any} style={{ flexDirection: 'row', flex: 1, alignItems: 'center' }}
onLayout={this.updateHeaderCoordinates}
ref={ref => (this.headerRef = ref)} ref={ref => (this.headerRef = ref)}
disabled={this.props.disabled}
onPress={() => {
this.updateHeaderCoordinates();
this.setState({ listVisible: true });
}}
> >
<Text ellipsizeMode="tail" numberOfLines={1} style={headerStyle}> <TouchableOpacity
{headerLabel} style={headerWrapperStyle}
</Text> disabled={this.props.disabled}
<Text style={headerArrowStyle}>{'▼'}</Text> onPress={this.onOpenList}
</TouchableOpacity> >
<Text ellipsizeMode="tail" numberOfLines={1} style={headerStyle}>
{headerLabel}
</Text>
<Text style={headerArrowStyle}>{'▼'}</Text>
</TouchableOpacity>
{this.state.listVisible ? null : this.props.coverableChildrenRight}
</View>
<Modal <Modal
transparent={true} transparent={true}
animationType='fade'
visible={this.state.listVisible} visible={this.state.listVisible}
onRequestClose={() => { onRequestClose={this.onCloseList}
closeList();
}}
supportedOrientations={['landscape', 'portrait']} supportedOrientations={['landscape', 'portrait']}
> >
<TouchableWithoutFeedback <TouchableWithoutFeedback
accessibilityElementsHidden={true} accessibilityElementsHidden={true}
importantForAccessibility='no-hide-descendants' importantForAccessibility='no-hide-descendants'
onPress={() => { onPress={this.onCloseList}
closeList();
}}
style={backgroundCloseButtonStyle} style={backgroundCloseButtonStyle}
> >
<View style={{ flex: 1 }}/> <View style={{ flex: 1 }}/>

View File

@ -1,6 +1,6 @@
const React = require('react'); const React = require('react');
import { FunctionComponent } from 'react'; import { FunctionComponent, ReactElement } from 'react';
import { _ } from '@joplin/lib/locale'; import { _ } from '@joplin/lib/locale';
import Folder, { FolderEntityWithChildren } from '@joplin/lib/models/Folder'; import Folder, { FolderEntityWithChildren } from '@joplin/lib/models/Folder';
import { themeStyle } from './global-style'; import { themeStyle } from './global-style';
@ -16,6 +16,7 @@ interface FolderPickerProps {
placeholder?: string; placeholder?: string;
darkText?: boolean; darkText?: boolean;
themeId?: number; themeId?: number;
coverableChildrenRight?: ReactElement|ReactElement[];
} }
@ -27,6 +28,7 @@ const FolderPicker: FunctionComponent<FolderPickerProps> = ({
folders, folders,
placeholder, placeholder,
darkText, darkText,
coverableChildrenRight,
themeId, themeId,
}) => { }) => {
const theme = themeStyle(themeId); const theme = themeStyle(themeId);
@ -66,6 +68,7 @@ const FolderPicker: FunctionComponent<FolderPickerProps> = ({
disabled={disabled} disabled={disabled}
labelTransform="trim" labelTransform="trim"
selectedValue={selectedFolderId || ''} selectedValue={selectedFolderId || ''}
coverableChildrenRight={coverableChildrenRight}
itemListStyle={{ itemListStyle={{
backgroundColor: theme.backgroundColor, backgroundColor: theme.backgroundColor,
}} }}

View File

@ -1,7 +1,7 @@
const React = require('react'); const React = require('react');
import { connect } from 'react-redux'; 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'; import { View, Text, StyleSheet, TouchableOpacity, Image, ScrollView, Dimensions, ViewStyle } from 'react-native';
const Icon = require('react-native-vector-icons/Ionicons').default; const Icon = require('react-native-vector-icons/Ionicons').default;
const { BackButtonService } = require('../services/back-button.js'); 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; const folderPickerOptions = this.props.folderPickerOptions;
if (folderPickerOptions && folderPickerOptions.enabled) { if (folderPickerOptions && folderPickerOptions.enabled) {
@ -613,11 +613,17 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
}} }}
mustSelect={!!folderPickerOptions.mustSelect} mustSelect={!!folderPickerOptions.mustSelect}
folders={Folder.getRealFolders(this.props.folders)} folders={Folder.getRealFolders(this.props.folders)}
coverableChildrenRight={hideableAfterTitleComponents}
/> />
); );
} else { } else {
const title = 'title' in this.props && this.props.title !== null ? this.props.title : ''; 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; if (this.props.noteSelectionEnabled) backButtonDisabled = false;
const headerItemDisabled = !(this.props.selectedNoteIds.length > 0); const headerItemDisabled = !(this.props.selectedNoteIds.length > 0);
const titleComp = createTitleComponent(headerItemDisabled);
const sideMenuComp = !showSideMenuButton ? null : sideMenuButton(this.styles(), () => this.sideMenuButton_press()); const sideMenuComp = !showSideMenuButton ? null : sideMenuButton(this.styles(), () => this.sideMenuButton_press());
const backButtonComp = !showBackButton ? null : backButton(this.styles(), () => this.backButton_press(), backButtonDisabled); 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 selectAllButtonComp = !showSelectAllButton ? null : selectAllButton(this.styles(), () => this.selectAllButton_press());
const searchButtonComp = !showSearchButton ? null : searchButton(this.styles(), () => this.searchButton_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 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 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 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; 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 windowHeight = Dimensions.get('window').height - 50;
const contextMenuStyle: ViewStyle = { const contextMenuStyle: ViewStyle = {
@ -692,7 +703,6 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
this.props.showSaveButton === true, this.props.showSaveButton === true,
)} )}
{titleComp} {titleComp}
{pluginPanelsComp}
{selectAllButtonComp} {selectAllButtonComp}
{searchButtonComp} {searchButtonComp}
{deleteButtonComp} {deleteButtonComp}

View File

@ -96,4 +96,5 @@ unresponded
activeline activeline
Prec Prec
ellipsized ellipsized
Trashable Trashable
hideable