Add Popover2 component
Summary: The current Popover component isn't compatible with the new (in progress) component library because it relies on overflowing container elements, which isn't allowed in the generic Layout component. So this is a new Popover element, which uses absolute positioning instead. It takes heavy inspiration from the Tooltip and TooltipProvider components which do a similar thing. Still to do: [x] Edge cases when popover would be near a window edge [x] Style it to look nice [x] Split the use case (RatingButton) changes into a separate diff [x] When the location of the popover container moves (the rating button in this case, e.g. if you resize the window), it doesn't currently cause the effect function in the popover, so it doesn't get moved when it should [x] Add a little pointer thingy like a speech bubble [x] Make sure it's perfectly positioned [ ] Rename it to Popover and delete the old one. Not done, since it's just a stopgap. Reviewed By: mweststrate Differential Revision: D22693105 fbshipit-source-id: bc141433914bc20da48f8ae96764a95f7cd74ce5
This commit is contained in:
committed by
Facebook GitHub Bot
parent
5a56ee65b8
commit
d7a6356fb6
158
desktop/app/src/ui/components/PopoverProvider.tsx
Normal file
158
desktop/app/src/ui/components/PopoverProvider.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useMemo,
|
||||
useState,
|
||||
ReactNode,
|
||||
useEffect,
|
||||
useRef,
|
||||
RefObject,
|
||||
} from 'react';
|
||||
import React from 'react';
|
||||
import {styled, colors} from 'flipper';
|
||||
import {useWindowSize} from '../../utils/useWindowSize';
|
||||
|
||||
type PopoverManager = {
|
||||
open(
|
||||
id: string,
|
||||
targetRef: RefObject<HTMLElement | null>,
|
||||
content: ReactNode,
|
||||
): void;
|
||||
close(id: string): void;
|
||||
};
|
||||
type Popover = {
|
||||
id: string;
|
||||
targetRef: RefObject<HTMLElement | null>;
|
||||
content: ReactNode;
|
||||
};
|
||||
|
||||
const Anchor = styled.img((props: {top: number; left: number}) => ({
|
||||
zIndex: 9999999,
|
||||
position: 'absolute',
|
||||
top: props.top,
|
||||
left: props.left,
|
||||
}));
|
||||
Anchor.displayName = 'Popover.Anchor';
|
||||
const ANCHOR_WIDTH = 34;
|
||||
|
||||
const PopoverContainer = styled('div')(
|
||||
(props: {left: number; top: number; hidden: boolean}) => ({
|
||||
position: 'absolute',
|
||||
top: props.top,
|
||||
left: props.left,
|
||||
zIndex: 9999998,
|
||||
backgroundColor: colors.white,
|
||||
borderRadius: 7,
|
||||
border: '1px solid rgba(0,0,0,0.3)',
|
||||
boxShadow: '0 2px 10px 0 rgba(0,0,0,0.3)',
|
||||
display: props.hidden ? 'none' : 'visible',
|
||||
}),
|
||||
);
|
||||
PopoverContainer.displayName = 'Popover.PopoverContainer';
|
||||
|
||||
const PopoverElement = (props: {
|
||||
targetRef: RefObject<HTMLElement | null>;
|
||||
children: ReactNode;
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const [dimensions, setDimensions] = useState<{
|
||||
width: number;
|
||||
height: number;
|
||||
} | null>(null);
|
||||
useEffect(() => {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
dimensions?.width !== ref.current.clientWidth ||
|
||||
dimensions?.height !== ref.current.clientHeight
|
||||
) {
|
||||
setDimensions({
|
||||
width: ref.current?.clientWidth,
|
||||
height: ref.current?.clientHeight,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const windowSize = useWindowSize();
|
||||
if (
|
||||
windowSize.height == null ||
|
||||
windowSize.width == null ||
|
||||
props.targetRef.current?.getBoundingClientRect() == null
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// target is the point that the anchor points to.
|
||||
// It is defined as the center of the bottom edge of the target element.
|
||||
const targetXCoord =
|
||||
props.targetRef.current?.getBoundingClientRect().left +
|
||||
props.targetRef.current?.getBoundingClientRect().width / 2;
|
||||
const targetYCoord = props.targetRef.current?.getBoundingClientRect().bottom;
|
||||
return (
|
||||
<>
|
||||
<Anchor
|
||||
top={targetYCoord}
|
||||
left={targetXCoord - ANCHOR_WIDTH / 2}
|
||||
src="./anchor.svg"
|
||||
key="anchor"
|
||||
/>
|
||||
<PopoverContainer
|
||||
ref={ref}
|
||||
hidden={ref.current === null}
|
||||
top={
|
||||
Math.min(
|
||||
targetYCoord,
|
||||
windowSize.height - (dimensions?.height ?? 0),
|
||||
) + 13
|
||||
}
|
||||
left={Math.min(
|
||||
targetXCoord - (dimensions?.width ?? 0) / 2,
|
||||
windowSize.width - (dimensions?.width ?? 0),
|
||||
)}>
|
||||
{props.children}
|
||||
</PopoverContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const PopoverContext = createContext<PopoverManager>(undefined as any);
|
||||
|
||||
export function PopoverProvider({children}: {children: React.ReactNode}) {
|
||||
const [popovers, setPopovers] = useState<Popover[]>([]);
|
||||
const popoverManager = useMemo(
|
||||
() => ({
|
||||
open: (
|
||||
id: string,
|
||||
targetRef: RefObject<HTMLElement | null>,
|
||||
content: ReactNode,
|
||||
) => {
|
||||
setPopovers((s) => [...s, {id, targetRef: targetRef, content}]);
|
||||
},
|
||||
close: (id: string) => {
|
||||
setPopovers((s) => s.filter((p) => p.id !== id));
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{popovers.map((p, index) => (
|
||||
<PopoverElement key={index} targetRef={p.targetRef}>
|
||||
{p.content}
|
||||
</PopoverElement>
|
||||
))}
|
||||
<PopoverContext.Provider value={popoverManager}>
|
||||
{children}
|
||||
</PopoverContext.Provider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user