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 { 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();
});
});

View File

@ -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 }}/>

View File

@ -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,
}}

View File

@ -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}

View File

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