Skip to content

Commit

Permalink
feat: popover
Browse files Browse the repository at this point in the history
  • Loading branch information
malangcat committed Jul 22, 2024
1 parent 61f053f commit 8e678ea
Show file tree
Hide file tree
Showing 50 changed files with 1,358 additions and 6 deletions.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1 change: 1 addition & 0 deletions examples/stackflow-spa/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"dependencies": {
"@radix-ui/react-slot": "^1.0.2",
"@seed-design/icon": "^0.6.1",
"@seed-design/react-popover": "0.0.0",
"@seed-design/react-radio-group": "0.0.0",
"@seed-design/react-switch": "0.0.0",
"@seed-design/react-text-field": "0.0.0",
Expand Down
81 changes: 81 additions & 0 deletions examples/stackflow-spa/src/activities/ActivityHelpBubble.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import "@seed-design/stylesheet/helpBubble.css";

import type { ActivityComponentType } from "@stackflow/react";

import { usePopover } from "@seed-design/react-popover";
import { AppScreen } from "@stackflow/plugin-basic-ui";
import { BoxButton } from "../design-system/components";
import { helpBubble } from "@seed-design/recipe/helpBubble";
import { forwardRef } from "react";

export interface HelpBubbleArrowProps extends React.ComponentPropsWithRef<"svg"> {
width: number;

height: number;

tipRadius: number;
}

const HelpBubbleArrow = forwardRef<SVGSVGElement, HelpBubbleArrowProps>((props, ref) => {
const { width, height, tipRadius, ...otherProps } = props;
const pathData = `M0,0
H${width}
L${width / 2 + tipRadius},${height - tipRadius}
Q${width / 2},${height} ${width / 2 - tipRadius},${height - tipRadius}
Z`;

return (
<svg
aria-hidden="true"
width={width}
height={width}
viewBox={`0 0 ${width} ${height > width ? height : width}`}
ref={ref}
{...otherProps}
>
<path stroke="none" d={pathData} />
</svg>
);
});

const ActivityHelpBubble: ActivityComponentType = () => {
const api = usePopover({
defaultOpen: true,
placement: "top",
gutter: 4,
overflowPadding: 16,
arrowPadding: 14,
});
const classNames = helpBubble();

const arrowRect = api.rects.arrow;

return (
<AppScreen appBar={{ title: "HelpBubble" }}>
<div style={{ overflowY: "auto", height: "200vh" }}>
<div style={{ display: "flex", paddingTop: "20vh", justifyContent: "center" }}>
<BoxButton ref={api.refs.trigger} {...api.triggerProps}>
Wow
</BoxButton>
</div>
{api.open && (
<div ref={api.refs.positioner} {...api.positionerProps} className={classNames.positioner}>
<div className={classNames.content}>
<div ref={api.refs.arrow} {...api.arrowProps} className={classNames.arrow}>
<HelpBubbleArrow
width={arrowRect?.width ?? 0}
height={arrowRect?.height ?? 0}
tipRadius={1}
/>
</div>
<span className={classNames.title}>Title</span>
<span className={classNames.description}>Description</span>
</div>
</div>
)}
</div>
</AppScreen>
);
};

export default ActivityHelpBubble;
2 changes: 1 addition & 1 deletion examples/stackflow-spa/src/activities/ActivityHome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ const ActivityHome: ActivityComponentType = () => {
<BoxButton onClick={() => push("ActivityChip", {})}>ChipButton</BoxButton>
<BoxButton onClick={() => push("ActivityAlertDialog", {})}>Dialog</BoxButton>
<BoxButton onClick={() => push("ActivityCallout", {})}>Callout</BoxButton>
<BoxButton onClick={() => push("ActivityCallout", {})}>Callout</BoxButton>
<BoxButton onClick={() => push("ActivitySwitch", {})}>Switch</BoxButton>
<BoxButton onClick={() => push("ActivityTextField", {})}>TextField</BoxButton>
<BoxButton onClick={() => push("ActivityHelpBubble", {})}>HelpBubble</BoxButton>
</div>
</AppScreen>
);
Expand Down
2 changes: 2 additions & 0 deletions examples/stackflow-spa/src/stackflow/Stack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const { Stack, useFlow, useStepFlow } = stackflow({
ActivityChip: React.lazy(() => import("../activities/ActivityChip")),
ActivityCallout: React.lazy(() => import("../activities/ActivityCallout")),
ActivitySwitch: React.lazy(() => import("../activities/ActivitySwitch")),
ActivityHelpBubble: React.lazy(() => import("../activities/ActivityHelpBubble")),
ActivityNotFound,
},
plugins: [
Expand Down Expand Up @@ -59,6 +60,7 @@ const { Stack, useFlow, useStepFlow } = stackflow({
ActivitySwitch: "/switch",
ActivityNotFound: "/404",
ActivityTextField: "/text-field",
ActivityHelpBubble: "/help-bubble",
},
}),
],
Expand Down
1 change: 0 additions & 1 deletion examples/stackflow-spa/vite.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";
import legacy from "@vitejs/plugin-legacy";
import react from "@vitejs/plugin-react-swc";
import { defineConfig } from "vite";
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"docs"
],
"scripts": {
"build": "ultra -r build",
"build": "ultra -r --filter \"packages/*\" build",
"build:dts": "ultra -r --build build:dts",
"build:style": "ultra -r --build build:style",
"build-only-package": "ultra -r --filter 'packages/*' build",
Expand Down Expand Up @@ -42,5 +42,5 @@
"ultra-runner": "^3.10.5",
"vitest": "^1.6.0"
},
"packageManager": "yarn@3.2.2"
"packageManager": "yarn@4.3.1"
}
36 changes: 36 additions & 0 deletions packages/component-spec/artifacts/help-bubble.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
base:
enabled:
root:
cornerRadius: $radii[1.5]
paddingX: $unit[3]
paddingY: $unit[2]
arrow:
size: $unit[2.5]
title:
fontSize: $font-size[75]
fontWeight: $font-weight.bold
description:
fontSize: $font-size[75]
fontWeight: $font-weight.regular
variant=non-modal:
enabled:
root:
color: $color.bg.neutral-inverted
arrow:
color: $color.bg.neutral-inverted
title:
color: $color.fg.neutral-inverted
description:
color: $color.fg.neutral-inverted
variant=modal:
enabled:
root:
color: $color.bg.static-white
arrow:
color: $color.bg.static-white
title:
color: $color.fg.static-black
description:
color: $color.fg.static-black
backdrop:
color: $color.bg.overlay
1 change: 1 addition & 0 deletions packages/react-headless/popover/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/lib/
8 changes: 8 additions & 0 deletions packages/react-headless/popover/.tshy/build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"rootDir": "../src",
"module": "nodenext",
"moduleResolution": "nodenext"
}
}
16 changes: 16 additions & 0 deletions packages/react-headless/popover/.tshy/commonjs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"extends": "./build.json",
"include": [
"../src/**/*.ts",
"../src/**/*.cts",
"../src/**/*.tsx",
"../src/**/*.json"
],
"exclude": [
"../src/**/*.mts",
"../src/package.json"
],
"compilerOptions": {
"outDir": "../.tshy-build/commonjs"
}
}
15 changes: 15 additions & 0 deletions packages/react-headless/popover/.tshy/esm.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"extends": "./build.json",
"include": [
"../src/**/*.ts",
"../src/**/*.mts",
"../src/**/*.tsx",
"../src/**/*.json"
],
"exclude": [
"../src/package.json"
],
"compilerOptions": {
"outDir": "../.tshy-build/esm"
}
}
175 changes: 175 additions & 0 deletions packages/react-headless/popover/dist/commonjs/floating.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import {
arrow,
autoUpdate,
flip,
limitShift,
offset,
shift,
useFloating,
type ElementRects,
type Middleware,
type Placement,
type Rect,
} from "@floating-ui/react";
import { useMemo, useState } from "react";

