diff --git a/package.json b/package.json index 23c43c3..d797a70 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "build": "turbo run build --filter=click-to-*-component", "env": "env", "dev": "turbo run dev --parallel", - "lint": "turbo run lint", + "lint": "turbo run lint --filter=click-to-*-component", "prerelease": "turbo run build", "release": "changeset publish" }, diff --git a/packages/click-to-react-component/src/ContextMenu.js b/packages/click-to-react-component/src/ContextMenu.js index 66ecf06..6155487 100644 --- a/packages/click-to-react-component/src/ContextMenu.js +++ b/packages/click-to-react-component/src/ContextMenu.js @@ -29,282 +29,276 @@ import { getPropsForInstance } from './getPropsForInstance.js' import { getReactInstancesForElement } from './getReactInstancesForElement.js' import { getSourceForInstance } from './getSourceForInstance.js' -export const ContextMenu = React.forwardRef( - ( - /** @type {Props} */ - props, - ref - ) => { - const { onClose, pathModifier } = props +// @ts-expect-error +export const ContextMenu = React.forwardRef((props, ref) => { + // @ts-expect-error + const { onClose, pathModifier } = props - const [target, setTarget] = React.useState( - /** @type {HTMLElement | null} */ - (null) - ) + const [target, setTarget] = React.useState( + /** @type {HTMLElement | null} */ + (null) + ) - const arrowRef = React.useRef( - /** @type {HTMLElement | null} */ - (null) - ) + const arrowRef = React.useRef( + /** @type {HTMLElement | null} */ + (null) + ) - const [activeIndex, setActiveIndex] = React.useState( - /** @type {number | null} */ - (null) - ) + const [activeIndex, setActiveIndex] = React.useState( + /** @type {number | null} */ + (null) + ) - const [open, setOpen] = React.useState(false) + const [open, setOpen] = React.useState(false) - const listItemsRef = React.useRef( - /** @type {Array} */ - ([]) - ) + const listItemsRef = React.useRef( + /** @type {Array} */ + ([]) + ) - const { - x, - y, - reference, - floating, - strategy, - refs, - update, - context, - placement, - middlewareData: { arrow: { x: arrowX, y: arrowY } = {} }, - } = useFloating({ - open, - onOpenChange(open) { - setOpen(open) + const { + x, + y, + reference, + floating, + strategy, + refs, + update, + context, + placement, + middlewareData: { arrow: { x: arrowX, y: arrowY } = {} }, + } = useFloating({ + open, + onOpenChange(open) { + setOpen(open) - if (!open) onClose?.() - }, - middleware: [ - offset({ mainAxis: 5, alignmentAxis: 4 }), - flip(), - shift(), - arrow({ element: arrowRef }), - ], - placement: 'right', - }) + if (!open) onClose?.() + }, + middleware: [ + offset({ mainAxis: 5, alignmentAxis: 4 }), + flip(), + shift(), + arrow({ element: arrowRef }), + ], + placement: 'right', + }) - const { getFloatingProps, getItemProps } = useInteractions([ - useRole(context, { role: 'menu' }), - useDismiss(context), - useListNavigation(context, { - listRef: listItemsRef, - activeIndex, - onNavigate: setActiveIndex, - focusItemOnOpen: false, - }), - ]) + const { getFloatingProps, getItemProps } = useInteractions([ + useRole(context, { role: 'menu' }), + useDismiss(context), + useListNavigation(context, { + listRef: listItemsRef, + activeIndex, + onNavigate: setActiveIndex, + focusItemOnOpen: false, + }), + ]) - React.useEffect(() => { - if (open && refs.reference.current && refs.floating.current) { - return autoUpdate(refs.reference.current, refs.floating.current, update) - } - }, [open, update, refs.reference, refs.floating]) + React.useEffect(() => { + if (open && refs.reference.current && refs.floating.current) { + return autoUpdate(refs.reference.current, refs.floating.current, update) + } + }, [open, update, refs.reference, refs.floating]) - const mergedReferenceRef = React.useMemo( - () => mergeRefs([ref, reference]), - [reference, ref] - ) + const mergedReferenceRef = React.useMemo( + () => mergeRefs([ref, reference]), + [reference, ref] + ) - React.useEffect(() => { - function onContextMenu( - /** @type {MouseEvent} */ - e - ) { - if (!e.altKey) { - return - } - - e.preventDefault() - mergedReferenceRef({ - getBoundingClientRect() { - return { - x: e.clientX, - y: e.clientY, - width: 0, - height: 0, - top: e.clientY, - right: e.clientX, - bottom: e.clientY, - left: e.clientX, - } - }, - }) - - setOpen(true) - - if (e.target instanceof HTMLElement) setTarget(e.target) + React.useEffect(() => { + function onContextMenu( + /** @type {MouseEvent} */ + e + ) { + if (!e.altKey) { + return } - document.addEventListener('contextmenu', onContextMenu) - return () => { - document.removeEventListener('contextmenu', onContextMenu) - } - }, [mergedReferenceRef]) - - React.useLayoutEffect(() => { - if (open) { - refs.floating.current?.focus() - } - }, [open, refs.floating]) - - React.useLayoutEffect(() => { - if (!arrowRef.current) return - - const staticSide = { - top: 'bottom', - right: 'left', - bottom: 'top', - left: 'right', - }[placement.split('-')[0]] - - Object.assign(arrowRef.current.style, { - display: 'block', - left: arrowX != null ? `${arrowX}px` : '', - top: arrowY != null ? `${arrowY}px` : '', - right: '', - bottom: '', - [staticSide]: '-4px', + e.preventDefault() + mergedReferenceRef({ + getBoundingClientRect() { + return { + x: e.clientX, + y: e.clientY, + width: 0, + height: 0, + top: e.clientY, + right: e.clientX, + bottom: e.clientY, + left: e.clientX, + } + }, }) - }, [arrowX, arrowY, placement]) - if (!target) { - return null + setOpen(true) + + if (e.target instanceof HTMLElement) setTarget(e.target) } - const instances = getReactInstancesForElement(target).filter((instance) => - getSourceForInstance(instance) - ) + document.addEventListener('contextmenu', onContextMenu) + return () => { + document.removeEventListener('contextmenu', onContextMenu) + } + }, [mergedReferenceRef]) - return html` - + [data-click-to-component-contextmenu] button:focus code, + [data-click-to-component-contextmenu] button:not([disabled]):active code { + color: white; + } - ${open && - html` + [data-click-to-component-contextmenu] button > * + * { + margin-top: 3px; + } + + [data-click-to-component-contextmenu] button code { + color: royalblue; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, + 'Liberation Mono', 'Courier New', monospace; + } + + [data-click-to-component-contextmenu] button code var { + background: rgba(0 0 0 / 5%); + cursor: help; + border-radius: 3px; + padding: 3px 6px; + font-style: normal; + font-weight: normal; + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, + 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, + 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', + 'Noto Color Emoji'; + } + + [data-click-to-component-contextmenu] button cite { + font-weight: normal; + font-style: normal; + font-size: 11px; + opacity: 0.5; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, + 'Liberation Mono', 'Courier New', monospace; + } + + [data-click-to-component-contextmenu] button cite data::after { + content: attr(value); + float: right; + padding-left: 15px; + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, + 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, + 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', + 'Noto Color Emoji'; + } + + [data-click-to-component-contextmenu-arrow] { + display: none; + position: absolute; + background: inherit; + width: 8px; + height: 8px; + transform: rotate(45deg); + } + + + ${open && + html` <${FloatingOverlay} key="click-to-component-overlay" lockScroll> <${FloatingFocusManager} context=${context}> `} - ` - } -) + ` +}) diff --git a/packages/click-to-react-component/src/types.d.ts b/packages/click-to-react-component/src/types.d.ts index 5a5314c..e0e6b1e 100644 --- a/packages/click-to-react-component/src/types.d.ts +++ b/packages/click-to-react-component/src/types.d.ts @@ -1,12 +1,12 @@ -export { ClickToComponent } from './ClickToComponent'; +export { ClickToComponent } from './ClickToComponent' -export type Editor = 'vscode' | 'vscode-insiders' | 'cursor'; +export type Editor = 'vscode' | 'vscode-insiders' | 'cursor' | string -export type PathModifier = (path: string) => string; +export type PathModifier = (path: string) => string export type ClickToComponentProps = { - editor?: Editor; - pathModifier?: PathModifier; + editor?: Editor + pathModifier?: PathModifier } export type Coords = [MouseEvent['pageX'], MouseEvent['pageY']] @@ -14,6 +14,6 @@ export type Coords = [MouseEvent['pageX'], MouseEvent['pageY']] export type Target = HTMLElement export type ContextMenuProps = { - onClose?: () => void; - pathModifier?: PathModifier; + onClose?: () => void + pathModifier?: PathModifier }