From e5215bd97b1f2694570d858fd928e3d84c4e52cd Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 6 Aug 2020 13:00:54 +0200 Subject: [PATCH] Adds generic popover component to ui-component --- CHANGELOG.md | 1 + scm-ui/ui-components/src/index.ts | 1 + .../src/popover/Popover.stories.tsx | 73 ++++++++++ scm-ui/ui-components/src/popover/Popover.tsx | 129 +++++++++++++++++ scm-ui/ui-components/src/popover/index.ts | 26 ++++ .../ui-components/src/popover/usePopover.ts | 137 ++++++++++++++++++ 6 files changed, 367 insertions(+) create mode 100644 scm-ui/ui-components/src/popover/Popover.stories.tsx create mode 100644 scm-ui/ui-components/src/popover/Popover.tsx create mode 100644 scm-ui/ui-components/src/popover/index.ts create mode 100644 scm-ui/ui-components/src/popover/usePopover.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index bb751325a4..16e999d1e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Added +- Add generic popover component to ui-components - Show changeset signatures in ui and add public keys ([#1273](https://github.com/scm-manager/scm-manager/pull/1273)) ## [2.3.0] - 2020-07-23 diff --git a/scm-ui/ui-components/src/index.ts b/scm-ui/ui-components/src/index.ts index 8fb3115c65..3b1eb82271 100644 --- a/scm-ui/ui-components/src/index.ts +++ b/scm-ui/ui-components/src/index.ts @@ -96,6 +96,7 @@ export * from "./navigation"; export * from "./repos"; export * from "./table"; export * from "./toast"; +export * from "./popover"; export { File, diff --git a/scm-ui/ui-components/src/popover/Popover.stories.tsx b/scm-ui/ui-components/src/popover/Popover.stories.tsx new file mode 100644 index 0000000000..d7df89486a --- /dev/null +++ b/scm-ui/ui-components/src/popover/Popover.stories.tsx @@ -0,0 +1,73 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { storiesOf } from "@storybook/react"; +import React from "react"; +import styled from "styled-components"; +import usePopover from "./usePopover"; +import Popover from "./Popover"; + +const Wrapper = styled.div` + width: 100%; + margin: 20rem; +`; + +storiesOf("Popover", module) + .addDecorator(storyFn => {storyFn()}) + .add("Default", () => { + const { triggerProps, popoverProps } = usePopover(); + + return ( +
+ Spaceship Heart of Gold} width={512} {...popoverProps}> +

+ The Heart of Gold is the sleekest, most advanced, coolest spaceship in the galaxy. Its stunning good looks + mirror its awesome speed and power. It is powered by the revolutionary new Infinite Improbability Drive, + which lets the ship pass through every point in every universe simultaneously. +

+
+ +
+ ); + }) + .add("Link", () => { + const { triggerProps, popoverProps } = usePopover(); + + return ( +
+ Spaceship Heart of Gold} width={512} {...popoverProps}> +

+ The Heart of Gold is the sleekest, most advanced, coolest spaceship in the galaxy. Its stunning good looks + mirror its awesome speed and power. It is powered by the revolutionary new Infinite Improbability Drive, + which lets the ship pass through every point in every universe simultaneously. +

+
+ + Trigger + +
+ ); + }); diff --git a/scm-ui/ui-components/src/popover/Popover.tsx b/scm-ui/ui-components/src/popover/Popover.tsx new file mode 100644 index 0000000000..5f12cccd00 --- /dev/null +++ b/scm-ui/ui-components/src/popover/Popover.tsx @@ -0,0 +1,129 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React, { Dispatch, FC, ReactNode, useLayoutEffect, useRef, useState } from "react"; +import { Action } from "./usePopover"; +import styled from "styled-components"; + +type Props = { + title: ReactNode; + width?: number; + // props should be defined by usePopover + offsetTop?: number; + offsetLeft?: number; + show: boolean; + dispatch: Dispatch; +}; + +type ContainerProps = { + width: number; +}; + +const PopoverContainer = styled.div` + position: absolute; + z-index: 100; + width: ${props => props.width}px; + display: block; + + &:before { + position: absolute; + content: ""; + border-style: solid; + pointer-events: none; + height: 0; + width: 0; + top: 100%; + left: ${props => props.width / 2}px; + border-color: transparent; + border-bottom-color: white; + border-left-color: white; + border-width: 0.4rem; + margin-left: -0.4rem; + margin-top: -0.4rem; + -webkit-transform-origin: center; + transform-origin: center; + box-shadow: -1px 1px 2px rgba(10, 10, 10, 0.2); + transform: rotate(-45deg); + } +`; + +const SmallHr = styled.hr` + margin: 0.5em 0; +`; + +const PopoverHeading = styled.div` + height: 1.5em; +`; + +const Popover: FC = props => { + if (!props.show) { + return null; + } + return ; +}; + +const InnerPopover: FC = ({ title, show, width, offsetTop, offsetLeft, dispatch, children }) => { + const [height, setHeight] = useState(125); + const ref = useRef(null); + useLayoutEffect(() => { + if (ref.current) { + setHeight(ref.current.clientHeight); + } + }, [ref]); + + const onMouseEnter = () => { + dispatch({ + type: "enter-popover" + }); + }; + + const onMouseLeave = () => { + dispatch({ + type: "leave-popover" + }); + }; + + const top = (offsetTop || 0) - height - 5; + const left = (offsetLeft || 0) - width! / 2; + return ( + + {title} + + {children} + + ); +}; + +Popover.defaultProps = { + width: 120 +}; + +export default Popover; diff --git a/scm-ui/ui-components/src/popover/index.ts b/scm-ui/ui-components/src/popover/index.ts new file mode 100644 index 0000000000..d768334db1 --- /dev/null +++ b/scm-ui/ui-components/src/popover/index.ts @@ -0,0 +1,26 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +export { default as Popover } from "./Popover"; +export { default as usePopover } from "./usePopover"; diff --git a/scm-ui/ui-components/src/popover/usePopover.ts b/scm-ui/ui-components/src/popover/usePopover.ts new file mode 100644 index 0000000000..db73194916 --- /dev/null +++ b/scm-ui/ui-components/src/popover/usePopover.ts @@ -0,0 +1,137 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { Dispatch, useReducer, useRef } from "react"; + +type EnterTrigger = { + type: "enter-trigger"; + offsetTop: number; + offsetLeft: number; +}; + +type LeaveTrigger = { + type: "leave-trigger"; +}; + +type EnterPopover = { + type: "enter-popover"; +}; + +type LeavePopover = { + type: "leave-popover"; +}; + +export type Action = EnterTrigger | LeaveTrigger | EnterPopover | LeavePopover; + +type State = { + offsetTop?: number; + offsetLeft?: number; + onPopover: boolean; + onTrigger: boolean; +}; + +const initialState = { + onPopover: false, + onTrigger: false +}; + +const reducer = (state: State, action: Action): State => { + switch (action.type) { + case "enter-trigger": { + if (state.onPopover) { + return state; + } + return { + offsetTop: action.offsetTop, + offsetLeft: action.offsetLeft, + onTrigger: true, + onPopover: false + }; + } + case "leave-trigger": { + if (state.onPopover) { + return { + ...state, + onTrigger: false + }; + } + return initialState; + } + case "enter-popover": { + return { + ...state, + onPopover: true + }; + } + case "leave-popover": { + if (state.onTrigger) { + return { + ...state, + onPopover: false + }; + } + return initialState; + } + } +}; + +const dispatchDeferred = (dispatch: Dispatch, action: Action) => { + setTimeout(() => dispatch(action), 250); +}; + +const usePopover = () => { + const [state, dispatch] = useReducer(reducer, initialState); + const triggerRef = useRef(null); + + const onMouseOver = () => { + const current = triggerRef.current!; + dispatchDeferred(dispatch, { + type: "enter-trigger", + offsetTop: current.offsetTop, + offsetLeft: current.offsetLeft + current.offsetWidth / 2 + }); + }; + + const onMouseLeave = () => { + dispatchDeferred(dispatch, { + type: "leave-trigger" + }); + }; + + return { + triggerProps: { + onMouseOver, + onMouseLeave, + ref: (node: HTMLElement | null) => (triggerRef.current = node) + }, + popoverProps: { + dispatch, + show: state.onPopover || state.onTrigger, + offsetTop: state.offsetTop, + offsetLeft: state.offsetLeft + } + }; +}; + +export default usePopover;