export interface PositioningOptions {
/**
* The strategy to use for positioning
* @default "absolute"
*/
strategy?: "absolute" | "fixed";
/**
* The initial placement of the floating element
* @default "bottom"
*/
placement?: Placement;
/**
* The gutter between the floating element and the reference element
*/
gutter?: number;
/**
* Whether to flip the placement
* @default true
*/
flip?: boolean | Placement[];
/**
* Whether the popover should slide when it overflows.
* @default true
*/
slide?: boolean;
/**
* The virtual padding around the viewport edges to check for overflow
* @default 8
*/
overflowPadding?: number;
/**
* The minimum padding between the arrow and the floating element's corner.
* @default 4
*/
arrowPadding?: number;
}

const defaultPositioningOptions: PositioningOptions = {
strategy: "absolute",
placement: "bottom",
flip: true,
slide: true,
overflowPadding: 8,
arrowPadding: 4,
};

function getArrowRect(arrowElement: HTMLElement | null): Rect | null {
return arrowElement?.getClientRects().item(0) ?? null;
}

function getArrowMiddleware(arrowElement: HTMLElement | null, opts: PositioningOptions) {
if (!arrowElement) return;
return arrow({ element: arrowElement, padding: opts.arrowPadding });
}

function getOffsetMiddleware(arrowOffset: number, opts: PositioningOptions) {
const offsetMainAxis = (opts.gutter ?? 0) + arrowOffset;
return offset(offsetMainAxis);
}

