2024-01-18 13:22:20 +02:00
|
|
|
import * as React from 'react';
|
2022-08-29 15:19:04 +02:00
|
|
|
|
2023-08-18 10:45:04 +02:00
|
|
|
import { ReactElement, useCallback, useMemo, useState } from 'react';
|
2022-10-30 20:37:58 +02:00
|
|
|
import { LayoutChangeEvent, ScrollView, View, ViewStyle } from 'react-native';
|
2022-08-29 15:19:04 +02:00
|
|
|
import ToggleOverflowButton from './ToggleOverflowButton';
|
|
|
|
import ToolbarButton, { buttonSize } from './ToolbarButton';
|
|
|
|
import ToolbarOverflowRows from './ToolbarOverflowRows';
|
|
|
|
import { ButtonGroup, ButtonSpec, StyleSheetData } from './types';
|
|
|
|
|
|
|
|
interface ToolbarProps {
|
|
|
|
buttons: ButtonGroup[];
|
|
|
|
styleSheet: StyleSheetData;
|
2022-09-09 16:11:58 +02:00
|
|
|
style?: ViewStyle;
|
2022-08-29 15:19:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Displays a list of buttons with an overflow menu.
|
2024-01-18 13:22:20 +02:00
|
|
|
const Toolbar: React.FC<ToolbarProps> = (props: ToolbarProps) => {
|
2022-08-29 15:19:04 +02:00
|
|
|
const [overflowButtonsVisible, setOverflowPopupVisible] = useState(false);
|
|
|
|
const [maxButtonsEachSide, setMaxButtonsEachSide] = useState(0);
|
|
|
|
|
2023-08-18 10:45:04 +02:00
|
|
|
const allButtonSpecs = useMemo(() => {
|
|
|
|
const buttons = props.buttons.reduce((accumulator: ButtonSpec[], current: ButtonGroup) => {
|
|
|
|
const newItems: ButtonSpec[] = [];
|
|
|
|
for (const item of current.items) {
|
|
|
|
if (item.visible ?? true) {
|
|
|
|
newItems.push(item);
|
|
|
|
}
|
2022-08-29 15:19:04 +02:00
|
|
|
}
|
|
|
|
|
2023-08-18 10:45:04 +02:00
|
|
|
return accumulator.concat(...newItems);
|
|
|
|
}, []);
|
2022-08-29 15:19:04 +02:00
|
|
|
|
2023-08-18 10:45:04 +02:00
|
|
|
// Sort from highest priority to lowest
|
|
|
|
buttons.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
|
|
|
return buttons;
|
|
|
|
}, [props.buttons]);
|
2022-08-29 15:19:04 +02:00
|
|
|
|
|
|
|
const allButtonComponents: ReactElement[] = [];
|
|
|
|
let key = 0;
|
|
|
|
for (const spec of allButtonSpecs) {
|
|
|
|
key++;
|
|
|
|
allButtonComponents.push(
|
|
|
|
<ToolbarButton
|
|
|
|
key={key.toString()}
|
|
|
|
styleSheet={props.styleSheet}
|
|
|
|
spec={spec}
|
2023-08-22 12:58:53 +02:00
|
|
|
/>,
|
2022-08-29 15:19:04 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const onContainerLayout = useCallback((event: LayoutChangeEvent) => {
|
|
|
|
const containerWidth = event.nativeEvent.layout.width;
|
|
|
|
const maxButtonsTotal = Math.floor(containerWidth / buttonSize);
|
|
|
|
setMaxButtonsEachSide(Math.floor(
|
2023-08-22 12:58:53 +02:00
|
|
|
Math.min((maxButtonsTotal - 1) / 2, allButtonSpecs.length / 2),
|
2022-08-29 15:19:04 +02:00
|
|
|
));
|
|
|
|
}, [allButtonSpecs.length]);
|
|
|
|
|
|
|
|
const onToggleOverflowVisible = useCallback(() => {
|
|
|
|
setOverflowPopupVisible(!overflowButtonsVisible);
|
|
|
|
}, [overflowButtonsVisible]);
|
|
|
|
|
|
|
|
const toggleOverflowButton = (
|
|
|
|
<ToggleOverflowButton
|
|
|
|
key={(++key).toString()}
|
|
|
|
styleSheet={props.styleSheet}
|
|
|
|
overflowVisible={overflowButtonsVisible}
|
|
|
|
onToggleOverflowVisible={onToggleOverflowVisible}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
|
|
|
|
const mainButtons: ReactElement[] = [];
|
2023-08-18 10:45:04 +02:00
|
|
|
if (maxButtonsEachSide >= allButtonComponents.length) {
|
|
|
|
mainButtons.push(...allButtonComponents);
|
|
|
|
} else if (maxButtonsEachSide > 0) {
|
2022-08-29 15:19:04 +02:00
|
|
|
// We want the menu to look something like this:
|
|
|
|
// B I (…) 🔍 ⌨
|
|
|
|
// where (…) shows/hides overflow.
|
|
|
|
// Add from the left and right of [allButtonComponents] to ensure that
|
|
|
|
// the (…) button is in the center:
|
|
|
|
mainButtons.push(...allButtonComponents.slice(0, maxButtonsEachSide));
|
|
|
|
mainButtons.push(toggleOverflowButton);
|
|
|
|
mainButtons.push(...allButtonComponents.slice(-maxButtonsEachSide));
|
|
|
|
} else {
|
2023-08-18 10:45:04 +02:00
|
|
|
mainButtons.push(toggleOverflowButton);
|
2022-08-29 15:19:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const styles = props.styleSheet.styles;
|
|
|
|
const mainButtonRow = (
|
|
|
|
<View style={styles.toolbarRow}>
|
2022-09-09 16:11:58 +02:00
|
|
|
{ mainButtons }
|
2022-08-29 15:19:04 +02:00
|
|
|
</View>
|
|
|
|
);
|
|
|
|
|
2024-08-02 15:51:49 +02:00
|
|
|
const overflow = (
|
|
|
|
<ScrollView style={{ flex: 1 }}>
|
|
|
|
<ToolbarOverflowRows
|
|
|
|
buttonGroups={props.buttons}
|
|
|
|
styleSheet={props.styleSheet}
|
|
|
|
onToggleOverflow={onToggleOverflowVisible}
|
|
|
|
/>
|
|
|
|
</ScrollView>
|
|
|
|
);
|
|
|
|
|
2022-08-29 15:19:04 +02:00
|
|
|
return (
|
|
|
|
<View
|
|
|
|
style={{
|
|
|
|
...styles.toolbarContainer,
|
|
|
|
|
|
|
|
// The number of buttons displayed is based on the width of the
|
|
|
|
// container. As such, we can't base the container's width on the
|
|
|
|
// size of its content.
|
|
|
|
width: '100%',
|
2022-09-09 16:11:58 +02:00
|
|
|
|
|
|
|
...props.style,
|
2022-08-29 15:19:04 +02:00
|
|
|
}}
|
|
|
|
onLayout={onContainerLayout}
|
|
|
|
>
|
2024-08-02 15:51:49 +02:00
|
|
|
{ overflowButtonsVisible ? overflow : null }
|
2022-09-09 16:11:58 +02:00
|
|
|
{ !overflowButtonsVisible ? mainButtonRow : null }
|
2022-08-29 15:19:04 +02:00
|
|
|
</View>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
export default Toolbar;
|