Skip to content

Commit

Permalink
Add new floating panels component (#346)
Browse files Browse the repository at this point in the history
* Add new floating panels component

* Added dynamic max height calculations

* fixed positioning

* Fixed max-height scroll issues

* removed unneeded code complexity

* updated component tests

* 3.2.48
  • Loading branch information
abottega authored Jan 17, 2025
1 parent 898df27 commit ecae70c
Show file tree
Hide file tree
Showing 9 changed files with 348 additions and 3 deletions.
1 change: 1 addition & 0 deletions lib/components.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ test("all components exported", () => {
"Expandable",
"Flex",
"FlexItem",
"FloatingPanels",
"GlobalStyles",
"Grid",
"GridItem",
Expand Down
58 changes: 58 additions & 0 deletions lib/components/FloatingPanels/FloatingPanels.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Story, Canvas, Controls, Meta } from "@storybook/addon-docs";
import FloatingPanels from ".";
import * as stories from "./FloatingPanels.stories";

<Meta of={stories} />

# Floating Panels

The Floating Panels Component is a flexible React component that creates a set of collapsible panels floating on the screen. It's ideal for displaying information or controls that need to be easily accessible but not always visible.

## Features

- Customisable container height
- Customisable position on the screen
- Expandable/collapsible panels
- Configurable icons and titles for each panel
- can default specific panels to start expanded on load
- Ability to include any content within panels (can pass content as a component)

## Usage

```js run
const panels = [
{
id: "legend",
iconName: "map",
title: "Legend",
defaultExpanded: true,
content: <div>Legend content goes here</div>
},
{
id: "view-options",
iconName: "eye",
title: "View Options",
defaultExpanded: true,
content: <div>View options content goes here</div>
},
{
id: "person-details",
iconName: "user",
title: "Person Details",
content: <div>Person details content goes here</div>
},
{
id: "properties",
iconName: "gear",
title: "Properties",
content: <div>Properties content goes here</div>
}
// Add more panels as needed...
];
```

<Canvas of={stories.defaultFloatingPanels} />

## Properties

<Controls component={FloatingPanels} />
79 changes: 79 additions & 0 deletions lib/components/FloatingPanels/FloatingPanels.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React from "react";
import FloatingPanels from ".";
import Box from "../Box";
import { far } from "@fortawesome/free-regular-svg-icons";
import { library } from "@fortawesome/fontawesome-svg-core";
import TextInput from "../TextInput";
import Toggle from "../Toggle";
import Spacer from "../Spacer";
import Badge from "../Badge";
import { P } from "../Typography";

library.add(far);

export default {
title: "Components/FloatingPanels",
decorators: [(storyFn) => <Box minHeight="600px">{storyFn()}</Box>],
component: FloatingPanels
};
const Properties = () => {
return (
<>
<Spacer mb="r">
<Badge variant="secondary">Blah</Badge>
<P>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
culpa qui officia deserunt mollit anim id est laborum.
</P>
<Toggle id="toggle1" label="Group items" small />
<Toggle id="toggle2" label="Show teams" small />
</Spacer>
<TextInput
id="textInput1"
key="textInput1"
type="text"
label="Full name"
placeholder="E.g. John Smith"
my="20px"
/>
</>
);
};
const panels = [
{
id: "view-options",
iconName: "eye",
title: "View Options",
defaultExpanded: true,
content: <Properties />
},
{
id: "properties",
iconName: "sun",
title: "Properties",
content: <Properties />
},
{
id: "person-details",
iconName: "user",
title: "Person Details",
content: <Properties />
}
];

export const defaultFloatingPanels = () => {
return (
<FloatingPanels
panels={panels}
containerHeight={500}
position={{ right: 20, top: 20 }}
/>
);
};

defaultFloatingPanels.storyName = "Default Floating Panels";
100 changes: 100 additions & 0 deletions lib/components/FloatingPanels/FloatingPanels.styles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import styled from "styled-components";
import { themeGet } from "@styled-system/theme-get";

export const Container = styled.div`
z-index: 2;
position: absolute;
display: flex;
flex-direction: column;
gap: 8px;
width: 300px;
max-height: ${({ containerHeight }) =>
containerHeight ? `${containerHeight}px` : "100%"};
${({ position }) =>
Object.entries(position)
.filter(([, value]) => value !== undefined)
.map(
([key, value]) =>
`${key}: ${typeof value === "number" ? `${value}px` : value};`
)
.join("\n")}
`;

export const PanelWrapper = styled.div`
background: white;
border: ${({ isExpanded, theme }) =>
isExpanded ? `1px solid ${theme.colors.greyLighter}` : "1px solid white"};
border-radius: 8px;
border-radius: 0 0 8px 8px;
box-shadow: ${({ isExpanded }) =>
isExpanded ? "0 1px 3px rgba(0, 0, 0, 0.1)" : "none"};
overflow-y: ${({ isExpanded }) => (isExpanded ? "auto" : "hidden")};
// min-height: 48px;
padding: ${({ isExpanded }) => (isExpanded ? "0 12px 12px 12px" : "0 12px")};
margin-top: 46px;
max-height: ${({ isExpanded }) => (isExpanded ? "none" : "0")};
transition: max-height 0.3s ease-in-out;
`;

export const PanelHeader = styled.button`
font-family: ${themeGet("fonts.main")};
box-shadow: 0 0 4px rgba(0, 0, 0, 0.1);
color: ${themeGet("colors.greyDarkest")};
width: 100%;
margin-left: -13px;
margin-top: -46px;
border-radius: ${({ isExpanded }) => (isExpanded ? "8px 8px 0 0" : "8px")};
appearance: none;
background-color: white;
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 12px 12px 12px;
height: 46px;
position: fixed;
width: 300px;
border-bottom: ${({ isExpanded, theme }) =>
isExpanded ? `1px solid ${theme.colors.greyLighter}` : "none"};
border: solid 1px ${themeGet("colors.greyLighter")};
z-index: 1;
cursor: pointer;
user-select: none;
transition: padding 0.3s ease-in-out;
&:focus {
outline: none;
}
`;

export const HeaderContent = styled.div`
display: flex;
align-items: center;
gap: 12px;
`;

export const Title = styled.span`
font-size: 14px;
font-weight: 500;
`;

export const IconWrapper = styled.div`
background-color: ${themeGet("colors.primary")};
width: 22px;
height: 22px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
`;

export const ChevronWrapper = styled(IconWrapper)`
transition: background-color 0.3s ease-in-out;
background-color: ${({ isHovered }) =>
isHovered ? themeGet("colors.greyLighter") : "white"};
`;

export const PanelContent = styled.div`
padding-top: 12px;
display: ${({ isExpanded }) => (isExpanded ? "block" : "none")};
height: ${({ isExpanded }) => (isExpanded ? "100%" : "0")};
opacity: ${({ isExpanded }) => (isExpanded ? "1" : "0")};
`;
63 changes: 63 additions & 0 deletions lib/components/FloatingPanels/Panel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React, { useState } from "react";
import PropTypes from "prop-types";

import Icon from "../Icon";
import {
PanelWrapper,
PanelHeader,
HeaderContent,
Title,
IconWrapper,
ChevronWrapper,
PanelContent
} from "./FloatingPanels.styles";

const Panel = ({
iconName,
title,
containerHeight,
content,
defaultExpanded = false
}) => {
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
const arrowIcon = isExpanded ? "chevron-up" : "chevron-down";
const [isHovered, setIsHovered] = useState(false);
return (
<PanelWrapper containerHeight={containerHeight} isExpanded={isExpanded}>
<PanelHeader
onClick={() => setIsExpanded(!isExpanded)}
isExpanded={isExpanded}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onFocus={() => setIsHovered(true)}
onBlur={() => setIsHovered(false)}
>
<HeaderContent>
<IconWrapper>
<Icon size="xs" color="white" icon={["far", iconName]} />
</IconWrapper>
<Title>{title}</Title>
</HeaderContent>
<ChevronWrapper isHovered={isHovered}>
<Icon size="sm" icon={["fas", arrowIcon]} color="greyDarker" />
</ChevronWrapper>
</PanelHeader>
<PanelContent isExpanded={isExpanded}>{content}</PanelContent>
</PanelWrapper>
);
};

Panel.propTypes = {
iconName: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
content: PropTypes.node.isRequired,
defaultExpanded: PropTypes.bool,
containerHeight: PropTypes.number,
isExpanded: PropTypes.bool.isRequired
};

Panel.defaultProps = {
defaultExpanded: false
};

export default Panel;
43 changes: 43 additions & 0 deletions lib/components/FloatingPanels/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from "react";
import { Container } from "./FloatingPanels.styles";
import Panel from "./Panel";
import PropTypes from "prop-types";

const FloatingPanels = ({
panels,
containerHeight,
position = { right: 20, top: 20 }
}) => {
return (
<Container containerHeight={containerHeight} position={position}>
{panels.map((panel) => (
<Panel key={panel?.id} {...panel} containerHeight={containerHeight} />
))}
</Container>
);
};

FloatingPanels.propTypes = {
panels: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
iconName: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
content: PropTypes.node.isRequired,
defaultExpanded: PropTypes.bool
})
).isRequired,
containerHeight: PropTypes.number,
position: PropTypes.shape({
top: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
right: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
bottom: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
left: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
})
};

FloatingPanels.defaultProps = {
position: { right: 20, top: 20 }
};

export default FloatingPanels;
1 change: 1 addition & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export { default as Divider } from "./components/Divider";
export { default as Expandable } from "./components/Expandable";
export { default as Flex, FlexItem } from "./components/Flex";
export { default as Grid, GridItem } from "./components/Grid";
export { default as FloatingPanels } from "./components/FloatingPanels";
export { default as Header } from "./components/Header";
export { default as HeaderSimple } from "./components/HeaderSimple";
export { default as Icon } from "./components/Icon";
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "orcs-design-system",
"version": "3.2.47",
"version": "3.2.48",
"engines": {
"node": "20.12.2"
},
Expand Down

0 comments on commit ecae70c

Please sign in to comment.