Pin sidebar with a button

This commit is contained in:
Wojciech Adaszynski
2020-05-19 16:30:50 +02:00
parent 986b230016
commit 2aacf677e9
10 changed files with 274 additions and 47 deletions
@@ -0,0 +1,25 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { LocalStorage } from './localStorage';
describe('local storage access object', () => {
it('should save and read sidebar pin state', () => {
LocalStorage.setSidebarPinState(false);
expect(LocalStorage.getSidebarPinState()).toBe(false);
LocalStorage.setSidebarPinState(true);
expect(LocalStorage.getSidebarPinState()).toBe(true);
});
});
+40
View File
@@ -0,0 +1,40 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
enum LocalStorageKeys {
SIDEBAR_PIN_STATE = 'sidebarPinState',
}
export const LocalStorage = {
getSidebarPinState(): boolean {
let value;
try {
value = JSON.parse(
window.localStorage.getItem(LocalStorageKeys.SIDEBAR_PIN_STATE) ||
'false',
);
} catch {
return false;
}
return !!value;
},
setSidebarPinState(state: boolean) {
return window.localStorage.setItem(
LocalStorageKeys.SIDEBAR_PIN_STATE,
JSON.stringify(state),
);
},
};
@@ -0,0 +1,28 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useContext } from 'react';
import { SidebarPinStateContext } from '../layout/Sidebar';
export function useSidebarPinState() {
const { isPinned, toggleSidebarPinState } = useContext(
SidebarPinStateContext,
);
return {
isPinned,
toggleSidebarPinState,
};
}
+15 -3
View File
@@ -19,8 +19,9 @@ import clsx from 'clsx';
import React, { FC, useRef, useState } from 'react';
import { sidebarConfig, SidebarContext } from './config';
import { BackstageTheme } from '@backstage/theme';
import { useSidebarPinState } from '../../hooks/useSidebarPinState';
const useStyles = makeStyles<BackstageTheme>((theme) => ({
const useStyles = makeStyles<BackstageTheme>(theme => ({
root: {
zIndex: 1000,
position: 'relative',
@@ -75,8 +76,12 @@ export const Sidebar: FC<Props> = ({
const classes = useStyles();
const [state, setState] = useState(State.Closed);
const hoverTimerRef = useRef<number>();
const { isPinned } = useSidebarPinState();
const handleOpen = () => {
if (isPinned) {
return;
}
if (hoverTimerRef.current) {
clearTimeout(hoverTimerRef.current);
hoverTimerRef.current = undefined;
@@ -92,6 +97,9 @@ export const Sidebar: FC<Props> = ({
};
const handleClose = () => {
if (isPinned) {
return;
}
if (hoverTimerRef.current) {
clearTimeout(hoverTimerRef.current);
hoverTimerRef.current = undefined;
@@ -115,11 +123,15 @@ export const Sidebar: FC<Props> = ({
onBlur={handleClose}
data-testid="sidebar-root"
>
<SidebarContext.Provider value={{ isOpen: state === State.Open }}>
<SidebarContext.Provider
value={{
isOpen: state === State.Open || isPinned,
}}
>
<div
className={clsx(classes.drawer, {
[classes.drawerPeek]: state === State.Peek,
[classes.drawerOpen]: state === State.Open,
[classes.drawerOpen]: state === State.Open || isPinned,
})}
>
{children}
@@ -0,0 +1,61 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { FC } from 'react';
import { makeStyles, Theme } from '@material-ui/core/styles';
import { sidebarConfig } from './config';
import { Avatar, Typography } from '@material-ui/core';
const useStyles = makeStyles<Theme>(() => {
const { drawerWidthOpen, userBadgeDiameter } = sidebarConfig;
return {
root: {
width: drawerWidthOpen,
display: 'flex',
alignItems: 'center',
color: '#b5b5b5',
paddingLeft: 18,
paddingTop: 14,
paddingBottom: 14,
},
avatar: {
width: userBadgeDiameter,
height: userBadgeDiameter,
marginRight: 8,
},
};
});
type Props = {
imageUrl: string;
name: string;
hideName?: boolean;
};
export const LoggedUserBadge: FC<Props> = ({
imageUrl,
name,
hideName = false,
}) => {
const classes = useStyles();
return (
<div className={classes.root}>
<Avatar alt={name} src={imageUrl} className={classes.avatar} />
{!hideName && <Typography variant="subtitle2">{name}</Typography>}
</div>
);
};
+41 -6
View File
@@ -15,18 +15,53 @@
*/
import { makeStyles } from '@material-ui/core';
import React, { FC } from 'react';
import React, { createContext, FC, useEffect, useState } from 'react';
import { sidebarConfig } from './config';
import { BackstageTheme } from '@backstage/theme';
import { LocalStorage } from '../../data/localStorage';
const useStyles = makeStyles({
const useStyles = makeStyles<BackstageTheme, { isPinned: boolean }>({
root: {
width: '100%',
minHeight: '100%',
paddingLeft: sidebarConfig.drawerWidthClosed,
transition: 'padding-left 0.1s ease-out',
paddingLeft: ({ isPinned }) =>
isPinned
? sidebarConfig.drawerWidthOpen
: sidebarConfig.drawerWidthClosed,
},
});
export const SidebarPage: FC<{}> = ({ children }) => {
const classes = useStyles();
return <div className={classes.root}>{children}</div>;
export type SidebarPinStateContextType = {
isPinned: boolean;
toggleSidebarPinState: () => any;
};
export const SidebarPinStateContext = createContext<SidebarPinStateContextType>(
{
isPinned: false,
toggleSidebarPinState: () => {},
},
);
export const SidebarPage: FC<{}> = (props) => {
const [isPinned, setIsPinned] = useState(LocalStorage.getSidebarPinState());
useEffect(() => {
LocalStorage.setSidebarPinState(isPinned);
}, [isPinned]);
const toggleSidebarPinState = () => setIsPinned(!isPinned);
const classes = useStyles({ isPinned });
return (
<SidebarPinStateContext.Provider
value={{
isPinned,
toggleSidebarPinState,
}}
>
<div className={classes.root}>{props.children}</div>
</SidebarPinStateContext.Provider>
);
};
+52 -37
View File
@@ -15,58 +15,73 @@
*/
import React, { FC, useContext } from 'react';
import { Avatar, makeStyles, Theme, Typography } from '@material-ui/core';
import { makeStyles } from '@material-ui/core';
import People from '@material-ui/icons/People';
import { sidebarConfig, SidebarContext } from './config';
import { SidebarContext } from './config';
import { SidebarItem } from './Items';
import { LoggedUserBadge } from './LoggedUserBadge';
import DoubleArrowIcon from '@material-ui/icons/DoubleArrow';
import { BackstageTheme } from '@backstage/theme';
import { SidebarPinStateContext } from './Page';
const useStyles = makeStyles<Theme>(() => {
const { drawerWidthOpen, userBadgeDiameter } = sidebarConfig;
const ARROW_BUTTON_SIZE = 20;
const useStyles = makeStyles<BackstageTheme, { isPinned: boolean }>(theme => {
return {
root: {
width: drawerWidthOpen,
position: 'relative',
},
arrowButtonWrapper: {
position: 'absolute',
right: 0,
width: ARROW_BUTTON_SIZE,
height: ARROW_BUTTON_SIZE,
top: `calc(50% - ${ARROW_BUTTON_SIZE / 2}px)`,
display: 'flex',
alignItems: 'center',
color: '#b5b5b5',
paddingLeft: 18,
paddingTop: 14,
paddingBottom: 14,
justifyContent: 'center',
borderRadius: '2px 0px 0px 2px',
background: theme.palette.pinSidebarButton.icon,
color: theme.palette.pinSidebarButton.background,
border: 'none',
outline: 'none',
cursor: 'pointer',
},
avatar: {
width: userBadgeDiameter,
height: userBadgeDiameter,
marginRight: 8,
arrowButtonIcon: {
transform: ({ isPinned }) => (isPinned ? 'rotate(180deg)' : 'none'),
},
};
});
type Props = {
imageUrl: string;
name: string;
hideName?: boolean;
};
export const UserBadge: FC<Props> = ({ imageUrl, name, hideName = false }) => {
const classes = useStyles();
export const SidebarUserBadge: FC<{}> = () => {
const { isOpen } = useContext(SidebarContext);
const { isPinned, toggleSidebarPinState } = useContext(
SidebarPinStateContext,
);
const classes = useStyles({ isPinned });
const isUserLoggedIn = false;
return (
<div className={classes.root}>
<Avatar alt={name} src={imageUrl} className={classes.avatar} />
{!hideName && <Typography variant="subtitle2">{name}</Typography>}
{isUserLoggedIn ? (
<LoggedUserBadge
imageUrl="https://via.placeholder.com/200/200"
name="Victor Viale"
hideName={!isOpen}
/>
) : (
<SidebarItem icon={People} text="Log in" to="/login" disableSelected />
)}
{isOpen && (
<button
className={classes.arrowButtonWrapper}
onClick={toggleSidebarPinState}
>
<DoubleArrowIcon
className={classes.arrowButtonIcon}
style={{ fontSize: 14 }}
/>
</button>
)}
</div>
);
};
export const SidebarUserBadge: FC = () => {
const { isOpen } = useContext(SidebarContext);
const isUserLoggedIn = false;
return isUserLoggedIn ? (
<UserBadge
imageUrl="https://via.placeholder.com/200/200"
name="Victor Viale"
hideName={!isOpen}
/>
) : (
<SidebarItem icon={People} text="Log in" to="/login" disableSelected />
);
};
-1
View File
@@ -110,7 +110,6 @@ class DevAppBuilder {
// Create a sidebar that exposes the touchpoints of a plugin
private setupSidebar(plugins: BackstagePlugin[]): JSX.Element {
const sidebarItems = new Array<JSX.Element>();
for (const plugin of plugins) {
for (const output of plugin.output()) {
switch (output.type) {
+8
View File
@@ -56,6 +56,10 @@ export const lightTheme = createTheme({
link: '#0A6EBE',
gold: yellow.A700,
sidebar: '#171717',
pinSidebarButton: {
icon: '#BDBDBD',
background: '#404040',
},
},
});
@@ -98,5 +102,9 @@ export const darkTheme = createTheme({
link: '#0A6EBE',
gold: yellow.A700,
sidebar: '#424242',
pinSidebarButton: {
icon: '#181818',
background: '#BDBDBD',
},
},
});
+4
View File
@@ -51,6 +51,10 @@ type PaletteAdditions = {
default: string;
};
};
pinSidebarButton: {
icon: string;
background: string;
};
};
export type BackstagePalette = Palette & PaletteAdditions;