@kinetixui/ui 0.19.0 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1622,12 +1622,17 @@ interface DataGridColumn<TData> {
1622
1622
  onCellEdit?: (row: TData, rowIndex: number, value: string) => void;
1623
1623
  }
1624
1624
  /** A rectangular range of cells, in display order (after sorting and column reordering). */
1625
- interface DataGridSelection {
1625
+ interface DataGridRange {
1626
1626
  /** first and last selected row index, inclusive, into the *displayed* (sorted) rows */
1627
1627
  rows: [number, number];
1628
1628
  /** ids of the selected columns, in display order */
1629
1629
  columns: string[];
1630
1630
  }
1631
+ /** The selection: the range being edited (`rows` / `columns`, the last one) plus every range in `ranges`. */
1632
+ interface DataGridSelection extends DataGridRange {
1633
+ /** every selected range, in the order they were added — more than one after Ctrl/Cmd+click or Ctrl+Space */
1634
+ ranges: DataGridRange[];
1635
+ }
1631
1636
  interface DataGridProps<TData> {
1632
1637
  columns: DataGridColumn<TData>[];
1633
1638
  data: TData[];
@@ -1638,9 +1643,10 @@ interface DataGridProps<TData> {
1638
1643
  getRowId?: (row: TData, index: number) => React$1.Key;
1639
1644
  className?: string;
1640
1645
  /**
1641
- * Opt in to range selection: Shift+arrows / Shift+click extend a rectangle from the anchor cell,
1642
- * Ctrl/Cmd+A selects everything, Ctrl/Cmd+C copies the range as tab-separated text (each
1643
- * column's `value()`; empty where a column has none), Esc clears. Sets aria-multiselectable.
1646
+ * Opt in to range selection: drag, or Shift+arrows / Shift+click, to extend a rectangle from the
1647
+ * anchor cell; Ctrl/Cmd+click or Ctrl+Space starts an additional, separate range; Ctrl/Cmd+A selects
1648
+ * everything; Ctrl/Cmd+C copies the ranges as tab-separated text (each column's `value()`; empty where
1649
+ * a column has none; ranges separated by a blank line); Esc clears. Sets aria-multiselectable.
1644
1650
  */
1645
1651
  selectable?: boolean;
1646
1652
  /** fires when the selected range changes; `null` when it is cleared */
@@ -1812,4 +1818,4 @@ declare const MarkdownEditor: React$1.ForwardRefExoticComponent<MarkdownEditorPr
1812
1818
  /** Merges Tailwind class names, resolving conflicts (clsx + tailwind-merge). */
1813
1819
  declare function cn(...inputs: ClassValue[]): string;
1814
1820
 
1815
- export { Accordion, AccordionContent, AccordionItem, AccordionTrigger, Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, AlertTitle, AppBar, AppBarActions, AppBarBrand, AppBarLink, type AppBarLinkProps, AppBarNav, AspectRatio, AudioPlayer, type AudioPlayerProps, Avatar, AvatarFallback, AvatarGroup, type AvatarGroupProps, AvatarImage, Badge, type BadgeProps, Banner, type BannerProps, Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, ButtonGroup, type ButtonGroupProps, ButtonGroupSeparator, type ButtonGroupSeparatorProps, ButtonGroupText, type ButtonProps, Calendar, type CalendarProps, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Carousel, type CarouselApi, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious, type ChartConfig, ChartContainer, ChartLegend, ChartLegendContent, ChartStyle, ChartTooltip, ChartTooltipContent, Checkbox, CircularProgress, type CircularProgressProps, CodeBlock, type CodeBlockFile, type CodeBlockProps, Collapsible, CollapsibleContent, CollapsibleTrigger, ColorPicker, type ColorPickerProps, Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut, ComparisonSlider, type ComparisonSliderProps, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, DataGrid, type DataGridColumn, type DataGridProps, DataTable, DatePicker, type DatePickerProps, DescriptionList, DescriptionListItem, type DescriptionListItemProps, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DiffViewer, type DiffViewerProps, Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerOverlay, DrawerPortal, DrawerTitle, DrawerTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, type EmptyMediaProps, EmptyTitle, Fab, type FabProps, Field, FieldControl, FieldDescription, FieldLabel, FieldMessage, FileUpload, FileUploadItem, type FileUploadItemProps, type FileUploadProps, Footer, FooterBottom, FooterColumn, type FooterColumnProps, FooterLink, Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, HoverCard, HoverCardContent, HoverCardTrigger, Image, type ImageProps, Inform, type InformProps, Input, InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText, InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, type InputProps, JsonViewer, type JsonViewerProps, KanbanBoard, type KanbanBoardProps, type KanbanCard, type KanbanColumn, Kbd, KbdGroup, type KbdGroupProps, type KbdProps, KinetixDirectionProvider, type KinetixDirectionProviderProps, Label, List, ListItem, type ListItemProps, MarkdownEditor, type MarkdownEditorProps, Marquee, type MarqueeProps, Menubar, MenubarCheckboxItem, MenubarContent, MenubarGroup, MenubarItem, MenubarLabel, MenubarMenu, MenubarPortal, MenubarRadioGroup, MenubarRadioItem, MenubarSeparator, MenubarShortcut, MenubarSub, MenubarSubContent, MenubarSubTrigger, MenubarTrigger, MessageBubble, type MessageBubbleProps, Metric, type MetricProps, Modal, type ModalProps, type ModalType, MultiSelect, type MultiSelectOption, type MultiSelectProps, NativeSelect, NativeSelectOptGroup, type NativeSelectOptGroupProps, NativeSelectOption, type NativeSelectOptionProps, type NativeSelectProps, NavigationBar, type NavigationBarProps, NavigationMenu, NavigationMenuContent, NavigationMenuIndicator, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger, NavigationMenuViewport, NotificationCenter, NotificationCenterContent, type NotificationCenterContentProps, NotificationCenterTrigger, type NotificationCenterTriggerProps, NotificationItem, type NotificationItemProps, NumberInput, type NumberInputProps, PageHeader, type PageHeaderProps, Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, PasswordInput, Popover, PopoverAnchor, PopoverContent, PopoverTrigger, Progress, Quote, type QuoteProps, RadioGroup, RadioGroupItem, Rating, type RatingProps, ResizableHandle, ResizablePanel, ResizablePanelGroup, ScrollArea, ScrollBar, SegmentedControl, SegmentedControlItem, type SegmentedControlProps, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue, Separator, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInput, SidebarInset, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarMenuSkeleton, SidebarProvider, SidebarSeparator, SidebarTrigger, Skeleton, Slider, Spinner, type SpinnerProps, Stepper, type StepperProps, type StepperStep, Switch, TabBar, TabBarItem, type TabBarItemProps, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableOfContents, type TableOfContentsProps, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, type TagProps, Textarea, type TextareaProps, Timeline, type TimelineItem, type TimelineProps, Toaster, type TocItem, Toggle, ToggleGroup, ToggleGroupItem, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, Tour, type TourProps, type TourStep, TreeItem, type TreeItemProps, TreeView, type TreeViewProps, TypingIndicator, type UploadFile, VirtualList, type VirtualListProps, badgeVariants, bannerVariants, buttonVariants, cn, emptyMediaVariants, fabVariants, formatBytes, informVariants, inputVariants, messageVariants, nativeSelectVariants, navigationMenuTriggerStyle, spinnerVariants, tagVariants, textareaVariants, toggleVariants, useAppBar, useFormField, useSidebar };
1821
+ export { Accordion, AccordionContent, AccordionItem, AccordionTrigger, Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, AlertTitle, AppBar, AppBarActions, AppBarBrand, AppBarLink, type AppBarLinkProps, AppBarNav, AspectRatio, AudioPlayer, type AudioPlayerProps, Avatar, AvatarFallback, AvatarGroup, type AvatarGroupProps, AvatarImage, Badge, type BadgeProps, Banner, type BannerProps, Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, ButtonGroup, type ButtonGroupProps, ButtonGroupSeparator, type ButtonGroupSeparatorProps, ButtonGroupText, type ButtonProps, Calendar, type CalendarProps, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Carousel, type CarouselApi, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious, type ChartConfig, ChartContainer, ChartLegend, ChartLegendContent, ChartStyle, ChartTooltip, ChartTooltipContent, Checkbox, CircularProgress, type CircularProgressProps, CodeBlock, type CodeBlockFile, type CodeBlockProps, Collapsible, CollapsibleContent, CollapsibleTrigger, ColorPicker, type ColorPickerProps, Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut, ComparisonSlider, type ComparisonSliderProps, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, DataGrid, type DataGridColumn, type DataGridProps, type DataGridRange, type DataGridSelection, DataTable, DatePicker, type DatePickerProps, DescriptionList, DescriptionListItem, type DescriptionListItemProps, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DiffViewer, type DiffViewerProps, Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerOverlay, DrawerPortal, DrawerTitle, DrawerTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, type EmptyMediaProps, EmptyTitle, Fab, type FabProps, Field, FieldControl, FieldDescription, FieldLabel, FieldMessage, FileUpload, FileUploadItem, type FileUploadItemProps, type FileUploadProps, Footer, FooterBottom, FooterColumn, type FooterColumnProps, FooterLink, Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, HoverCard, HoverCardContent, HoverCardTrigger, Image, type ImageProps, Inform, type InformProps, Input, InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupText, InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, type InputProps, JsonViewer, type JsonViewerProps, KanbanBoard, type KanbanBoardProps, type KanbanCard, type KanbanColumn, Kbd, KbdGroup, type KbdGroupProps, type KbdProps, KinetixDirectionProvider, type KinetixDirectionProviderProps, Label, List, ListItem, type ListItemProps, MarkdownEditor, type MarkdownEditorProps, Marquee, type MarqueeProps, Menubar, MenubarCheckboxItem, MenubarContent, MenubarGroup, MenubarItem, MenubarLabel, MenubarMenu, MenubarPortal, MenubarRadioGroup, MenubarRadioItem, MenubarSeparator, MenubarShortcut, MenubarSub, MenubarSubContent, MenubarSubTrigger, MenubarTrigger, MessageBubble, type MessageBubbleProps, Metric, type MetricProps, Modal, type ModalProps, type ModalType, MultiSelect, type MultiSelectOption, type MultiSelectProps, NativeSelect, NativeSelectOptGroup, type NativeSelectOptGroupProps, NativeSelectOption, type NativeSelectOptionProps, type NativeSelectProps, NavigationBar, type NavigationBarProps, NavigationMenu, NavigationMenuContent, NavigationMenuIndicator, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger, NavigationMenuViewport, NotificationCenter, NotificationCenterContent, type NotificationCenterContentProps, NotificationCenterTrigger, type NotificationCenterTriggerProps, NotificationItem, type NotificationItemProps, NumberInput, type NumberInputProps, PageHeader, type PageHeaderProps, Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, PasswordInput, Popover, PopoverAnchor, PopoverContent, PopoverTrigger, Progress, Quote, type QuoteProps, RadioGroup, RadioGroupItem, Rating, type RatingProps, ResizableHandle, ResizablePanel, ResizablePanelGroup, ScrollArea, ScrollBar, SegmentedControl, SegmentedControlItem, type SegmentedControlProps, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue, Separator, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInput, SidebarInset, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarMenuSkeleton, SidebarProvider, SidebarSeparator, SidebarTrigger, Skeleton, Slider, Spinner, type SpinnerProps, Stepper, type StepperProps, type StepperStep, Switch, TabBar, TabBarItem, type TabBarItemProps, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableOfContents, type TableOfContentsProps, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, type TagProps, Textarea, type TextareaProps, Timeline, type TimelineItem, type TimelineProps, Toaster, type TocItem, Toggle, ToggleGroup, ToggleGroupItem, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, Tour, type TourProps, type TourStep, TreeItem, type TreeItemProps, TreeView, type TreeViewProps, TypingIndicator, type UploadFile, VirtualList, type VirtualListProps, badgeVariants, bannerVariants, buttonVariants, cn, emptyMediaVariants, fabVariants, formatBytes, informVariants, inputVariants, messageVariants, nativeSelectVariants, navigationMenuTriggerStyle, spinnerVariants, tagVariants, textareaVariants, toggleVariants, useAppBar, useFormField, useSidebar };
package/dist/index.js CHANGED
@@ -5148,6 +5148,19 @@ function DataGrid({
5148
5148
  const pendingFocusRef = React83.useRef(null);
5149
5149
  const [announcement, setAnnouncement] = React83.useState("");
5150
5150
  const [sel, setSel] = React83.useState(null);
5151
+ const [extra, setExtra] = React83.useState([]);
5152
+ const dragRef = React83.useRef(null);
5153
+ React83.useEffect(() => {
5154
+ const end = () => {
5155
+ dragRef.current = null;
5156
+ };
5157
+ window.addEventListener("mouseup", end);
5158
+ return () => window.removeEventListener("mouseup", end);
5159
+ }, []);
5160
+ const clearSel = () => {
5161
+ setSel(null);
5162
+ setExtra([]);
5163
+ };
5151
5164
  const onSelectionChangeRef = React83.useRef(onSelectionChange);
5152
5165
  onSelectionChangeRef.current = onSelectionChange;
5153
5166
  const lastEmittedRef = React83.useRef("null");
@@ -5158,43 +5171,62 @@ function DataGrid({
5158
5171
  const activeRendered = activeRow < 0 || activeRow >= startIndex && activeRow < endIndex;
5159
5172
  const tabStop = { row: activeRendered ? activeRow : -1, columnId: activeCol };
5160
5173
  const colKey = colIds.join("|");
5161
- const range = React83.useMemo(() => {
5162
- if (!selectable || !sel) return null;
5163
- const a = colIds.indexOf(sel.anchor.columnId);
5164
- const b = colIds.indexOf(sel.end.columnId);
5165
- if (a < 0 || b < 0) return null;
5166
- return { r0: Math.min(sel.anchor.row, sel.end.row), r1: Math.max(sel.anchor.row, sel.end.row), c0: Math.min(a, b), c1: Math.max(a, b) };
5167
- }, [selectable, sel, colKey]);
5168
- React83.useEffect(() => setSel(null), [sort, rowCount]);
5174
+ const rects = React83.useMemo(() => {
5175
+ if (!selectable) return [];
5176
+ const out = [];
5177
+ for (const s of sel ? [...extra, sel] : extra) {
5178
+ const a = colIds.indexOf(s.anchor.columnId);
5179
+ const b = colIds.indexOf(s.end.columnId);
5180
+ if (a < 0 || b < 0) continue;
5181
+ out.push({ r0: Math.min(s.anchor.row, s.end.row), r1: Math.max(s.anchor.row, s.end.row), c0: Math.min(a, b), c1: Math.max(a, b) });
5182
+ }
5183
+ return out;
5184
+ }, [selectable, sel, extra, colKey]);
5185
+ const hasSelection = rects.length > 0;
5186
+ const cellCount = rects.reduce((n, r) => n + (r.r1 - r.r0 + 1) * (r.c1 - r.c0 + 1), 0);
5187
+ const isSelectedCell = (row, colIdx) => rects.some((r) => row >= r.r0 && row <= r.r1 && colIdx >= r.c0 && colIdx <= r.c1);
5188
+ React83.useEffect(() => {
5189
+ setSel(null);
5190
+ setExtra([]);
5191
+ }, [sort, rowCount]);
5169
5192
  React83.useEffect(() => {
5170
5193
  if (!selectable) return;
5171
- const value = range ? { rows: [range.r0, range.r1], columns: colIds.slice(range.c0, range.c1 + 1) } : null;
5194
+ const ranges = rects.map((r) => ({ rows: [r.r0, r.r1], columns: colIds.slice(r.c0, r.c1 + 1) }));
5195
+ const value = ranges.length ? { ...ranges[ranges.length - 1], ranges } : null;
5172
5196
  const key = JSON.stringify(value);
5173
5197
  if (key === lastEmittedRef.current) return;
5174
5198
  lastEmittedRef.current = key;
5175
5199
  onSelectionChangeRef.current?.(value);
5176
- setAnnouncement(value ? `${(range.r1 - range.r0 + 1) * (range.c1 - range.c0 + 1)} cells selected` : "Selection cleared");
5177
- }, [selectable, range]);
5200
+ setAnnouncement(value ? `${cellCount} cells selected` : "Selection cleared");
5201
+ }, [selectable, rects]);
5178
5202
  function extendSelection(fromRow, fromCol, toRow, toCol) {
5179
5203
  const anchor = sel?.anchor ?? { row: fromRow, columnId: fromCol };
5180
5204
  const end = { row: Math.max(0, Math.min(toRow, rowCount - 1)), columnId: toCol };
5181
5205
  setSel({ anchor, end });
5182
5206
  moveTo(end.row, end.columnId);
5183
5207
  }
5208
+ function addRange(row, columnId, prev) {
5209
+ const previous = sel ?? (prev ? { anchor: prev, end: prev } : null);
5210
+ if (previous) setExtra((x) => [...x, previous]);
5211
+ setSel({ anchor: { row, columnId }, end: { row, columnId } });
5212
+ }
5184
5213
  function selectAll() {
5185
5214
  if (rowCount === 0 || colIds.length === 0) return;
5215
+ setExtra([]);
5186
5216
  setSel({ anchor: { row: 0, columnId: colIds[0] }, end: { row: rowCount - 1, columnId: colIds[colIds.length - 1] } });
5187
5217
  }
5188
5218
  function copySelection() {
5189
- if (!range) return;
5190
- const ids = colIds.slice(range.c0, range.c1 + 1);
5191
- const text = sortedData.slice(range.r0, range.r1 + 1).map((r) => ids.map((id) => {
5192
- const c = columnById.get(id);
5193
- return c?.value ? String(c.value(r)) : "";
5194
- }).join(" ")).join("\n");
5219
+ if (!hasSelection) return;
5220
+ const text = rects.map((r) => {
5221
+ const ids = colIds.slice(r.c0, r.c1 + 1);
5222
+ return sortedData.slice(r.r0, r.r1 + 1).map((row) => ids.map((id) => {
5223
+ const c = columnById.get(id);
5224
+ return c?.value ? String(c.value(row)) : "";
5225
+ }).join(" ")).join("\n");
5226
+ }).join("\n\n");
5195
5227
  try {
5196
5228
  void navigator.clipboard?.writeText(text);
5197
- setAnnouncement(`Copied ${(range.r1 - range.r0 + 1) * ids.length} cells`);
5229
+ setAnnouncement(`Copied ${cellCount} cells`);
5198
5230
  } catch {
5199
5231
  }
5200
5232
  }
@@ -5277,13 +5309,17 @@ function DataGrid({
5277
5309
  e.preventDefault();
5278
5310
  return selectAll();
5279
5311
  }
5280
- if (mod && e.key.toLowerCase() === "c" && range) {
5312
+ if (mod && e.key.toLowerCase() === "c" && hasSelection) {
5281
5313
  e.preventDefault();
5282
5314
  return copySelection();
5283
5315
  }
5284
- if (e.key === "Escape" && sel) {
5316
+ if (mod && e.key === " ") {
5317
+ e.preventDefault();
5318
+ return addRange(row, column.id, { row, columnId: column.id });
5319
+ }
5320
+ if (e.key === "Escape" && hasSelection) {
5285
5321
  e.preventDefault();
5286
- return setSel(null);
5322
+ return clearSel();
5287
5323
  }
5288
5324
  }
5289
5325
  const extend = selectable && e.shiftKey && row >= 0;
@@ -5291,7 +5327,7 @@ function DataGrid({
5291
5327
  e.preventDefault();
5292
5328
  const target = colIds[Math.max(0, Math.min(i, colIds.length - 1))];
5293
5329
  if (extend) return extendSelection(row, column.id, r, target);
5294
- if (sel) setSel(null);
5330
+ if (hasSelection) clearSel();
5295
5331
  moveTo(r, target);
5296
5332
  };
5297
5333
  switch (e.key) {
@@ -5413,7 +5449,7 @@ function DataGrid({
5413
5449
  const width = widths[column.id] ?? column.width ?? DEFAULT_WIDTH;
5414
5450
  const pinnedStyle = column.pinned === "left" ? { position: "sticky", left: leftOffsets[column.id], zIndex: 1 } : column.pinned === "right" ? { position: "sticky", right: rightOffsets[column.id], zIndex: 1 } : {};
5415
5451
  const isEditing = editing?.rowIndex === rowIndex && editing.columnId === column.id;
5416
- const selected = selectable && !!range && rowIndex >= range.r0 && rowIndex <= range.r1 && colIdx >= range.c0 && colIdx <= range.c1;
5452
+ const selected = selectable && isSelectedCell(rowIndex, colIdx);
5417
5453
  return /* @__PURE__ */ jsx89(
5418
5454
  "div",
5419
5455
  {
@@ -5422,11 +5458,29 @@ function DataGrid({
5422
5458
  "aria-selected": selectable ? selected : void 0,
5423
5459
  "data-cell": `${rowIndex}:${column.id}`,
5424
5460
  onMouseDown: (e) => {
5425
- if (!selectable || e.button !== 0) return;
5461
+ if (!selectable || e.button !== 0 || isEditing) return;
5462
+ const here = { row: rowIndex, columnId: column.id };
5426
5463
  if (e.shiftKey) {
5427
5464
  e.preventDefault();
5428
5465
  extendSelection(activeRow >= 0 ? activeRow : rowIndex, activeCol, rowIndex, column.id);
5429
- } else if (sel) setSel(null);
5466
+ } else if (e.ctrlKey || e.metaKey) {
5467
+ addRange(rowIndex, column.id, activeRow >= 0 ? { row: activeRow, columnId: activeCol } : void 0);
5468
+ dragRef.current = here;
5469
+ } else {
5470
+ if (hasSelection) clearSel();
5471
+ dragRef.current = here;
5472
+ }
5473
+ },
5474
+ onMouseEnter: (e) => {
5475
+ const start = dragRef.current;
5476
+ if (!selectable || !start) return;
5477
+ if (e.buttons !== 1) {
5478
+ dragRef.current = null;
5479
+ return;
5480
+ }
5481
+ if (start.row === rowIndex && start.columnId === column.id) return;
5482
+ setSel({ anchor: start, end: { row: rowIndex, columnId: column.id } });
5483
+ moveTo(rowIndex, column.id);
5430
5484
  },
5431
5485
  onDoubleClick: () => column.editable && setEditing({ rowIndex, columnId: column.id }),
5432
5486
  tabIndex: tabStop.row === rowIndex && tabStop.columnId === column.id ? 0 : -1,
@@ -5435,6 +5489,7 @@ function DataGrid({
5435
5489
  className: cn(
5436
5490
  "flex shrink-0 items-center px-2 outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring",
5437
5491
  column.pinned && "bg-background",
5492
+ selectable && "select-none",
5438
5493
  selected && "bg-accent"
5439
5494
  ),
5440
5495
  style: { width, ...pinnedStyle },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kinetixui/ui",
3
- "version": "0.19.0",
3
+ "version": "0.20.1",
4
4
  "private": false,
5
5
  "description": "KinetixUI React components — CVA + Radix + Tailwind on the KinetixUI token contract. Also distributed via the kinetixui CLI as a component registry.",
6
6
  "keywords": [
@@ -44,7 +44,7 @@
44
44
  "@dnd-kit/sortable": "^10.0.0",
45
45
  "@dnd-kit/utilities": "^3.2.2",
46
46
  "@hookform/resolvers": "^5.9.1",
47
- "@kinetixui/tokens": "0.19.0",
47
+ "@kinetixui/tokens": "0.20.1",
48
48
  "@radix-ui/react-accordion": "^1.2.20",
49
49
  "@radix-ui/react-alert-dialog": "^1.1.23",
50
50
  "@radix-ui/react-aspect-ratio": "^1.1.15",
@@ -25,13 +25,19 @@ export interface DataGridColumn<TData> {
25
25
  }
26
26
 
27
27
  /** A rectangular range of cells, in display order (after sorting and column reordering). */
28
- export interface DataGridSelection {
28
+ export interface DataGridRange {
29
29
  /** first and last selected row index, inclusive, into the *displayed* (sorted) rows */
30
30
  rows: [number, number];
31
31
  /** ids of the selected columns, in display order */
32
32
  columns: string[];
33
33
  }
34
34
 
35
+ /** The selection: the range being edited (`rows` / `columns`, the last one) plus every range in `ranges`. */
36
+ export interface DataGridSelection extends DataGridRange {
37
+ /** every selected range, in the order they were added — more than one after Ctrl/Cmd+click or Ctrl+Space */
38
+ ranges: DataGridRange[];
39
+ }
40
+
35
41
  export interface DataGridProps<TData> {
36
42
  columns: DataGridColumn<TData>[];
37
43
  data: TData[];
@@ -42,9 +48,10 @@ export interface DataGridProps<TData> {
42
48
  getRowId?: (row: TData, index: number) => React.Key;
43
49
  className?: string;
44
50
  /**
45
- * Opt in to range selection: Shift+arrows / Shift+click extend a rectangle from the anchor cell,
46
- * Ctrl/Cmd+A selects everything, Ctrl/Cmd+C copies the range as tab-separated text (each
47
- * column's `value()`; empty where a column has none), Esc clears. Sets aria-multiselectable.
51
+ * Opt in to range selection: drag, or Shift+arrows / Shift+click, to extend a rectangle from the
52
+ * anchor cell; Ctrl/Cmd+click or Ctrl+Space starts an additional, separate range; Ctrl/Cmd+A selects
53
+ * everything; Ctrl/Cmd+C copies the ranges as tab-separated text (each column's `value()`; empty where
54
+ * a column has none; ranges separated by a blank line); Esc clears. Sets aria-multiselectable.
48
55
  */
49
56
  selectable?: boolean;
50
57
  /** fires when the selected range changes; `null` when it is cleared */
@@ -223,7 +230,22 @@ function DataGrid<TData>({
223
230
 
224
231
  // ── range selection (opt-in) ──────────────────────────────────────────────────────────────
225
232
  type Anchor = { row: number; columnId: string };
226
- const [sel, setSel] = React.useState<{ anchor: Anchor; end: Anchor } | null>(null);
233
+ type Sel = { anchor: Anchor; end: Anchor };
234
+ const [sel, setSel] = React.useState<Sel | null>(null);
235
+ /** ranges committed before the current one (Ctrl/Cmd+click, Ctrl+Space) */
236
+ const [extra, setExtra] = React.useState<Sel[]>([]);
237
+ const dragRef = React.useRef<Anchor | null>(null);
238
+ React.useEffect(() => {
239
+ const end = () => {
240
+ dragRef.current = null;
241
+ };
242
+ window.addEventListener("mouseup", end);
243
+ return () => window.removeEventListener("mouseup", end);
244
+ }, []);
245
+ const clearSel = () => {
246
+ setSel(null);
247
+ setExtra([]);
248
+ };
227
249
  const onSelectionChangeRef = React.useRef(onSelectionChange);
228
250
  onSelectionChangeRef.current = onSelectionChange;
229
251
  const lastEmittedRef = React.useRef("null");
@@ -237,28 +259,39 @@ function DataGrid<TData>({
237
259
  const activeRendered = activeRow < 0 || (activeRow >= startIndex && activeRow < endIndex);
238
260
  const tabStop: Pos = { row: activeRendered ? activeRow : -1, columnId: activeCol };
239
261
  const colKey = colIds.join("|");
240
- const range = React.useMemo(() => {
241
- if (!selectable || !sel) return null;
242
- const a = colIds.indexOf(sel.anchor.columnId);
243
- const b = colIds.indexOf(sel.end.columnId);
244
- if (a < 0 || b < 0) return null;
245
- return { r0: Math.min(sel.anchor.row, sel.end.row), r1: Math.max(sel.anchor.row, sel.end.row), c0: Math.min(a, b), c1: Math.max(a, b) };
262
+ const rects = React.useMemo(() => {
263
+ if (!selectable) return [];
264
+ const out: { r0: number; r1: number; c0: number; c1: number }[] = [];
265
+ for (const s of sel ? [...extra, sel] : extra) {
266
+ const a = colIds.indexOf(s.anchor.columnId);
267
+ const b = colIds.indexOf(s.end.columnId);
268
+ if (a < 0 || b < 0) continue;
269
+ out.push({ r0: Math.min(s.anchor.row, s.end.row), r1: Math.max(s.anchor.row, s.end.row), c0: Math.min(a, b), c1: Math.max(a, b) });
270
+ }
271
+ return out;
246
272
  // eslint-disable-next-line react-hooks/exhaustive-deps
247
- }, [selectable, sel, colKey]);
273
+ }, [selectable, sel, extra, colKey]);
274
+ const hasSelection = rects.length > 0;
275
+ const cellCount = rects.reduce((n, r) => n + (r.r1 - r.r0 + 1) * (r.c1 - r.c0 + 1), 0);
276
+ const isSelectedCell = (row: number, colIdx: number) => rects.some((r) => row >= r.r0 && row <= r.r1 && colIdx >= r.c0 && colIdx <= r.c1);
248
277
 
249
278
  // Selection refers to displayed rows, so a re-sort or a different row count invalidates it.
250
- React.useEffect(() => setSel(null), [sort, rowCount]);
279
+ React.useEffect(() => {
280
+ setSel(null);
281
+ setExtra([]);
282
+ }, [sort, rowCount]);
251
283
 
252
284
  React.useEffect(() => {
253
285
  if (!selectable) return;
254
- const value = range ? { rows: [range.r0, range.r1] as [number, number], columns: colIds.slice(range.c0, range.c1 + 1) } : null;
286
+ const ranges: DataGridRange[] = rects.map((r) => ({ rows: [r.r0, r.r1] as [number, number], columns: colIds.slice(r.c0, r.c1 + 1) }));
287
+ const value: DataGridSelection | null = ranges.length ? { ...ranges[ranges.length - 1]!, ranges } : null;
255
288
  const key = JSON.stringify(value);
256
289
  if (key === lastEmittedRef.current) return;
257
290
  lastEmittedRef.current = key;
258
291
  onSelectionChangeRef.current?.(value);
259
- setAnnouncement(value ? `${(range!.r1 - range!.r0 + 1) * (range!.c1 - range!.c0 + 1)} cells selected` : "Selection cleared");
292
+ setAnnouncement(value ? `${cellCount} cells selected` : "Selection cleared");
260
293
  // eslint-disable-next-line react-hooks/exhaustive-deps
261
- }, [selectable, range]);
294
+ }, [selectable, rects]);
262
295
 
263
296
  function extendSelection(fromRow: number, fromCol: string, toRow: number, toCol: string) {
264
297
  const anchor = sel?.anchor ?? { row: fromRow, columnId: fromCol };
@@ -267,21 +300,33 @@ function DataGrid<TData>({
267
300
  moveTo(end.row, end.columnId);
268
301
  }
269
302
 
303
+ /** Start a new range at a cell, keeping the current one (the active cell counts when nothing is selected yet). */
304
+ function addRange(row: number, columnId: string, prev?: Anchor) {
305
+ const previous = sel ?? (prev ? { anchor: prev, end: prev } : null);
306
+ if (previous) setExtra((x) => [...x, previous]);
307
+ setSel({ anchor: { row, columnId }, end: { row, columnId } });
308
+ }
309
+
270
310
  function selectAll() {
271
311
  if (rowCount === 0 || colIds.length === 0) return;
312
+ setExtra([]);
272
313
  setSel({ anchor: { row: 0, columnId: colIds[0]! }, end: { row: rowCount - 1, columnId: colIds[colIds.length - 1]! } });
273
314
  }
274
315
 
275
316
  function copySelection() {
276
- if (!range) return;
277
- const ids = colIds.slice(range.c0, range.c1 + 1);
278
- const text = sortedData
279
- .slice(range.r0, range.r1 + 1)
280
- .map((r) => ids.map((id) => { const c = columnById.get(id); return c?.value ? String(c.value(r)) : ""; }).join("\t"))
281
- .join("\n");
317
+ if (!hasSelection) return;
318
+ const text = rects
319
+ .map((r) => {
320
+ const ids = colIds.slice(r.c0, r.c1 + 1);
321
+ return sortedData
322
+ .slice(r.r0, r.r1 + 1)
323
+ .map((row) => ids.map((id) => { const c = columnById.get(id); return c?.value ? String(c.value(row)) : ""; }).join("\t"))
324
+ .join("\n");
325
+ })
326
+ .join("\n\n");
282
327
  try {
283
328
  void navigator.clipboard?.writeText(text);
284
- setAnnouncement(`Copied ${(range.r1 - range.r0 + 1) * ids.length} cells`);
329
+ setAnnouncement(`Copied ${cellCount} cells`);
285
330
  } catch {
286
331
  /* clipboard unavailable (insecure context / permission) — nothing to announce */
287
332
  }
@@ -377,13 +422,18 @@ function DataGrid<TData>({
377
422
  e.preventDefault();
378
423
  return selectAll();
379
424
  }
380
- if (mod && e.key.toLowerCase() === "c" && range) {
425
+ if (mod && e.key.toLowerCase() === "c" && hasSelection) {
381
426
  e.preventDefault();
382
427
  return copySelection();
383
428
  }
384
- if (e.key === "Escape" && sel) {
429
+ if (mod && e.key === " ") {
430
+ // keyboard equivalent of Ctrl/Cmd+click: keep what is selected and start another range here
385
431
  e.preventDefault();
386
- return setSel(null);
432
+ return addRange(row, column.id, { row, columnId: column.id });
433
+ }
434
+ if (e.key === "Escape" && hasSelection) {
435
+ e.preventDefault();
436
+ return clearSel();
387
437
  }
388
438
  }
389
439
  const extend = selectable && e.shiftKey && row >= 0;
@@ -391,7 +441,7 @@ function DataGrid<TData>({
391
441
  e.preventDefault();
392
442
  const target = colIds[Math.max(0, Math.min(i, colIds.length - 1))]!;
393
443
  if (extend) return extendSelection(row, column.id, r, target);
394
- if (sel) setSel(null); // a plain move collapses the range
444
+ if (hasSelection) clearSel(); // a plain move collapses the selection
395
445
  moveTo(r, target);
396
446
  };
397
447
  switch (e.key) {
@@ -530,7 +580,7 @@ function DataGrid<TData>({
530
580
  ? { position: "sticky", right: rightOffsets[column.id], zIndex: 1 }
531
581
  : {};
532
582
  const isEditing = editing?.rowIndex === rowIndex && editing.columnId === column.id;
533
- const selected = selectable && !!range && rowIndex >= range.r0 && rowIndex <= range.r1 && colIdx >= range.c0 && colIdx <= range.c1;
583
+ const selected = selectable && isSelectedCell(rowIndex, colIdx);
534
584
  return (
535
585
  <div
536
586
  key={column.id}
@@ -539,13 +589,31 @@ function DataGrid<TData>({
539
589
  aria-selected={selectable ? selected : undefined}
540
590
  data-cell={`${rowIndex}:${column.id}`}
541
591
  onMouseDown={(e) => {
542
- if (!selectable || e.button !== 0) return;
592
+ if (!selectable || e.button !== 0 || isEditing) return;
593
+ const here = { row: rowIndex, columnId: column.id };
543
594
  if (e.shiftKey) {
544
595
  // extend from the previously active cell (focus has not moved yet on mousedown) — even when it has
545
596
  // since scrolled out of the rendered window
546
597
  e.preventDefault();
547
598
  extendSelection(activeRow >= 0 ? activeRow : rowIndex, activeCol, rowIndex, column.id);
548
- } else if (sel) setSel(null);
599
+ } else if (e.ctrlKey || e.metaKey) {
600
+ addRange(rowIndex, column.id, activeRow >= 0 ? { row: activeRow, columnId: activeCol } : undefined);
601
+ dragRef.current = here; // Ctrl+drag sizes the new range
602
+ } else {
603
+ if (hasSelection) clearSel();
604
+ dragRef.current = here; // a plain click selects nothing until the pointer reaches another cell
605
+ }
606
+ }}
607
+ onMouseEnter={(e) => {
608
+ const start = dragRef.current;
609
+ if (!selectable || !start) return;
610
+ if (e.buttons !== 1) {
611
+ dragRef.current = null; // the button was released outside the grid
612
+ return;
613
+ }
614
+ if (start.row === rowIndex && start.columnId === column.id) return;
615
+ setSel({ anchor: start, end: { row: rowIndex, columnId: column.id } });
616
+ moveTo(rowIndex, column.id); // the dragged-to cell becomes the active one, so Shift+arrows continue from it
549
617
  }}
550
618
  onDoubleClick={() => column.editable && setEditing({ rowIndex, columnId: column.id })}
551
619
  // roving tabindex; Enter or F2 edits an editable cell (the spreadsheet convention)
@@ -555,6 +623,7 @@ function DataGrid<TData>({
555
623
  className={cn(
556
624
  "flex shrink-0 items-center px-2 outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring",
557
625
  column.pinned && "bg-background",
626
+ selectable && "select-none",
558
627
  selected && "bg-accent",
559
628
  )}
560
629
  style={{ width, ...pinnedStyle }}
@@ -660,7 +660,7 @@ describe("DataGrid", () => {
660
660
 
661
661
  expect(selectedCells()).toHaveLength(4); // rows 0-1 x Name, ID
662
662
  expect(cell(1, 1)).toHaveFocus();
663
- expect(onSelectionChange).toHaveBeenLastCalledWith({ rows: [0, 1], columns: ["name", "id"] });
663
+ expect(onSelectionChange).toHaveBeenLastCalledWith({ rows: [0, 1], columns: ["name", "id"], ranges: [{ rows: [0, 1], columns: ["name", "id"] }] });
664
664
  expect(screen.getByRole("status")).toHaveTextContent("4 cells selected");
665
665
  });
666
666
 
@@ -749,7 +749,106 @@ describe("DataGrid", () => {
749
749
  await user.keyboard("{Shift>}");
750
750
  await user.click(target);
751
751
  await user.keyboard("{/Shift}");
752
- expect(onSelectionChange).toHaveBeenLastCalledWith({ rows: [0, targetRow], columns: ["name"] });
752
+ expect(onSelectionChange).toHaveBeenLastCalledWith({ rows: [0, targetRow], columns: ["name"], ranges: [{ rows: [0, targetRow], columns: ["name"] }] });
753
+ });
754
+
755
+ it("Ctrl+click adds a separate range and keeps the first, reporting both", async () => {
756
+ const user = userEvent.setup();
757
+ const onSelectionChange = vi.fn();
758
+ setup({ selectable: true, onSelectionChange });
759
+ await user.tab();
760
+ await user.keyboard("{ArrowDown}{Shift>}{ArrowDown}{/Shift}"); // Name, rows 0-1
761
+ await user.keyboard("{Control>}");
762
+ await user.click(cell(2, 1)); // ID, row 2
763
+ await user.keyboard("{/Control}");
764
+ expect(selectedCells()).toHaveLength(3);
765
+ expect(cell(0, 0)).toHaveAttribute("aria-selected", "true");
766
+ expect(cell(2, 1)).toHaveAttribute("aria-selected", "true");
767
+ expect(cell(2, 0)).toHaveAttribute("aria-selected", "false");
768
+ expect(onSelectionChange).toHaveBeenLastCalledWith({
769
+ rows: [2, 2],
770
+ columns: ["id"],
771
+ ranges: [
772
+ { rows: [0, 1], columns: ["name"] },
773
+ { rows: [2, 2], columns: ["id"] },
774
+ ],
775
+ });
776
+ expect(screen.getByRole("status")).toHaveTextContent("3 cells selected");
777
+ });
778
+
779
+ it("Ctrl+click with nothing selected keeps the active cell as the first range", async () => {
780
+ const user = userEvent.setup();
781
+ setup({ selectable: true });
782
+ await user.tab();
783
+ await user.keyboard("{ArrowDown}"); // active: row 0, Name — not selected yet
784
+ await user.keyboard("{Control>}");
785
+ await user.click(cell(2, 1));
786
+ await user.keyboard("{/Control}");
787
+ expect(selectedCells()).toHaveLength(2);
788
+ expect(cell(0, 0)).toHaveAttribute("aria-selected", "true");
789
+ });
790
+
791
+ it("Ctrl+Space is the keyboard way to add a range; Shift+arrows then extends the new one", async () => {
792
+ const user = userEvent.setup();
793
+ const onSelectionChange = vi.fn();
794
+ setup({ selectable: true, onSelectionChange });
795
+ await user.tab();
796
+ await user.keyboard("{ArrowDown}"); // row 0, Name
797
+ await user.keyboard("{Control>} {/Control}"); // range 1 = this cell
798
+ await user.keyboard("{ArrowDown}{ArrowDown}"); // plain arrows collapse everything...
799
+ expect(selectedCells()).toHaveLength(0);
800
+ await user.keyboard("{Control>} {/Control}"); // ...so start over at row 2
801
+ await user.keyboard("{ArrowUp}");
802
+ expect(selectedCells()).toHaveLength(0);
803
+ // build two ranges without collapsing: Shift+arrows extends, Ctrl+Space branches
804
+ await user.keyboard("{ArrowUp}{Shift>}{ArrowDown}{/Shift}"); // rows 0-1
805
+ await user.keyboard("{Control>} {/Control}"); // second range starts at the active cell (row 1)
806
+ await user.keyboard("{Shift>}{ArrowDown}{ArrowRight}{/Shift}"); // rows 1-2, Name+ID
807
+ expect(onSelectionChange).toHaveBeenLastCalledWith({
808
+ rows: [1, 2],
809
+ columns: ["name", "id"],
810
+ ranges: [
811
+ { rows: [0, 1], columns: ["name"] },
812
+ { rows: [1, 2], columns: ["name", "id"] },
813
+ ],
814
+ });
815
+ });
816
+
817
+ it("Ctrl+C copies every range, separated by a blank line", async () => {
818
+ const user = userEvent.setup();
819
+ const writeText = vi.fn().mockResolvedValue(undefined);
820
+ Object.defineProperty(navigator, "clipboard", { value: { writeText }, configurable: true });
821
+ setup({ selectable: true });
822
+ await user.tab();
823
+ await user.keyboard("{ArrowDown}"); // Cara
824
+ await user.keyboard("{Control>}");
825
+ await user.click(cell(2, 1)); // 3
826
+ await user.keyboard("{/Control}");
827
+ await user.keyboard("{Control>}c{/Control}");
828
+ expect(writeText).toHaveBeenCalledWith("Cara\n\n3");
829
+ expect(screen.getByRole("status")).toHaveTextContent("Copied 2 cells");
830
+ });
831
+
832
+ it("dragging across cells selects the rectangle; a plain click selects nothing", async () => {
833
+ const user = userEvent.setup();
834
+ const onSelectionChange = vi.fn();
835
+ setup({ selectable: true, onSelectionChange });
836
+ await user.pointer({ keys: "[MouseLeft>]", target: cell(0, 0) });
837
+ expect(selectedCells()).toHaveLength(0); // pressed but not moved
838
+ await user.pointer({ target: cell(1, 1) });
839
+ await user.pointer({ keys: "[/MouseLeft]" });
840
+ expect(selectedCells()).toHaveLength(4);
841
+ expect(onSelectionChange).toHaveBeenLastCalledWith({ rows: [0, 1], columns: ["name", "id"], ranges: [{ rows: [0, 1], columns: ["name", "id"] }] });
842
+ expect(cell(1, 1)).toHaveFocus(); // Shift+arrows continue from the end
843
+ });
844
+
845
+ it("moving over cells with no button held does not select", async () => {
846
+ const user = userEvent.setup();
847
+ setup({ selectable: true });
848
+ await user.pointer({ keys: "[MouseLeft>]", target: cell(0, 0) });
849
+ await user.pointer({ keys: "[/MouseLeft]" });
850
+ await user.pointer({ target: cell(2, 1) });
851
+ expect(selectedCells()).toHaveLength(0);
753
852
  });
754
853
 
755
854
  it("re-sorting clears the selection (it refers to displayed rows)", async () => {
package/src/index.ts CHANGED
@@ -372,7 +372,7 @@ export { MultiSelect, type MultiSelectOption, type MultiSelectProps } from "./co
372
372
  export { MessageBubble, TypingIndicator, type MessageBubbleProps } from "./components/message-bubble";
373
373
  export { Tour, type TourStep, type TourProps } from "./components/tour";
374
374
  export { VirtualList, type VirtualListProps } from "./components/virtual-list";
375
- export { DataGrid, type DataGridColumn, type DataGridProps } from "./components/data-grid";
375
+ export { DataGrid, type DataGridColumn, type DataGridProps, type DataGridRange, type DataGridSelection } from "./components/data-grid";
376
376
  export { JsonViewer, type JsonViewerProps } from "./components/json-viewer";
377
377
  export { DiffViewer, type DiffViewerProps } from "./components/diff-viewer";
378
378
  export { ColorPicker, type ColorPickerProps } from "./components/color-picker";