Improve sidebar auto-floating behavior (#868)

I got some feedback in the discord that this behavior was disruptive
when zooming in.

- I’ve reduced the breakpoint so hopefully the disruption of the
transition is matched by a significant improvement in available space
now.
- Also the 2 places in the app that use window width (including here)
now check for the width of the `<html>` tag, not the width of the
viewport (those 2 values can be different when doing a pinch-zoom,
causing undesirable layout shifts.)
- Most of the logic has been rewritten to improve the transitions

Mobile & desktop experience

https://user-images.githubusercontent.com/25517624/233653721-b13c5e22-ae11-4bdf-a494-a6916556d05e.mov

https://user-images.githubusercontent.com/25517624/233654784-b6cc1006-44ea-4066-be7a-8d0dd786fb5b.mov

(I’d like tapping on something to close the sidebar on mobile, but that
can be approached in a future PR)
This commit is contained in:
Jed Fox
2023-04-21 17:48:47 -04:00
committed by GitHub
parent e9188813fd
commit 944c7ff30f
13 changed files with 401 additions and 474 deletions

View File

@@ -28,6 +28,8 @@ module.exports = {
require('confusing-browser-globals').filter(g => g !== 'self'),
),
'react/jsx-no-useless-fragment': 'error',
'rulesdir/typography': 'error',
// https://github.com/eslint/eslint/issues/16954

View File

@@ -234,10 +234,7 @@ class FinancesApp extends React.Component {
}
handleWindowResize() {
this.setState({
isMobile: isMobile(),
windowWidth: window.innerWidth,
});
this.setState({ isMobile: isMobile() });
}
componentDidMount() {

View File

@@ -1,15 +1,11 @@
import React, { useState, useEffect, useContext } from 'react';
import React, { useState, useContext, useMemo } from 'react';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { useViewportSize } from '@react-aria/utils';
import mitt from 'mitt';
import * as actions from 'loot-core/src/client/actions';
import { colors } from '../style';
import { breakpoints } from '../tokens';
import { View } from './common';
import { SIDEBAR_WIDTH } from './sidebar';
import SidebarWithData from './SidebarWithData';
@@ -17,115 +13,66 @@ import SidebarWithData from './SidebarWithData';
const SidebarContext = React.createContext(null);
export function SidebarProvider({ children }) {
let emitter = mitt();
let [hidden, setHidden] = useState(true);
return (
<SidebarContext.Provider
value={{
show: () => emitter.emit('show'),
hide: () => emitter.emit('hide'),
toggle: () => emitter.emit('toggle'),
on: (name, listener) => {
emitter.on(name, listener);
return () => emitter.off(name, listener);
},
}}
>
<SidebarContext.Provider value={[hidden, setHidden]}>
{children}
</SidebarContext.Provider>
);
}
export function useSidebar() {
return useContext(SidebarContext);
useViewportSize(); // Force re-render on window resize
let windowWidth = document.documentElement.clientWidth;
let alwaysFloats = windowWidth < 668;
let [hidden, setHidden] = useContext(SidebarContext);
return useMemo(
() => ({ hidden, setHidden, alwaysFloats }),
[hidden, setHidden, alwaysFloats],
);
}
function Sidebar({ floatingSidebar }) {
let [hidden, setHidden] = useState(true);
let sidebar = useSidebar();
let windowWidth = useViewportSize().width;
let sidebarShouldFloat = floatingSidebar || windowWidth < breakpoints.medium;
if (!sidebarShouldFloat && hidden) {
setHidden(false);
}
useEffect(() => {
let cleanups = [
sidebar.on('show', () => setHidden(false)),
sidebar.on('hide', () => setHidden(true)),
sidebar.on('toggle', () => setHidden(hidden => !hidden)),
];
return () => {
cleanups.forEach(fn => fn());
};
}, [sidebar]);
let sidebarShouldFloat = floatingSidebar || sidebar.alwaysFloats;
return (
<>
{sidebarShouldFloat && (
<View
onMouseOver={() => setHidden(false)}
onMouseLeave={() => setHidden(true)}
style={{
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
width: hidden ? 0 : 160,
zIndex: 999,
}}
></View>
)}
<View
onMouseOver={
sidebarShouldFloat
? e => {
e.stopPropagation();
setHidden(false);
}
: null
}
onMouseLeave={sidebarShouldFloat ? () => setHidden(true) : null}
style={{
position: 'absolute',
top: 50,
// If not floating, the -50 takes into account the transform below
bottom: sidebarShouldFloat ? 50 : -50,
zIndex: 1001,
borderRadius: '0 6px 6px 0',
overflow: 'hidden',
boxShadow:
!sidebarShouldFloat || hidden
? 'none'
: '0 15px 30px 0 rgba(0,0,0,0.25), 0 3px 15px 0 rgba(0,0,0,.5)',
transform: `translateY(${!sidebarShouldFloat ? -50 : 0}px)
translateX(${hidden ? -SIDEBAR_WIDTH : 0}px)`,
transition: 'transform .5s, box-shadow .5s',
}}
>
<SidebarWithData />
</View>
<View
style={[
{
backgroundColor: colors.n1,
opacity: sidebarShouldFloat ? 0 : 1,
transform: `translateX(${sidebarShouldFloat ? -50 : 0}px)`,
transition: 'transform .4s, opacity .2s',
width: SIDEBAR_WIDTH,
},
sidebarShouldFloat && {
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
},
]}
></View>
</>
<View
onMouseOver={
sidebarShouldFloat
? e => {
e.stopPropagation();
sidebar.setHidden(false);
}
: null
}
onMouseLeave={sidebarShouldFloat ? () => sidebar.setHidden(true) : null}
style={{
position: sidebarShouldFloat ? 'absolute' : null,
top: 12,
// If not floating, the -50 takes into account the transform below
bottom: sidebarShouldFloat ? 12 : -50,
zIndex: 1001,
borderRadius: sidebarShouldFloat ? '0 6px 6px 0' : 0,
overflow: 'hidden',
boxShadow:
!sidebarShouldFloat || sidebar.hidden
? 'none'
: '0 15px 30px 0 rgba(0,0,0,0.25), 0 3px 15px 0 rgba(0,0,0,.5)',
transform: `translateY(${!sidebarShouldFloat ? -12 : 0}px)
translateX(${
sidebarShouldFloat && sidebar.hidden
? -SIDEBAR_WIDTH
: 0
}px)`,
transition:
'transform .5s, box-shadow .5s, border-radius .5s, bottom .5s',
}}
>
<SidebarWithData />
</View>
);
}

View File

@@ -2,7 +2,6 @@ import React, { useState, useEffect, useRef, useContext } from 'react';
import { connect } from 'react-redux';
import { Switch, Route, withRouter } from 'react-router-dom';
import { useViewportSize } from '@react-aria/utils';
import { css, media } from 'glamor';
import * as actions from 'loot-core/src/client/actions';
@@ -13,10 +12,9 @@ import { listen } from 'loot-core/src/platform/client/fetch';
import useFeatureFlag from '../hooks/useFeatureFlag';
import ArrowLeft from '../icons/v1/ArrowLeft';
import AlertTriangle from '../icons/v2/AlertTriangle';
import ArrowButtonRight1 from '../icons/v2/ArrowButtonRight1';
import NavigationMenu from '../icons/v2/NavigationMenu';
import { colors } from '../style';
import tokens, { breakpoints } from '../tokens';
import tokens from '../tokens';
import AccountSyncCheck from './accounts/AccountSyncCheck';
import AnimatedRefresh from './AnimatedRefresh';
@@ -267,9 +265,6 @@ function Titlebar({
let sidebar = useSidebar();
const serverURL = useServerURL();
let windowWidth = useViewportSize().width;
let sidebarAlwaysFloats = windowWidth < breakpoints.medium;
return (
<View
style={[
@@ -289,50 +284,30 @@ function Titlebar({
style,
]}
>
{(floatingSidebar || sidebarAlwaysFloats) && (
{(floatingSidebar || sidebar.alwaysFloats) && (
<Button
bare
style={{
marginRight: 8,
'& .arrow-right': { opacity: 0, transition: 'opacity .3s' },
'& .menu': { opacity: 1, transition: 'opacity .3s' },
'&:hover .arrow-right': !sidebarAlwaysFloats && { opacity: 1 },
'&:hover .menu': !sidebarAlwaysFloats && { opacity: 0 },
style={{ marginRight: 8 }}
onPointerEnter={e => {
if (e.pointerType === 'mouse') {
sidebar.setHidden(false);
}
}}
onMouseEnter={() => sidebar.show()}
onMouseLeave={() => sidebar.hide()}
onClick={() => {
if (windowWidth >= breakpoints.medium) {
saveGlobalPrefs({ floatingSidebar: !floatingSidebar });
} else {
sidebar.toggle();
onPointerLeave={e => {
if (e.pointerType === 'mouse') {
sidebar.setHidden(true);
}
}}
onPointerUp={e => {
if (e.pointerType !== 'mouse') {
sidebar.setHidden(!sidebar.hidden);
}
}}
>
<View style={{ width: 15, height: 15 }}>
<ArrowButtonRight1
className="arrow-right"
style={{
width: 13,
height: 13,
color: colors.n5,
position: 'absolute',
top: 1,
left: 1,
}}
/>
<NavigationMenu
className="menu"
style={{
width: 15,
height: 15,
color: colors.n5,
position: 'absolute',
top: 0,
left: 0,
}}
/>
</View>
<NavigationMenu
className="menu"
style={{ width: 15, height: 15, color: colors.n5, left: 0 }}
/>
</Button>
)}

View File

@@ -394,48 +394,46 @@ function ListBoxSection({ section, state }) {
// The heading is rendered inside an <li> element, which contains
// a <ul> with the child items.
return (
<>
<li {...itemProps} style={{ width: '100%' }}>
{section.rendered && (
<div
{...headingProps}
style={{
...styles.smallText,
backgroundColor: colors.n10,
borderBottom: `1px solid ${colors.n9}`,
borderTop: `1px solid ${colors.n9}`,
color: colors.n4,
display: 'flex',
justifyContent: 'center',
paddingBottom: 4,
paddingTop: 4,
position: 'sticky',
top: '0',
width: '100%',
zIndex: zIndices.SECTION_HEADING,
}}
>
{section.rendered}
</div>
)}
<ul
{...groupProps}
<li {...itemProps} style={{ width: '100%' }}>
{section.rendered && (
<div
{...headingProps}
style={{
padding: 0,
listStyle: 'none',
...styles.smallText,
backgroundColor: colors.n10,
borderBottom: `1px solid ${colors.n9}`,
borderTop: `1px solid ${colors.n9}`,
color: colors.n4,
display: 'flex',
justifyContent: 'center',
paddingBottom: 4,
paddingTop: 4,
position: 'sticky',
top: '0',
width: '100%',
zIndex: zIndices.SECTION_HEADING,
}}
>
{[...section.childNodes].map((node, index, nodes) => (
<Option
key={node.key}
item={node}
state={state}
isLast={index === nodes.length - 1}
/>
))}
</ul>
</li>
</>
{section.rendered}
</div>
)}
<ul
{...groupProps}
style={{
padding: 0,
listStyle: 'none',
}}
>
{[...section.childNodes].map((node, index, nodes) => (
<Option
key={node.key}
item={node}
state={state}
isLast={index === nodes.length - 1}
/>
))}
</ul>
</li>
);
}

View File

@@ -424,28 +424,26 @@ function PayeeCell({
inputStyle,
}) => {
return (
<>
<PayeeAutocomplete
payees={payees}
accounts={accounts}
value={payeeId}
shouldSaveFromKey={shouldSaveFromKey}
inputProps={{
onBlur,
onKeyDown,
style: inputStyle,
}}
showManagePayees={true}
tableBehavior={true}
defaultFocusTransferPayees={transaction.is_child}
focused={true}
onUpdate={onUpdate}
onSelect={onSave}
onManagePayees={() => onManagePayees(payeeId)}
isCreatable
menuPortalTarget={undefined}
/>
</>
<PayeeAutocomplete
payees={payees}
accounts={accounts}
value={payeeId}
shouldSaveFromKey={shouldSaveFromKey}
inputProps={{
onBlur,
onKeyDown,
style: inputStyle,
}}
showManagePayees={true}
tableBehavior={true}
defaultFocusTransferPayees={transaction.is_child}
focused={true}
onUpdate={onUpdate}
onSelect={onSave}
onManagePayees={() => onManagePayees(payeeId)}
isCreatable
menuPortalTarget={undefined}
/>
);
}}
</CustomCell>

View File

@@ -85,119 +85,117 @@ export default function ConfigServer() {
}
return (
<>
<View style={{ maxWidth: 500, marginTop: -30 }}>
<Title text="Wheres the server?" />
<View style={{ maxWidth: 500, marginTop: -30 }}>
<Title text="Wheres the server?" />
<Text
style={{
fontSize: 16,
color: colors.n2,
lineHeight: 1.5,
}}
>
{currentUrl ? (
<>
Existing sessions will be logged out and you will log in to this
server. We will validate that Actual is running at this URL.
</>
) : (
<>
There is no server configured. After running the server, specify the
URL here to use the app. You can always change this later. We will
validate that Actual is running at this URL.
</>
)}
</Text>
{error && (
<Text
style={{
fontSize: 16,
color: colors.n2,
lineHeight: 1.5,
marginTop: 20,
color: colors.r4,
borderRadius: 4,
fontSize: 15,
}}
>
{currentUrl ? (
<>
Existing sessions will be logged out and you will log in to this
server. We will validate that Actual is running at this URL.
</>
) : (
<>
There is no server configured. After running the server, specify
the URL here to use the app. You can always change this later. We
will validate that Actual is running at this URL.
</>
)}
{getErrorMessage(error)}
</Text>
)}
{error && (
<Text
style={{
marginTop: 20,
color: colors.r4,
borderRadius: 4,
fontSize: 15,
}}
<form
style={{ display: 'flex', flexDirection: 'row', marginTop: 30 }}
onSubmit={e => {
e.preventDefault();
onSubmit();
}}
>
<Input
autoFocus={true}
placeholder={'https://example.com'}
value={url}
onChange={e => setUrl(e.target.value)}
style={{ flex: 1, marginRight: 10 }}
/>
<ButtonWithLoading primary loading={loading} style={{ fontSize: 15 }}>
OK
</ButtonWithLoading>
{currentUrl && (
<Button
bare
type="button"
style={{ fontSize: 15, marginLeft: 10 }}
onClick={() => history.goBack()}
>
{getErrorMessage(error)}
</Text>
Cancel
</Button>
)}
</form>
<form
style={{ display: 'flex', flexDirection: 'row', marginTop: 30 }}
onSubmit={e => {
e.preventDefault();
onSubmit();
}}
>
<Input
autoFocus={true}
placeholder={'https://example.com'}
value={url}
onChange={e => setUrl(e.target.value)}
style={{ flex: 1, marginRight: 10 }}
/>
<ButtonWithLoading primary loading={loading} style={{ fontSize: 15 }}>
OK
</ButtonWithLoading>
{currentUrl && (
<View
style={{
flexDirection: 'row',
flexFlow: 'row wrap',
justifyContent: 'center',
marginTop: 15,
}}
>
{currentUrl ? (
<Button bare style={{ color: colors.n4 }} onClick={onSkip}>
Stop using a server
</Button>
) : (
<>
<Button
bare
type="button"
style={{ fontSize: 15, marginLeft: 10 }}
onClick={() => history.goBack()}
style={{
color: colors.n4,
margin: 5,
marginRight: 15,
}}
onClick={onSameDomain}
>
Cancel
Use {window.location.origin.replace(/https?:\/\//, '')}
</Button>
)}
</form>
<View
style={{
flexDirection: 'row',
flexFlow: 'row wrap',
justifyContent: 'center',
marginTop: 15,
}}
>
{currentUrl ? (
<Button bare style={{ color: colors.n4 }} onClick={onSkip}>
Stop using a server
<Button
bare
style={{ color: colors.n4, margin: 5 }}
onClick={onSkip}
>
Dont use a server
</Button>
) : (
<>
<Button
bare
style={{
color: colors.n4,
margin: 5,
marginRight: 15,
}}
onClick={onSameDomain}
>
Use {window.location.origin.replace(/https?:\/\//, '')}
</Button>
<Button
bare
style={{ color: colors.n4, margin: 5 }}
onClick={onSkip}
>
Dont use a server
</Button>
{isNonProductionEnvironment() && (
<Button
primary
style={{ marginLeft: 15 }}
onClick={onCreateTestFile}
>
Create test file
</Button>
)}
</>
)}
</View>
{isNonProductionEnvironment() && (
<Button
primary
style={{ marginLeft: 15 }}
onClick={onCreateTestFile}
>
Create test file
</Button>
)}
</>
)}
</View>
</>
</View>
);
}

View File

@@ -50,55 +50,53 @@ export default function Bootstrap() {
}
return (
<>
<View style={{ maxWidth: 450, marginTop: -30 }}>
<Title text="Welcome to Actual!" />
<P style={{ fontSize: 16, color: colors.n2 }}>
Actual is a super fast privacy-focused app for managing your finances.
To secure your data, youll need to set a password for your server.
</P>
<View style={{ maxWidth: 450, marginTop: -30 }}>
<Title text="Welcome to Actual!" />
<P style={{ fontSize: 16, color: colors.n2 }}>
Actual is a super fast privacy-focused app for managing your finances.
To secure your data, youll need to set a password for your server.
</P>
<P isLast style={{ fontSize: 16, color: colors.n2 }}>
Consider opening{' '}
<a
href="https://actualbudget.github.io/docs/Getting-Started/using-actual/"
target="_blank"
rel="noopener noreferrer"
style={{ color: colors.b4 }}
<P isLast style={{ fontSize: 16, color: colors.n2 }}>
Consider opening{' '}
<a
href="https://actualbudget.github.io/docs/Getting-Started/using-actual/"
target="_blank"
rel="noopener noreferrer"
style={{ color: colors.b4 }}
>
our tour
</a>{' '}
in a new tab for some guidance on what to do when youve set your
password.
</P>
{error && (
<Text
style={{
marginTop: 20,
color: colors.r4,
borderRadius: 4,
fontSize: 15,
}}
>
{getErrorMessage(error)}
</Text>
)}
<ConfirmPasswordForm
buttons={
<Button
bare
style={{ fontSize: 15, color: colors.b4, marginRight: 15 }}
onClick={onDemo}
>
our tour
</a>{' '}
in a new tab for some guidance on what to do when youve set your
password.
</P>
{error && (
<Text
style={{
marginTop: 20,
color: colors.r4,
borderRadius: 4,
fontSize: 15,
}}
>
{getErrorMessage(error)}
</Text>
)}
<ConfirmPasswordForm
buttons={
<Button
bare
style={{ fontSize: 15, color: colors.b4, marginRight: 15 }}
onClick={onDemo}
>
Try Demo
</Button>
}
onSetPassword={onSetPassword}
onError={setError}
/>
</View>
</>
Try Demo
</Button>
}
onSetPassword={onSetPassword}
onError={setError}
/>
</View>
);
}

View File

@@ -41,61 +41,59 @@ export default function ChangePassword() {
}
return (
<>
<View style={{ maxWidth: 500, marginTop: -30 }}>
<Title text="Change server password" />
<View style={{ maxWidth: 500, marginTop: -30 }}>
<Title text="Change server password" />
<Text
style={{
fontSize: 16,
color: colors.n2,
lineHeight: 1.4,
}}
>
This will change the password for this server instance. All existing
sessions will stay logged in.
</Text>
{error && (
<Text
style={{
fontSize: 16,
color: colors.n2,
lineHeight: 1.4,
marginTop: 20,
color: colors.r4,
borderRadius: 4,
fontSize: 15,
}}
>
This will change the password for this server instance. All existing
sessions will stay logged in.
{getErrorMessage(error)}
</Text>
)}
{error && (
<Text
style={{
marginTop: 20,
color: colors.r4,
borderRadius: 4,
fontSize: 15,
}}
{msg && (
<Text
style={{
marginTop: 20,
color: colors.g4,
borderRadius: 4,
fontSize: 15,
}}
>
{msg}
</Text>
)}
<ConfirmPasswordForm
buttons={
<Button
bare
type="button"
style={{ fontSize: 15, marginRight: 10 }}
onClick={() => history.push('/')}
>
{getErrorMessage(error)}
</Text>
)}
{msg && (
<Text
style={{
marginTop: 20,
color: colors.g4,
borderRadius: 4,
fontSize: 15,
}}
>
{msg}
</Text>
)}
<ConfirmPasswordForm
buttons={
<Button
bare
type="button"
style={{ fontSize: 15, marginRight: 10 }}
onClick={() => history.push('/')}
>
Cancel
</Button>
}
onSetPassword={onSetPassword}
onError={setError}
/>
</View>
</>
Cancel
</Button>
}
onSetPassword={onSetPassword}
onError={setError}
/>
</View>
);
}

View File

@@ -56,64 +56,62 @@ export default function Login() {
}
return (
<>
<View style={{ maxWidth: 450, marginTop: -30 }}>
<Title text="Sign in to this Actual instance" />
<View style={{ maxWidth: 450, marginTop: -30 }}>
<Title text="Sign in to this Actual instance" />
<Text
style={{
fontSize: 16,
color: colors.n2,
lineHeight: 1.4,
}}
>
If you lost your password, you likely still have access to your server
to manually reset it.
</Text>
{error && (
<Text
style={{
fontSize: 16,
color: colors.n2,
lineHeight: 1.4,
marginTop: 20,
color: colors.r4,
borderRadius: 4,
fontSize: 15,
}}
>
If you lost your password, you likely still have access to your server
to manually reset it.
{getErrorMessage(error)}
</Text>
)}
{error && (
<Text
style={{
marginTop: 20,
color: colors.r4,
borderRadius: 4,
fontSize: 15,
}}
>
{getErrorMessage(error)}
</Text>
)}
<form
style={{ display: 'flex', flexDirection: 'row', marginTop: 30 }}
onSubmit={onSubmit}
<form
style={{ display: 'flex', flexDirection: 'row', marginTop: 30 }}
onSubmit={onSubmit}
>
<Input
autoFocus={true}
placeholder="Password"
type="password"
onChange={e => setPassword(e.target.value)}
style={{ flex: 1, marginRight: 10 }}
/>
<ButtonWithLoading primary loading={loading} style={{ fontSize: 15 }}>
Sign in
</ButtonWithLoading>
</form>
<View
style={{
flexDirection: 'row',
justifyContent: 'center',
marginTop: 15,
}}
>
<Button
bare
style={{ fontSize: 15, color: colors.b4, marginLeft: 10 }}
onClick={onDemo}
>
<Input
autoFocus={true}
placeholder="Password"
type="password"
onChange={e => setPassword(e.target.value)}
style={{ flex: 1, marginRight: 10 }}
/>
<ButtonWithLoading primary loading={loading} style={{ fontSize: 15 }}>
Sign in
</ButtonWithLoading>
</form>
<View
style={{
flexDirection: 'row',
justifyContent: 'center',
marginTop: 15,
}}
>
<Button
bare
style={{ fontSize: 15, color: colors.b4, marginLeft: 10 }}
onClick={onDemo}
>
Try Demo &rarr;
</Button>
</View>
Try Demo &rarr;
</Button>
</View>
</>
</View>
);
}

View File

@@ -162,15 +162,13 @@ function SchedulePreview({ previewDates }) {
}
return (
<>
<Stack
direction="column"
spacing={1}
style={{ marginTop: 15, color: colors.n4 }}
>
{content}
</Stack>
</>
<Stack
direction="column"
spacing={1}
style={{ marginTop: 15, color: colors.n4 }}
>
{content}
</Stack>
);
}

View File

@@ -1,7 +1,6 @@
import React, { useState, useMemo, useCallback, useEffect } from 'react';
import { useLocation } from 'react-router';
import { useViewportSize } from '@react-aria/utils';
import { css } from 'glamor';
import * as Platform from 'loot-core/src/client/platform';
@@ -10,6 +9,7 @@ import Add from '../icons/v1/Add';
import CheveronDown from '../icons/v1/CheveronDown';
import CheveronRight from '../icons/v1/CheveronRight';
import Cog from '../icons/v1/Cog';
import Pin from '../icons/v1/Pin';
import Reports from '../icons/v1/Reports';
import StoreFrontIcon from '../icons/v1/StoreFront';
import TuningIcon from '../icons/v1/Tuning';
@@ -17,7 +17,6 @@ import Wallet from '../icons/v1/Wallet';
import ArrowButtonLeft1 from '../icons/v2/ArrowButtonLeft1';
import CalendarIcon from '../icons/v2/Calendar';
import { styles, colors } from '../style';
import { breakpoints } from '../tokens';
import {
View,
@@ -27,6 +26,7 @@ import {
ButtonLink,
Button,
} from './common';
import { useSidebar } from './FloatableSidebar';
import { useDraggable, useDroppable, DropHighlight } from './sort.js';
import CellValue from './spreadsheet/CellValue';
@@ -460,11 +460,25 @@ function Accounts({
);
}
function ToggleButton({ style, onFloat }) {
function ToggleButton({ style, isFloating, onFloat }) {
return (
<View className="float" style={[style, { flexShrink: 0 }]}>
<Button bare onClick={onFloat}>
<ArrowButtonLeft1 style={{ width: 13, height: 13, color: colors.n5 }} />
{isFloating ? (
<Pin
style={{
margin: -2,
width: 15,
height: 15,
color: colors.n5,
transform: 'rotate(45deg)',
}}
/>
) : (
<ArrowButtonLeft1
style={{ width: 13, height: 13, color: colors.n5 }}
/>
)}
</Button>
</View>
);
@@ -539,8 +553,7 @@ export function Sidebar({
}) {
let hasWindowButtons = !Platform.isBrowser && Platform.OS === 'mac';
let windowWidth = useViewportSize().width;
let sidebarAlwaysFloats = windowWidth < breakpoints.medium;
const sidebar = useSidebar();
return (
<View
@@ -550,9 +563,9 @@ export function Sidebar({
color: colors.n9,
backgroundColor: colors.n1,
'& .float': {
opacity: 0,
opacity: isFloating ? 1 : 0,
transition: 'opacity .25s, width .25s',
width: hasWindowButtons ? null : 0,
width: hasWindowButtons || isFloating ? null : 0,
},
'&:hover .float': {
opacity: 1,
@@ -562,7 +575,7 @@ export function Sidebar({
style,
]}
>
{hasWindowButtons && !sidebarAlwaysFloats && (
{hasWindowButtons && !sidebar.alwaysFloats && (
<ToggleButton
style={[
{
@@ -574,6 +587,7 @@ export function Sidebar({
paddingRight: 8,
},
]}
isFloating={isFloating}
onFloat={onFloat}
/>
)}
@@ -614,8 +628,8 @@ export function Sidebar({
<View style={{ flex: 1, flexDirection: 'row' }} />
{!hasWindowButtons && !sidebarAlwaysFloats && (
<ToggleButton onFloat={onFloat} />
{!hasWindowButtons && !sidebar.alwaysFloats && (
<ToggleButton isFloating={isFloating} onFloat={onFloat} />
)}
</View>

View File

@@ -0,0 +1,6 @@
---
category: Enhancements
authors: [j-f1]
---
Improve sidebar auto-floating behavior