diff options
Diffstat (limited to 'apps/web/src/selection/selectable-item.tsx')
| -rw-r--r-- | apps/web/src/selection/selectable-item.tsx | 107 |
1 files changed, 107 insertions, 0 deletions
diff --git a/apps/web/src/selection/selectable-item.tsx b/apps/web/src/selection/selectable-item.tsx new file mode 100644 index 00000000..cc9e1dbf --- /dev/null +++ b/apps/web/src/selection/selectable-item.tsx @@ -0,0 +1,107 @@ +"use client";
+
+import { forwardRef, useCallback } from "react";
+import { useSelectionContext } from "@/selection/context";
+import { SELECTABLE_ITEM_ATTRIBUTE } from "@/selection/attributes";
+import type { SelectableItemProps } from "@/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>
+ );
+ },
+);
|
