A design system in a box. hip-ui.tngl.io/docs/introduction
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

context menu

+473 -6
+3
apps/example/package.json
··· 9 9 "start": "waku start" 10 10 }, 11 11 "dependencies": { 12 + "@react-types/overlays": "^3.9.2", 12 13 "@repo/typescript-config": "workspace:*", 13 14 "@stylexjs/stylex": "^0.16.1", 14 15 "hip-ui": "workspace:*", 15 16 "lucide-react": "^0.545.0", 16 17 "react": "catalog:", 18 + "react-aria": "^3.44.0", 17 19 "react-aria-components": "^1.13.0", 18 20 "react-dom": "catalog:", 19 21 "react-server-dom-webpack": "19.1.1", 22 + "react-stately": "^3.42.0", 20 23 "unplugin-stylex": "^0.5.5", 21 24 "waku": "0.26.1" 22 25 },
+34 -5
apps/example/src/components/KitchenSink.tsx
··· 61 61 MenuSeparator, 62 62 SubMenu, 63 63 } from "./menu"; 64 - import { 65 - ListBoxSection, 66 - ListBoxSectionHeader, 67 - ListBoxSeparator, 68 - } from "./listbox"; 64 + import { ContextMenu } from "./context-menu"; 65 + import { slate } from "./theme/colors.stylex"; 66 + import { radius } from "./theme/radius.stylex"; 69 67 70 68 const styles = stylex.create({ 71 69 subCard: { ··· 84 82 }, 85 83 grow: { 86 84 flexGrow: 1, 85 + }, 86 + contextMenuArea: { 87 + width: "100px", 88 + height: "100px", 89 + backgroundColor: slate[3], 90 + padding: spacing["4"], 91 + borderRadius: radius["md"], 92 + borderWidth: 1, 93 + borderStyle: "solid", 94 + borderColor: slate[7], 87 95 }, 88 96 }); 89 97 ··· 504 512 </SubMenu> 505 513 </MenuSection> 506 514 </Menu> 515 + 516 + <ContextMenu 517 + trigger={ 518 + <Flex 519 + style={styles.contextMenuArea} 520 + align="center" 521 + justify="center" 522 + {...stylex.props( 523 + typeramp.label, 524 + styles.contextMenuArea, 525 + gray.borderInteractive 526 + )} 527 + > 528 + Right Click me 529 + </Flex> 530 + } 531 + > 532 + <MenuItem>Menu Item 1</MenuItem> 533 + <MenuItem>Menu Item 2</MenuItem> 534 + <MenuItem>Menu Item 3</MenuItem> 535 + </ContextMenu> 507 536 </Flex> 508 537 ); 509 538 }
+206
apps/example/src/components/context-menu/index.tsx
··· 1 + import { OverlayTriggerProps } from "@react-types/overlays"; 2 + import { 3 + Menu as AriaMenu, 4 + MenuProps as AriaMenuProps, 5 + MenuContext, 6 + OverlayTriggerStateContext, 7 + Popover, 8 + PopoverContext, 9 + PopoverProps, 10 + Provider, 11 + RootMenuTriggerStateContext, 12 + } from "react-aria-components"; 13 + import * as stylex from "@stylexjs/stylex"; 14 + 15 + import { 16 + Children, 17 + cloneElement, 18 + createContext, 19 + use, 20 + useCallback, 21 + useRef, 22 + useState, 23 + } from "react"; 24 + import { Size } from "../types"; 25 + import { gray } from "../theme/semantic-color.stylex"; 26 + import { SizeContext } from "../context"; 27 + import { useMenuTriggerState } from "react-stately"; 28 + import { AriaButtonProps, useMenuTrigger } from "react-aria"; 29 + import { radius } from "../theme/radius.stylex"; 30 + import { spacing } from "../theme/spacing.stylex"; 31 + 32 + const styles = stylex.create({ 33 + popover: { 34 + borderRadius: radius["md"], 35 + minWidth: spacing["40"], 36 + outline: "none", 37 + overflow: "auto", 38 + paddingBottom: spacing["1"], 39 + paddingTop: spacing["1"], 40 + }, 41 + menu: { 42 + outline: "none", 43 + }, 44 + }); 45 + 46 + const ContextMenuTriggerProps = createContext< 47 + AriaButtonProps<"button"> & { ref?: React.Ref<HTMLDivElement> } 48 + >({}); 49 + 50 + interface Position { 51 + x: number; 52 + y: number; 53 + } 54 + 55 + const ContextMenuStateContext = createContext<{ 56 + position: Position; 57 + setPosition: (position: Position) => void; 58 + }>({ 59 + position: { x: 0, y: 0 }, 60 + setPosition: () => {}, 61 + }); 62 + 63 + function ContextMenuRoot( 64 + props: OverlayTriggerProps & { children: React.ReactNode } 65 + ) { 66 + const state = useMenuTriggerState(props); 67 + const ref = useRef<HTMLDivElement>(null); 68 + const [position, setPosition] = useState<Position>({ 69 + x: 0, 70 + y: 0, 71 + }); 72 + const { menuTriggerProps, menuProps } = useMenuTrigger( 73 + { 74 + ...props, 75 + type: "menu", 76 + }, 77 + state, 78 + ref 79 + ); 80 + 81 + const scrollRef = useRef(null); 82 + 83 + return ( 84 + <Provider 85 + values={[ 86 + [MenuContext, { ...menuProps, ref: scrollRef }], 87 + [OverlayTriggerStateContext, state], 88 + [RootMenuTriggerStateContext, state], 89 + [ContextMenuTriggerProps, { ...menuTriggerProps, ref }], 90 + [ContextMenuStateContext, { position, setPosition }], 91 + [ 92 + PopoverContext, 93 + { 94 + trigger: "MenuTrigger", 95 + triggerRef: ref, 96 + scrollRef, 97 + placement: "bottom start", 98 + "aria-labelledby": menuProps["aria-labelledby"], 99 + }, 100 + ], 101 + ]} 102 + > 103 + {props.children} 104 + </Provider> 105 + ); 106 + } 107 + 108 + function ContextMenuTrigger( 109 + props: OverlayTriggerProps & { children: React.ReactNode } 110 + ) { 111 + const overlayTriggerState = use(OverlayTriggerStateContext); 112 + const menuTriggerProps = use(ContextMenuTriggerProps); 113 + const { position, setPosition } = use(ContextMenuStateContext); 114 + const onContextMenu = useCallback( 115 + (e: React.MouseEvent<HTMLButtonElement>) => { 116 + e.preventDefault(); 117 + e.stopPropagation(); 118 + overlayTriggerState?.open(); 119 + setPosition({ x: e.clientX, y: e.clientY }); 120 + }, 121 + [menuTriggerProps, overlayTriggerState, setPosition] 122 + ); 123 + 124 + if (Children.count(props.children) !== 1) { 125 + throw new Error("ContextMenuTrigger must have exactly one child"); 126 + } 127 + 128 + return ( 129 + <> 130 + {cloneElement( 131 + props.children as React.ReactElement<React.HTMLAttributes<HTMLElement>>, 132 + { 133 + "aria-controls": menuTriggerProps["aria-controls"], 134 + "aria-expanded": menuTriggerProps["aria-expanded"], 135 + "aria-haspopup": menuTriggerProps["aria-haspopup"], 136 + id: menuTriggerProps["id"], 137 + onContextMenu: onContextMenu, 138 + } 139 + )} 140 + <div 141 + ref={menuTriggerProps.ref} 142 + style={{ 143 + position: "absolute", 144 + top: position?.y, 145 + left: position?.x, 146 + }} 147 + /> 148 + </> 149 + ); 150 + } 151 + 152 + export interface ContextMenuProps<T extends object> 153 + extends OverlayTriggerProps, 154 + Omit<AriaMenuProps<T>, "children" | "className" | "style">, 155 + Pick< 156 + PopoverProps, 157 + | "shouldCloseOnInteractOutside" 158 + | "shouldFlip" 159 + | "shouldUpdatePosition" 160 + | "placement" 161 + > { 162 + trigger: React.ReactNode; 163 + items?: Iterable<T>; 164 + children: React.ReactNode | ((item: T) => React.ReactNode); 165 + size?: Size; 166 + } 167 + 168 + export function ContextMenu<T extends object>({ 169 + trigger, 170 + size = "md", 171 + defaultOpen, 172 + isOpen, 173 + onOpenChange, 174 + shouldCloseOnInteractOutside, 175 + shouldFlip, 176 + shouldUpdatePosition, 177 + placement, 178 + ...props 179 + }: ContextMenuProps<T>) { 180 + return ( 181 + <SizeContext.Provider value={size}> 182 + <ContextMenuRoot 183 + defaultOpen={defaultOpen} 184 + isOpen={isOpen} 185 + onOpenChange={onOpenChange} 186 + > 187 + <ContextMenuTrigger>{trigger}</ContextMenuTrigger> 188 + <Popover 189 + containerPadding={8} 190 + shouldCloseOnInteractOutside={shouldCloseOnInteractOutside} 191 + shouldFlip={shouldFlip} 192 + shouldUpdatePosition={shouldUpdatePosition} 193 + placement={placement} 194 + {...stylex.props( 195 + styles.popover, 196 + gray.bgSubtle, 197 + gray.text, 198 + gray.border 199 + )} 200 + > 201 + <AriaMenu {...props} {...stylex.props(styles.menu)} /> 202 + </Popover> 203 + </ContextMenuRoot> 204 + </SizeContext.Provider> 205 + ); 206 + }
+4 -1
packages/hip-ui/package.json
··· 25 25 }, 26 26 "dependencies": { 27 27 "@inkjs/ui": "^2.0.0", 28 + "@react-types/overlays": "^3.9.2", 28 29 "@stylexjs/stylex": "^0.16.1", 29 30 "command-line-application": "^0.10.1", 30 31 "ink": "^6.3.1", 31 32 "lilconfig": "^3.1.3", 32 33 "lucide-react": "^0.545.0", 33 34 "react": "catalog:", 35 + "react-aria": "^3.44.0", 34 36 "react-aria-components": "^1.13.0", 35 - "react-dom": "catalog:" 37 + "react-dom": "catalog:", 38 + "react-stately": "^3.42.0" 36 39 } 37 40 }
+2
packages/hip-ui/src/cli/install.tsx
··· 29 29 import { toggleButtonGroupConfig } from "../components/toggle-button-group/toggle-button-group-config.js"; 30 30 import { listboxConfig } from "../components/listbox/listbox-config.js"; 31 31 import { menuConfig } from "../components/menu/menu-config.js"; 32 + import { contextMenuConfig } from "../components/context-menu/context-menu-config.js"; 32 33 33 34 const __dirname = path.dirname(new URL(import.meta.url).pathname); 34 35 ··· 53 54 toggleButtonGroupConfig, 54 55 menuConfig, 55 56 listboxConfig, 57 + contextMenuConfig, 56 58 ]; 57 59 58 60 function StringSetting({
+11
packages/hip-ui/src/components/context-menu/context-menu-config.ts
··· 1 + import { ComponentConfig } from "../../types"; 2 + 3 + export const contextMenuConfig: ComponentConfig = { 4 + name: "context-menu", 5 + filepath: "./index.tsx", 6 + dependencies: { 7 + "@react-types/overlays": "^3.9.2", 8 + "react-stately": "^3.42.0", 9 + "react-aria": "^3.44.0", 10 + }, 11 + };
+195
packages/hip-ui/src/components/context-menu/index.tsx
··· 1 + import { OverlayTriggerProps } from "@react-types/overlays"; 2 + import { 3 + Menu as AriaMenu, 4 + MenuProps as AriaMenuProps, 5 + MenuContext, 6 + OverlayTriggerStateContext, 7 + Popover, 8 + PopoverContext, 9 + PopoverProps, 10 + Provider, 11 + RootMenuTriggerStateContext, 12 + } from "react-aria-components"; 13 + import * as stylex from "@stylexjs/stylex"; 14 + 15 + import { 16 + Children, 17 + cloneElement, 18 + createContext, 19 + use, 20 + useCallback, 21 + useRef, 22 + useState, 23 + } from "react"; 24 + import { Size } from "../types"; 25 + import { gray } from "../theme/semantic-color.stylex"; 26 + import { SizeContext } from "../context"; 27 + import { useMenuTriggerState } from "react-stately"; 28 + import { AriaButtonProps, useMenuTrigger } from "react-aria"; 29 + import { radius } from "../theme/radius.stylex"; 30 + import { spacing } from "../theme/spacing.stylex"; 31 + 32 + const styles = stylex.create({ 33 + popover: { 34 + borderRadius: radius["md"], 35 + minWidth: spacing["40"], 36 + outline: "none", 37 + overflow: "auto", 38 + paddingBottom: spacing["1"], 39 + paddingTop: spacing["1"], 40 + }, 41 + menu: { 42 + outline: "none", 43 + }, 44 + }); 45 + 46 + const ContextMenuTriggerProps = createContext< 47 + AriaButtonProps<"button"> & { ref?: React.Ref<HTMLDivElement> } 48 + >({}); 49 + 50 + interface Position { 51 + x: number; 52 + y: number; 53 + } 54 + 55 + const ContextMenuStateContext = createContext<{ 56 + position: Position; 57 + setPosition: (position: Position) => void; 58 + }>({ 59 + position: { x: 0, y: 0 }, 60 + setPosition: () => {}, 61 + }); 62 + 63 + function ContextMenuRoot( 64 + props: OverlayTriggerProps & { children: React.ReactNode } 65 + ) { 66 + const scrollRef = useRef(null); 67 + const state = useMenuTriggerState(props); 68 + const ref = useRef<HTMLDivElement>(null); 69 + const [position, setPosition] = useState<Position>({ x: 0, y: 0 }); 70 + const { menuTriggerProps, menuProps } = useMenuTrigger( 71 + { ...props, type: "menu" }, 72 + state, 73 + ref 74 + ); 75 + 76 + return ( 77 + <Provider 78 + values={[ 79 + [MenuContext, { ...menuProps, ref: scrollRef }], 80 + [OverlayTriggerStateContext, state], 81 + [RootMenuTriggerStateContext, state], 82 + [ContextMenuTriggerProps, { ...menuTriggerProps, ref }], 83 + [ContextMenuStateContext, { position, setPosition }], 84 + [ 85 + PopoverContext, 86 + { 87 + trigger: "MenuTrigger", 88 + triggerRef: ref, 89 + scrollRef, 90 + placement: "bottom start", 91 + "aria-labelledby": menuProps["aria-labelledby"], 92 + }, 93 + ], 94 + ]} 95 + > 96 + {props.children} 97 + </Provider> 98 + ); 99 + } 100 + 101 + function ContextMenuTrigger( 102 + props: OverlayTriggerProps & { children: React.ReactNode } 103 + ) { 104 + const overlayTriggerState = use(OverlayTriggerStateContext); 105 + const menuTriggerProps = use(ContextMenuTriggerProps); 106 + const { position, setPosition } = use(ContextMenuStateContext); 107 + const onContextMenu = useCallback( 108 + (e: React.MouseEvent<HTMLButtonElement>) => { 109 + e.preventDefault(); 110 + e.stopPropagation(); 111 + overlayTriggerState?.open(); 112 + setPosition({ x: e.clientX, y: e.clientY }); 113 + }, 114 + [overlayTriggerState, setPosition] 115 + ); 116 + 117 + if (Children.count(props.children) !== 1) { 118 + throw new Error("ContextMenuTrigger must have exactly one child"); 119 + } 120 + 121 + return ( 122 + <> 123 + {cloneElement( 124 + props.children as React.ReactElement<React.HTMLAttributes<HTMLElement>>, 125 + { 126 + "aria-controls": menuTriggerProps["aria-controls"], 127 + "aria-expanded": menuTriggerProps["aria-expanded"], 128 + "aria-haspopup": menuTriggerProps["aria-haspopup"], 129 + id: menuTriggerProps["id"], 130 + onContextMenu: onContextMenu, 131 + } 132 + )} 133 + <div 134 + ref={menuTriggerProps.ref} 135 + style={{ position: "absolute", top: position?.y, left: position?.x }} 136 + /> 137 + </> 138 + ); 139 + } 140 + 141 + export interface ContextMenuProps<T extends object> 142 + extends OverlayTriggerProps, 143 + Omit<AriaMenuProps<T>, "children" | "className" | "style">, 144 + Pick< 145 + PopoverProps, 146 + | "shouldCloseOnInteractOutside" 147 + | "shouldFlip" 148 + | "shouldUpdatePosition" 149 + | "placement" 150 + > { 151 + trigger: React.ReactNode; 152 + items?: Iterable<T>; 153 + children: React.ReactNode | ((item: T) => React.ReactNode); 154 + size?: Size; 155 + } 156 + 157 + export function ContextMenu<T extends object>({ 158 + trigger, 159 + size = "md", 160 + defaultOpen, 161 + isOpen, 162 + onOpenChange, 163 + shouldCloseOnInteractOutside, 164 + shouldFlip, 165 + shouldUpdatePosition, 166 + placement, 167 + ...props 168 + }: ContextMenuProps<T>) { 169 + return ( 170 + <SizeContext.Provider value={size}> 171 + <ContextMenuRoot 172 + defaultOpen={defaultOpen} 173 + isOpen={isOpen} 174 + onOpenChange={onOpenChange} 175 + > 176 + <ContextMenuTrigger>{trigger}</ContextMenuTrigger> 177 + <Popover 178 + containerPadding={8} 179 + shouldCloseOnInteractOutside={shouldCloseOnInteractOutside} 180 + shouldFlip={shouldFlip} 181 + shouldUpdatePosition={shouldUpdatePosition} 182 + placement={placement} 183 + {...stylex.props( 184 + styles.popover, 185 + gray.bgSubtle, 186 + gray.text, 187 + gray.border 188 + )} 189 + > 190 + <AriaMenu {...props} {...stylex.props(styles.menu)} /> 191 + </Popover> 192 + </ContextMenuRoot> 193 + </SizeContext.Provider> 194 + ); 195 + }
+18
pnpm-lock.yaml
··· 35 35 36 36 apps/example: 37 37 dependencies: 38 + '@react-types/overlays': 39 + specifier: ^3.9.2 40 + version: 3.9.2(react@19.2.0) 38 41 '@repo/typescript-config': 39 42 specifier: workspace:* 40 43 version: link:../../packages/typescript-config ··· 50 53 react: 51 54 specifier: 'catalog:' 52 55 version: 19.2.0 56 + react-aria: 57 + specifier: ^3.44.0 58 + version: 3.44.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) 53 59 react-aria-components: 54 60 specifier: ^1.13.0 55 61 version: 1.13.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) ··· 59 65 react-server-dom-webpack: 60 66 specifier: 19.1.1 61 67 version: 19.1.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(webpack@5.102.1) 68 + react-stately: 69 + specifier: ^3.42.0 70 + version: 3.42.0(react@19.2.0) 62 71 unplugin-stylex: 63 72 specifier: ^0.5.5 64 73 version: 0.5.5(@stylexjs/stylex@0.16.1)(rollup@4.52.4)(vitest@3.2.4(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)) ··· 121 130 '@inkjs/ui': 122 131 specifier: ^2.0.0 123 132 version: 2.0.0(ink@6.3.1(@types/react@19.2.2)(react@19.2.0)) 133 + '@react-types/overlays': 134 + specifier: ^3.9.2 135 + version: 3.9.2(react@19.2.0) 124 136 '@stylexjs/stylex': 125 137 specifier: ^0.16.1 126 138 version: 0.16.1 ··· 139 151 react: 140 152 specifier: 'catalog:' 141 153 version: 19.2.0 154 + react-aria: 155 + specifier: ^3.44.0 156 + version: 3.44.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) 142 157 react-aria-components: 143 158 specifier: ^1.13.0 144 159 version: 1.13.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) 145 160 react-dom: 146 161 specifier: 'catalog:' 147 162 version: 19.2.0(react@19.2.0) 163 + react-stately: 164 + specifier: ^3.42.0 165 + version: 3.42.0(react@19.2.0) 148 166 devDependencies: 149 167 '@repo/eslint-config': 150 168 specifier: workspace:*