@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 +11 -5
- package/dist/index.js +80 -25
- package/package.json +2 -2
- package/src/components/data-grid.tsx +99 -30
- package/src/components-keyboard.test.tsx +101 -2
- package/src/index.ts +1 -1
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
|
|
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
|
|
1642
|
-
* Ctrl/Cmd+
|
|
1643
|
-
* column's `value()`; empty where
|
|
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
|
|
5162
|
-
if (!selectable
|
|
5163
|
-
const
|
|
5164
|
-
const
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
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
|
|
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 ? `${
|
|
5177
|
-
}, [selectable,
|
|
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 (!
|
|
5190
|
-
const
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
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 ${
|
|
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" &&
|
|
5312
|
+
if (mod && e.key.toLowerCase() === "c" && hasSelection) {
|
|
5281
5313
|
e.preventDefault();
|
|
5282
5314
|
return copySelection();
|
|
5283
5315
|
}
|
|
5284
|
-
if (e.key === "
|
|
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
|
|
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 (
|
|
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 &&
|
|
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 (
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
46
|
-
* Ctrl/Cmd+
|
|
47
|
-
* column's `value()`; empty where
|
|
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
|
-
|
|
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
|
|
241
|
-
if (!selectable
|
|
242
|
-
const
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
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(() =>
|
|
279
|
+
React.useEffect(() => {
|
|
280
|
+
setSel(null);
|
|
281
|
+
setExtra([]);
|
|
282
|
+
}, [sort, rowCount]);
|
|
251
283
|
|
|
252
284
|
React.useEffect(() => {
|
|
253
285
|
if (!selectable) return;
|
|
254
|
-
const
|
|
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 ? `${
|
|
292
|
+
setAnnouncement(value ? `${cellCount} cells selected` : "Selection cleared");
|
|
260
293
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
261
|
-
}, [selectable,
|
|
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 (!
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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 ${
|
|
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" &&
|
|
425
|
+
if (mod && e.key.toLowerCase() === "c" && hasSelection) {
|
|
381
426
|
e.preventDefault();
|
|
382
427
|
return copySelection();
|
|
383
428
|
}
|
|
384
|
-
if (e.key === "
|
|
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
|
|
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 (
|
|
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 &&
|
|
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 (
|
|
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";
|