Pin sidebar with a button
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 />
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -51,6 +51,10 @@ type PaletteAdditions = {
|
||||
default: string;
|
||||
};
|
||||
};
|
||||
pinSidebarButton: {
|
||||
icon: string;
|
||||
background: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type BackstagePalette = Palette & PaletteAdditions;
|
||||
|
||||
Reference in New Issue
Block a user