diff options
Diffstat (limited to 'apps/web/src/lib/selection/selectable-item.tsx')
| -rw-r--r-- | apps/web/src/lib/selection/selectable-item.tsx | 107 |
1 files changed, 107 insertions, 0 deletions
diff --git a/apps/web/src/lib/selection/selectable-item.tsx b/apps/web/src/lib/selection/selectable-item.tsx new file mode 100644 index 00000000..1d7e74a8 --- /dev/null +++ b/apps/web/src/lib/selection/selectable-item.tsx @@ -0,0 +1,107 @@ +"use client"; + +import { forwardRef, useCallback } from "react"; +import { useSelectionContext } from "@/lib/selection/context"; +import { SELECTABLE_ITEM_ATTRIBUTE } from "@/lib/selection/constants"; +import type { SelectableItemProps } from "@/lib/selection/types"; +import { cn } from "@/utils/ui"; + +function setForwardedRef<T>(ref: React.ForwardedRef<T>, value: T | null) { + if (typeof ref === "function") { + ref(value); + return; + } + + if (ref) { + ref.current = value; + } +} + +export const SelectableItem = forwardRef<HTMLDivElement, SelectableItemProps>( + function SelectableItem( + { + id, + children, + className, + onClick, + onKeyDown, + onMouseDown, + onContextMenu, + tabIndex, + ...rest + }: SelectableItemProps, + forwardedRef, + ) { + const { + highlightedId, + isBoxSelecting, + isSelected, + handleItemClick, + handleItemMouseDown, + registerItem, + } = useSelectionContext(); + const isItemSelected = isSelected(id); + const isItemHighlighted = highlightedId === id; + const stateClassName = cn( + "relative", + isBoxSelecting && "pointer-events-none", + isItemSelected && "ring-1 ring-primary rounded-sm bg-primary/10", + isItemHighlighted && + (isItemSelected + ? "rounded-sm shadow-[0_0_0_1px_hsl(var(--primary))]" + : "ring-1 ring-primary/60 rounded-sm bg-primary/5"), + ); + + const handleRef = useCallback( + (element: HTMLDivElement | null) => { + registerItem(id, element); + setForwardedRef(forwardedRef, element); + }, + [forwardedRef, id, registerItem], + ); + + return ( + <div + ref={handleRef} + className={cn(stateClassName, className)} + {...{ [SELECTABLE_ITEM_ATTRIBUTE]: "true" }} + role="option" + aria-selected={isItemSelected} + tabIndex={tabIndex ?? 0} + onClick={(event) => { + onClick?.(event); + if (event.defaultPrevented) { + return; + } + + handleItemClick({ event, id }); + }} + onMouseDown={(event) => { + onMouseDown?.(event); + if (event.defaultPrevented) { + return; + } + + handleItemMouseDown({ event, id }); + }} + onKeyDown={(event) => { + onKeyDown?.(event); + if (event.defaultPrevented || event.target !== event.currentTarget) { + return; + } + + if (event.key !== "Enter" && event.key !== " ") { + return; + } + + event.preventDefault(); + handleItemClick({ event, id }); + }} + onContextMenu={onContextMenu} + {...rest} + > + {children} + </div> + ); + }, +); |