function getFlipMiddleware(opts: PositioningOptions) {
if (!opts.flip) return;
return flip({
padding: opts.overflowPadding,
fallbackPlacements: opts.flip === true ? undefined : opts.flip,
});
}

function getShiftMiddleware(opts: PositioningOptions) {
if (!opts.slide) return;
return shift({
mainAxis: opts.slide,
padding: opts.overflowPadding,
limiter: limitShift(),
});
}

const rectMiddleware: Middleware = {
name: "rects",
fn({ rects }) {
return {
data: rects,
};
},
};

export interface UsePositionedFloatingProps extends PositioningOptions {
/**
* Whether the floating element is initially open
*/
defaultOpen?: boolean;
/**
* Whether the floating element is open
*/
open?: boolean;
/**
* Callback when the floating element is opened or closed
*/
onOpenChange?: (open: boolean) => void;
}

const ARROW_FLOATING_STYLE = {
top: "",
right: "rotate(90deg)",
bottom: "rotate(180deg)",
left: "rotate(270deg)",
} as const;

export function usePositionedFloating(props: UsePositionedFloatingProps) {
const options = { ...defaultPositioningOptions, ...props };

const [uncontrolledOpen, setUncontrolledOpen] = useState(props.defaultOpen);
const [arrowEl, setArrowEl] = useState<HTMLElement | null>(null);
const arrowRect = useMemo(() => getArrowRect(arrowEl), [arrowEl]);
const arrowOffset = arrowRect?.height ?? 0;

const open = props.open ?? uncontrolledOpen;
const onOpenChange = props.onOpenChange ?? setUncontrolledOpen;

const { refs, context, floatingStyles, middlewareData } = useFloating({
strategy: options.strategy,
open,
placement: options.placement,
onOpenChange: onOpenChange,
whileElementsMounted: autoUpdate,
middleware: [
getOffsetMiddleware(arrowOffset, options),
getFlipMiddleware(options),
getShiftMiddleware(options),
getArrowMiddleware(arrowEl, options),
rectMiddleware,
],
});

const side = context.placement.split("-")[0] as "top" | "bottom" | "left" | "right";

const arrowStyles = {
position: "absolute",
left: middlewareData.arrow?.x,
top: middlewareData.arrow?.y,
[side]: "100%",
transform: ARROW_FLOATING_STYLE[side],
} as const;

return {
open,
refs: {
...refs,
arrow: arrowEl,
setArrow: setArrowEl,
},
rects: {
...(middlewareData.rects as ElementRects),
arrow: arrowRect,
},
context,
floatingStyles,
arrowStyles,
};
}
Loading

0 comments on commit 8e678ea

Please sign in to comment.