Skip to content

Commit 8bf431e

Browse files
committed
feat(*): add Popover as beta
1 parent 4e89d71 commit 8bf431e

File tree

22 files changed

+1708
-1
lines changed

22 files changed

+1708
-1
lines changed

packages/plasma-new-hope/package-lock.json

Lines changed: 545 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/plasma-new-hope/src/components/TextField/TextField.types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ export type TextFieldPropsBase = {
262262
export type TextFieldProps = {
263263
/**
264264
* Стиль для UI конфигурации
265-
* Влияет на выбор предустановленого набора токенов
265+
* Влияет на выбор предустановленного набора токенов
266266
* @default default
267267
*/
268268
appearance?: 'default' | 'clear';
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import * as React from 'react';
2+
import { mergeRefs, mergeHandlers } from 'src/utils';
3+
4+
export const Slot = React.forwardRef<any, any>((props, forwardedRef) => {
5+
const { children, ...slotProps } = props;
6+
7+
if (!React.isValidElement(children)) {
8+
return null;
9+
}
10+
11+
const child = children as React.ReactElement<any>;
12+
const childProps = child.props;
13+
14+
const mergedProps: Record<string, any> = { ...slotProps };
15+
16+
// merge event handlers
17+
for (const key in slotProps) {
18+
if (/^on[A-Z]/.test(key)) {
19+
mergedProps[key] = mergeHandlers(childProps[key], slotProps[key]);
20+
}
21+
}
22+
23+
if (childProps.className || slotProps.className) {
24+
mergedProps.className = [slotProps.className, childProps.className].filter(Boolean).join(' ');
25+
}
26+
27+
// merge style
28+
if (childProps.style || slotProps.style) {
29+
mergedProps.style = { ...slotProps.style, ...childProps.style };
30+
}
31+
32+
mergedProps.ref = mergeRefs(childProps.ref, forwardedRef);
33+
34+
return React.cloneElement(children, mergedProps);
35+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { styled } from '@linaria/react';
2+
import { css } from '@linaria/core';
3+
4+
import { tokens } from './Popover.tokens';
5+
6+
export const base = css`
7+
position: relative;
8+
background: var(${tokens.backgroundColor});
9+
padding: var(${tokens.padding});
10+
border-radius: var(${tokens.borderRadius});
11+
box-shadow: var(${tokens.boxShadow});
12+
width: 100%;
13+
height: 100%;
14+
box-sizing: border-box;
15+
`;
16+
17+
export const CloseButton = styled.button`
18+
position: absolute;
19+
top: var(${tokens.iconOffset});
20+
right: var(${tokens.iconOffset});
21+
padding: 0;
22+
margin: 0;
23+
border: none;
24+
background: none;
25+
cursor: pointer;
26+
line-height: 0;
27+
color: var(${tokens.iconColor});
28+
29+
&:hover {
30+
color: var(${tokens.iconColorHover});
31+
}
32+
33+
&:active {
34+
color: var(${tokens.iconColorActive});
35+
}
36+
`;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export const classes = {
2+
popoverRoot: 'popover-root',
3+
popoverCloseIconButton: 'popover-close-icon-button',
4+
};
5+
6+
export const tokens = {
7+
backgroundColor: '--plasma-popover-background-color',
8+
borderRadius: '--plasma-popover-border-radius',
9+
padding: '--plasma-popover-padding',
10+
boxShadow: '--plasma-popover-box-shadow',
11+
iconColor: '--plasma-popover-icon-color',
12+
iconColorHover: '--plasma-popover-icon-color-hover',
13+
iconColorActive: '--plasma-popover-icon-color-active',
14+
iconOffset: '--plasma-popover-icon-offset',
15+
};
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import React, { forwardRef, useState, useRef } from 'react';
2+
import type { RootProps } from 'src/engines/types';
3+
import cls from 'classnames';
4+
import {
5+
useFloating,
6+
useInteractions,
7+
useClick,
8+
useDismiss,
9+
useRole,
10+
FloatingFocusManager,
11+
FloatingArrow,
12+
arrow,
13+
offset,
14+
useHover,
15+
safePolygon,
16+
shift,
17+
flip,
18+
FloatingPortal,
19+
autoUpdate,
20+
} from '@floating-ui/react';
21+
import { css } from '@linaria/core';
22+
23+
import { IconClose } from '../../_Icon';
24+
import { Resizable } from '../../_Resizable';
25+
import { Slot } from '../../_Slot/Slot';
26+
27+
import { sizeToIconSize, matchPlacements } from './utils';
28+
import { tokens, classes } from './Popover.tokens';
29+
import { base, CloseButton } from './Popover.styles';
30+
import type { PopoverProps } from './Popover.types';
31+
32+
/* Ширина хвостика */
33+
const ARROW_WIDTH = 20;
34+
/* Высота хвостика */
35+
const ARROW_HEIGHT = 8;
36+
/* SVG хвостика */
37+
const ARROW_POLYGON = 'M20 20L0 20C8.88889 20.0001 10 12.5714 10 12C10 12.5714 11.3273 20.006 20 20Z';
38+
/* Отступ хвостика по краям (чтобы избежать коллизии со скругленными углами) */
39+
const ARROW_PADDING = 16;
40+
41+
export const popoverRoot = (Root: RootProps<HTMLDivElement, PopoverProps>) =>
42+
forwardRef<HTMLDivElement, PopoverProps>(
43+
(
44+
{
45+
appearance = 'closeNone',
46+
children,
47+
target,
48+
opened: outerOpened,
49+
defaultOpened = false,
50+
trigger = 'click',
51+
placement = 'bottom',
52+
arrow: outerArrow = true,
53+
flip: outsideFlip = true,
54+
shift: outsideShift = true,
55+
offset: outerOffset = 0,
56+
outsideClick = true,
57+
resizable = false,
58+
onResizeStart,
59+
onResizeEnd,
60+
onToggle,
61+
delayOpen = 0,
62+
delayClose = 0,
63+
zIndex = 1000,
64+
className,
65+
size,
66+
...rest
67+
},
68+
outerRootRef,
69+
) => {
70+
const [innerOpened, setInnerOpened] = useState(defaultOpened);
71+
72+
const opened = outerOpened ?? innerOpened;
73+
74+
const arrowRef = useRef(null);
75+
76+
const handleToggle = (opened: boolean) => {
77+
setInnerOpened(opened);
78+
79+
if (onToggle) {
80+
onToggle(opened);
81+
}
82+
};
83+
84+
const { refs, floatingStyles, context } = useFloating({
85+
whileElementsMounted: autoUpdate,
86+
placement,
87+
open: opened,
88+
onOpenChange: handleToggle,
89+
middleware: [
90+
outsideShift && shift(),
91+
outsideFlip && flip(),
92+
outerArrow && arrow({ element: arrowRef, padding: ARROW_PADDING }),
93+
offset((outerArrow ? ARROW_HEIGHT : 0) + outerOffset),
94+
],
95+
});
96+
97+
const click = useClick(context, {
98+
enabled: trigger === 'click' || matchMedia('(hover: none)').matches,
99+
});
100+
const dismiss = useDismiss(context, {
101+
enabled: true,
102+
outsidePress: outsideClick,
103+
});
104+
const role = useRole(context);
105+
const hover = useHover(context, {
106+
mouseOnly: true,
107+
enabled: trigger === 'hover',
108+
handleClose: safePolygon({
109+
requireIntent: false,
110+
}),
111+
delay: {
112+
open: delayOpen,
113+
close: delayClose,
114+
},
115+
});
116+
117+
const { getReferenceProps, getFloatingProps } = useInteractions([dismiss, role, click, hover]);
118+
119+
return (
120+
<>
121+
<Slot ref={refs.setReference} {...getReferenceProps()}>
122+
{target}
123+
</Slot>
124+
125+
{opened && (
126+
<FloatingPortal>
127+
<FloatingFocusManager context={context}>
128+
<div
129+
ref={refs.setFloating}
130+
style={{ ...floatingStyles, zIndex }}
131+
{...getFloatingProps()}
132+
>
133+
<Resizable
134+
placement={matchPlacements(placement)}
135+
resizable={resizable}
136+
onResizeStart={onResizeStart}
137+
onResizeEnd={onResizeEnd}
138+
>
139+
<Root
140+
ref={outerRootRef}
141+
target={target}
142+
className={cls(className, classes.popoverRoot)}
143+
size={size}
144+
{...rest}
145+
data-popover-open={opened}
146+
>
147+
{outerArrow && (
148+
<FloatingArrow
149+
ref={arrowRef}
150+
context={context}
151+
width={ARROW_WIDTH}
152+
height={ARROW_HEIGHT}
153+
fill={`var(${tokens.backgroundColor})`}
154+
d={ARROW_POLYGON}
155+
/>
156+
)}
157+
158+
{children}
159+
160+
{appearance === 'closeInner' && (
161+
<CloseButton
162+
className={classes.popoverCloseIconButton}
163+
onClick={() => handleToggle(false)}
164+
>
165+
<IconClose size={sizeToIconSize(size)} color="inherit" />
166+
</CloseButton>
167+
)}
168+
</Root>
169+
</Resizable>
170+
</div>
171+
</FloatingFocusManager>
172+
</FloatingPortal>
173+
)}
174+
</>
175+
);
176+
},
177+
);
178+
179+
export const popoverConfig = {
180+
name: 'Popover',
181+
tag: 'div',
182+
layout: popoverRoot,
183+
base,
184+
variations: {
185+
view: {
186+
css: css``,
187+
},
188+
size: {
189+
css: css``,
190+
},
191+
},
192+
defaults: {
193+
view: 'default',
194+
size: 'm',
195+
},
196+
};

0 commit comments

Comments
 (0)