@chassis-ui/react 0.0.0-20260831090525 → 0.1.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.
Files changed (106) hide show
  1. package/README.md +11 -0
  2. package/dist/index.d.ts +682 -237
  3. package/dist/index.js +2 -2
  4. package/dist/style.css +1 -1
  5. package/package.json +53 -46
  6. package/src/components/accordion/Accordion.tsx +2 -2
  7. package/src/components/accordion/AccordionItem.tsx +6 -6
  8. package/src/components/autocomplete/Autocomplete.tsx +4 -4
  9. package/src/components/avatar/Avatar.tsx +5 -2
  10. package/src/components/breadcrumb/Breadcrumb.tsx +11 -2
  11. package/src/components/button/Button.tsx +7 -4
  12. package/src/components/button-group/ButtonGroup.tsx +2 -2
  13. package/src/components/calendar/Calendar.tsx +56 -42
  14. package/src/components/calendar/CalendarMonthGrid.tsx +3 -1
  15. package/src/components/calendar/CalendarMonthYearPicker.tsx +4 -2
  16. package/src/components/calendar/CalendarYearGrid.tsx +5 -3
  17. package/src/components/calendar/RangeCalendar.scss +8 -8
  18. package/src/components/calendar/RangeCalendar.tsx +56 -44
  19. package/src/components/calendar/index.ts +1 -0
  20. package/src/components/calendar/labels.tsx +71 -0
  21. package/src/components/card/Card.tsx +4 -4
  22. package/src/components/card/CardGroup.tsx +1 -1
  23. package/src/components/card/CardImage.tsx +1 -1
  24. package/src/components/carousel/Carousel.tsx +20 -9
  25. package/src/components/carousel/CarouselControlButton.tsx +1 -1
  26. package/src/components/carousel/CarouselPlayPause.tsx +1 -1
  27. package/src/components/carousel/carouselEngine.ts +3 -3
  28. package/src/components/checkbox/Checkbox.tsx +12 -11
  29. package/src/components/checkbox/CheckboxGroup.tsx +1 -1
  30. package/src/components/chip/Chip.tsx +5 -4
  31. package/src/components/chip-input/ChipInput.tsx +12 -4
  32. package/src/components/chip-input/ChipList.tsx +2 -2
  33. package/src/components/close-button/CloseButton.tsx +2 -2
  34. package/src/components/color-input/ColorInput.tsx +2 -2
  35. package/src/components/combobox/Combobox.tsx +2 -2
  36. package/src/components/datagrid/DataGrid.scss +203 -0
  37. package/src/components/datagrid/DataGrid.tsx +259 -0
  38. package/src/components/datagrid/DataGridBody.tsx +162 -0
  39. package/src/components/datagrid/DataGridCell.tsx +47 -0
  40. package/src/components/datagrid/DataGridColumn.tsx +109 -0
  41. package/src/components/datagrid/DataGridHeader.tsx +47 -0
  42. package/src/components/datagrid/DataGridPinBehavior.tsx +225 -0
  43. package/src/components/datagrid/DataGridRow.tsx +48 -0
  44. package/src/components/datagrid/DataGridSelectionCell.tsx +74 -0
  45. package/src/components/datagrid/DataGridTableLayout.ts +30 -0
  46. package/src/components/datagrid/index.ts +14 -0
  47. package/src/components/datagrid/useColumnGeometry.ts +161 -0
  48. package/src/components/datepicker/ClearButton.tsx +4 -1
  49. package/src/components/datepicker/DatePicker.scss +2 -2
  50. package/src/components/datepicker/DatePicker.tsx +179 -147
  51. package/src/components/datepicker/DateRangePicker.tsx +107 -89
  52. package/src/components/datepicker/renderDatePickerShell.tsx +1 -1
  53. package/src/components/drawer/Drawer.tsx +3 -1
  54. package/src/components/file-input/FileInput.tsx +2 -2
  55. package/src/components/floating-input/FloatingInput.tsx +8 -8
  56. package/src/components/form/renderFormCheck.tsx +3 -3
  57. package/src/components/form-field/FormField.tsx +8 -8
  58. package/src/components/form-field/renderFormField.tsx +14 -3
  59. package/src/components/grid/Col.tsx +1 -1
  60. package/src/components/grid/GridItem.tsx +1 -1
  61. package/src/components/grid/Row.tsx +1 -1
  62. package/src/components/input-group/InputGroup.tsx +2 -2
  63. package/src/components/list/List.tsx +5 -5
  64. package/src/components/menu/Menu.tsx +10 -0
  65. package/src/components/menu/MenuSubmenu.tsx +3 -3
  66. package/src/components/menu/MenuSubmenuBack.tsx +1 -1
  67. package/src/components/menu/MenuToggle.tsx +2 -2
  68. package/src/components/modal/Modal.tsx +5 -3
  69. package/src/components/navbar/Navbar.tsx +1 -1
  70. package/src/components/navbar/NavbarBrand.tsx +5 -2
  71. package/src/components/notification/NotificationStack.tsx +1 -1
  72. package/src/components/otp-input/OtpBox.tsx +1 -1
  73. package/src/components/otp-input/OtpInput.tsx +12 -11
  74. package/src/components/pagination/Pagination.tsx +2 -2
  75. package/src/components/pagination/PaginationItem.tsx +23 -4
  76. package/src/components/popover/Popover.tsx +40 -14
  77. package/src/components/progress/Progress.tsx +1 -1
  78. package/src/components/progress/ProgressBar.tsx +1 -1
  79. package/src/components/radio/Radio.tsx +2 -2
  80. package/src/components/radio/RadioGroup.tsx +1 -1
  81. package/src/components/select/Select.tsx +8 -8
  82. package/src/components/skeleton/Skeleton.tsx +1 -1
  83. package/src/components/spinner/Spinner.tsx +2 -2
  84. package/src/components/stepper/Stepper.tsx +5 -5
  85. package/src/components/switch/Switch.tsx +3 -3
  86. package/src/components/table/Table.css +2 -2
  87. package/src/components/table/Table.tsx +12 -6
  88. package/src/components/text-input/TextInput.tsx +2 -2
  89. package/src/components/textarea/Textarea.tsx +2 -2
  90. package/src/components/toast/ToastBody.tsx +1 -1
  91. package/src/components/toast/ToastHeader.tsx +2 -2
  92. package/src/components/toast/Toaster.tsx +1 -1
  93. package/src/components/tooltip/Tooltip.tsx +23 -7
  94. package/src/hooks/useAutoDismiss.ts +4 -1
  95. package/src/hooks/useDialogElement.ts +10 -0
  96. package/src/hooks/useFloatingOverlay.ts +12 -2
  97. package/src/hooks/usePagination.ts +6 -2
  98. package/src/index.ts +226 -0
  99. package/src/types.ts +26 -26
  100. package/src/utils/breakpoints.ts +1 -1
  101. package/src/utils/devWarning.ts +58 -0
  102. package/src/utils/direction.ts +14 -0
  103. package/src/utils/focusRedirect.ts +10 -16
  104. package/src/utils/polymorphic.ts +18 -1
  105. package/src/utils/suppressFocusRingGlobally.ts +22 -7
  106. package/src/utils/triggerElement.ts +25 -0
@@ -0,0 +1,109 @@
1
+ import React, { ReactNode } from 'react'
2
+ import classNames from 'classnames'
3
+ import { Key, Column as AriaColumn } from 'react-aria-components'
4
+ import { ColumnSize, ColumnStaticSize } from 'react-stately'
5
+
6
+ export interface DataGridColumnProps {
7
+ /**
8
+ * Whether the column allows sorting. Adds a sort indicator and makes the header
9
+ * clickable/keyboard-activatable.
10
+ */
11
+ allowsSorting?: boolean
12
+ /**
13
+ * Rendered contents of the column header.
14
+ */
15
+ children: ReactNode
16
+ /**
17
+ * A string of all className you want applied to the column header.
18
+ */
19
+ className?: string
20
+ /**
21
+ * The initial width of the column, e.g. for a narrow selection/icon column — a plain number or
22
+ * `'<n>%'` for a fixed size, or `'<n>fr'` to share remaining space with other fractional
23
+ * columns in proportion (columns default to `'1fr'`, so they split space evenly unless given
24
+ * one of these). Unlike `Table`, which sizes a narrow column via `width: 1%` relying on native
25
+ * `<table>` auto-layout, `DataGrid`'s virtualized layout computes every column's width itself —
26
+ * a CSS `width` rule on the cell has no effect, so this prop is the only way to size a column.
27
+ */
28
+ defaultWidth?: ColumnSize | null
29
+ /**
30
+ * The unique id of the column, referenced by `sortDescriptor.column`.
31
+ */
32
+ id?: Key
33
+ /**
34
+ * Whether this column is a row header, announced by assistive technology during row
35
+ * navigation.
36
+ */
37
+ isRowHeader?: boolean
38
+ /**
39
+ * The maximum width of the column, as a fixed pixel number or `'<n>%'`.
40
+ */
41
+ maxWidth?: ColumnStaticSize | null
42
+ /**
43
+ * The minimum width of the column, as a fixed pixel number or `'<n>%'`.
44
+ */
45
+ minWidth?: ColumnStaticSize | null
46
+ /**
47
+ * Pin the column so it stays visible while the grid scrolls horizontally: `'start'` pins it to
48
+ * the leading edge, `'end'` to the trailing edge. Stacks correctly with other pinned columns on
49
+ * the same side, in declaration order.
50
+ */
51
+ pin?: 'start' | 'end'
52
+ /**
53
+ * A string representation of the column header, used for accessibility announcements. Defaults
54
+ * to `children` when it's a plain string — set this explicitly when the header contains
55
+ * anything else (an icon, a `Tooltip`, etc.).
56
+ */
57
+ textValue?: string
58
+ }
59
+
60
+ /**
61
+ * A column within `DataGrid`. Unlike `Table`'s sub-parts, this renders directly — it's a thin
62
+ * wrapper over react-aria-components' own `Column`.
63
+ *
64
+ * When `allowsSorting` is set, renders a sort indicator after `children` — mirrors `Table`'s own
65
+ * `TableColumnHeader`, but reads the active direction from react-aria-components' own
66
+ * `ColumnRenderProps` (via the `children` render-prop form) instead of reaching into table state
67
+ * directly, since this wrapper has no state of its own to read.
68
+ */
69
+ export const DataGridColumn = ({
70
+ allowsSorting,
71
+ children,
72
+ className,
73
+ defaultWidth,
74
+ id,
75
+ isRowHeader,
76
+ maxWidth,
77
+ minWidth,
78
+ pin,
79
+ textValue
80
+ }: DataGridColumnProps) => (
81
+ <AriaColumn
82
+ allowsSorting={allowsSorting}
83
+ className={classNames('datagrid-col', pin && `datagrid-col-pinned-${pin}`, className)}
84
+ defaultWidth={defaultWidth}
85
+ id={id}
86
+ isRowHeader={isRowHeader}
87
+ maxWidth={maxWidth}
88
+ minWidth={minWidth}
89
+ textValue={textValue}
90
+ // `pin` isn't a react-aria-components `Column` prop, but its collection builder preserves
91
+ // the full props object it's given verbatim (see react-stately's `Column.getCollectionNode`)
92
+ // — passing it through here is what makes it reach `column.props.pin` in
93
+ // `DataGridTableLayout`'s `isStickyColumn` override, which is what positions `pin="start"`.
94
+ // `pin="end"` is handled separately by `DataGridPinBehavior`, which reads the same prop off
95
+ // the header's JSX rather than the resolved collection.
96
+ {...({ pin } as Record<string, unknown>)}
97
+ >
98
+ {allowsSorting
99
+ ? ({ sortDirection }) => (
100
+ <>
101
+ {children}
102
+ <span aria-hidden="true" className="datagrid-sort-icon">
103
+ {sortDirection === 'ascending' ? '▲' : sortDirection === 'descending' ? '▼' : '⇅'}
104
+ </span>
105
+ </>
106
+ )
107
+ : children}
108
+ </AriaColumn>
109
+ )
@@ -0,0 +1,47 @@
1
+ import React, { ReactElement, ReactNode } from 'react'
2
+ import classNames from 'classnames'
3
+ import { TableHeader as AriaTableHeader } from 'react-aria-components'
4
+
5
+ export interface DataGridHeaderProps<T> {
6
+ /**
7
+ * `DataGridColumn` elements, or a render function paired with `columns` for dynamic column
8
+ * generation.
9
+ */
10
+ children: ReactNode | ((column: T) => ReactElement)
11
+ /**
12
+ * A string of all className you want applied to the header row group.
13
+ */
14
+ className?: string
15
+ /**
16
+ * A list of column data objects, rendered via the function form of `children`.
17
+ */
18
+ columns?: Iterable<T>
19
+ /**
20
+ * Values that should invalidate the column cache when using the `columns`/render-function form
21
+ * of `children` — see `DataGridBody`'s `dependencies` for why this matters.
22
+ */
23
+ dependencies?: ReadonlyArray<unknown>
24
+ }
25
+
26
+ /**
27
+ * Header row group for `DataGrid`, containing `DataGridColumn`s. Unlike `Table`'s sub-parts,
28
+ * this renders directly — it's a thin wrapper over react-aria-components' own `TableHeader`.
29
+ *
30
+ * Always carries the `datagrid-header` class (not just the caller's `className`) — `DataGrid.scss`
31
+ * uses it to tell header cells from body cells, since react-aria-components renders both as plain
32
+ * `[role="rowgroup"]` divs with no structural difference to select on otherwise.
33
+ */
34
+ export const DataGridHeader = <T extends object>({
35
+ children,
36
+ className,
37
+ columns,
38
+ dependencies
39
+ }: DataGridHeaderProps<T>) => (
40
+ <AriaTableHeader
41
+ className={classNames('datagrid-header', className)}
42
+ columns={columns}
43
+ dependencies={dependencies}
44
+ >
45
+ {children}
46
+ </AriaTableHeader>
47
+ )
@@ -0,0 +1,225 @@
1
+ import React, { ReactElement, RefObject, useEffect, useMemo } from 'react'
2
+
3
+ import { DataGridColumnProps } from './DataGridColumn'
4
+ import { DataGridHeaderProps } from './DataGridHeader'
5
+ import { ColumnGeometryColumn, useColumnGeometry } from './useColumnGeometry'
6
+
7
+ export interface DataGridPinBehaviorProps<T> {
8
+ /**
9
+ * The same header element passed as `DataGrid`'s first child. Read directly (rather than via
10
+ * `TableStateContext`) because react-aria-components' virtualized `<Table>` only ever renders
11
+ * DOM output for its own collection-derived tree — an arbitrary extra child (like this
12
+ * component, if it were nested inside `<Table>`) never mounts, regardless of context
13
+ * availability. Only the static-children form of `DataGridHeader` can be introspected this
14
+ * way; the `columns`/render-function form gets no pinned-column behavior from here (`pin=
15
+ * "start"`'s positioning still works either way — see `DataGridTableLayout`, which reads pin
16
+ * state off the resolved collection instead, not off this introspection).
17
+ */
18
+ header: ReactElement<DataGridHeaderProps<T>>
19
+ /**
20
+ * A CSS class unique to this `DataGrid` instance, scoping the generated rules below so
21
+ * multiple grids on the same page (with different pinned columns) don't collide.
22
+ */
23
+ instanceClassName: string
24
+ /**
25
+ * Ref to `DataGrid`'s own scrolling root — used both to measure `fr`-sized pinned columns via
26
+ * `useColumnGeometry` and, for `pin="end"`, to find and reposition its cells on scroll (see
27
+ * below).
28
+ */
29
+ containerRef: RefObject<HTMLElement | null>
30
+ }
31
+
32
+ interface PinnedColumnInfo {
33
+ index: number
34
+ /** Cumulative width of the end-pinned columns after this one — its distance from the edge. */
35
+ rightOffset: number
36
+ }
37
+
38
+ interface DeclaredColumns {
39
+ all: ColumnGeometryColumn[]
40
+ endIndices: number[]
41
+ startIndices: number[]
42
+ }
43
+
44
+ /**
45
+ * A cell's (1-based) `aria-colindex` for the (0-based) column position it was declared at.
46
+ *
47
+ * Every cell and column header react-aria-components renders carries `aria-colindex`, set from the
48
+ * column's position in the resolved collection — which is the declaration order this component
49
+ * reads off the header's JSX. That makes it the one stable handle from a declared column back to
50
+ * its rendered DOM, and the reason nothing here counts DOM children: the layout engine windows
51
+ * columns horizontally as well as rows (only the cells intersecting the horizontal viewport are
52
+ * mounted, see `datagrid.mdx`'s Scope section), so a row's child count is the size of the mounted
53
+ * window, not the column count. The `nth-child`/`nth-last-child` arithmetic this used to do was
54
+ * therefore only correct while every column happened to be mounted, and silently moved and
55
+ * shadowed the wrong cells in exactly the wide grids pinning exists for.
56
+ */
57
+ const colIndexSelector = (index: number) => `[aria-colindex="${index + 1}"]`
58
+
59
+ /**
60
+ * Handles the two things `DataGrid`'s own layout subclass (`DataGridTableLayout`) can't:
61
+ *
62
+ * - `pin="end"`'s positioning. `DataGridTableLayout` reuses the layout engine's own sticky
63
+ * mechanism for `pin="start"`, but that mechanism is hardcoded to the leading edge — there's no
64
+ * upstream hook for a trailing-edge pin. A pure-CSS `position: sticky; right: 0` equivalent
65
+ * doesn't work either: for the *last* child of a horizontally-scrolling container, the browser's
66
+ * own max-scroll bound is defined such that its gap to the viewport's trailing edge never goes
67
+ * negative until the very last pixel of scroll — sticky has nothing to correct until then, so it
68
+ * renders exactly like `position: absolute` for the entire scroll range up to that point (this is
69
+ * a well-known, verified-here CSS limitation, not specific to this layout). So `pin="end"` is
70
+ * positioned imperatively instead: on every scroll (and on mount/resize), each `pin="end"`
71
+ * column's auto-generated wrapper div gets a `transform: translateX(...)` that keeps it flush
72
+ * against the scroll viewport's trailing edge, computed from `useColumnGeometry`'s cumulative
73
+ * offsets, plus the `z-index` that keeps it above the cells it now overlaps. This bypasses React
74
+ * for those two styles, similar to how other virtualized grids (e.g. AG-Grid) handle right-pinned
75
+ * columns — react-aria-components' own layout doesn't re-render an already-mounted cell's style on
76
+ * a plain scroll (its `layoutInfo` doesn't depend on scroll position), so there's no fight between
77
+ * this and React's own reconciliation; newly-mounted rows (from vertical scroll bringing fresh
78
+ * cells into the virtualized window) are covered by the same scroll-driven sync running again.
79
+ * - The scroll-cue box-shadow on the boundary column of each pinned group (the last `pin="start"`
80
+ * column, the first `pin="end"` column) — shown only once the grid has actually scrolled far
81
+ * enough to hide content under it, via the `datagrid-scrolled-start`/`datagrid-scrolled-end`
82
+ * classes `DataGrid` toggles on scroll. This part stays pure CSS since box-shadow, unlike
83
+ * sticky's position offsets, has no "last child" gotcha.
84
+ *
85
+ * Rendered as a plain sibling of `DataGrid`'s `<Table>`, not inside it.
86
+ */
87
+ export const DataGridPinBehavior = <T extends object>({
88
+ header,
89
+ instanceClassName,
90
+ containerRef
91
+ }: DataGridPinBehaviorProps<T>) => {
92
+ const headerChildren = header.props.children
93
+
94
+ // Re-derived only when a column's own sizing/pin props actually change, not on every parent
95
+ // render: `useColumnGeometry` keys its memoized result off this array's identity, and the
96
+ // scroll-sync effect below keys off that result in turn — rebuilding it unconditionally tore
97
+ // down and re-registered the effect's scroll listener and its two observers on every single
98
+ // `DataGrid` render.
99
+ const columnElements = useMemo(
100
+ () => (typeof headerChildren === 'function' ? [] : React.Children.toArray(headerChildren)),
101
+ [headerChildren]
102
+ )
103
+ const declaredKey = columnElements
104
+ .map((element) => {
105
+ if (!React.isValidElement<DataGridColumnProps>(element)) return ''
106
+ const { defaultWidth, maxWidth, minWidth, pin } = element.props
107
+ return `${defaultWidth ?? ''}/${minWidth ?? ''}/${maxWidth ?? ''}/${pin ?? ''}`
108
+ })
109
+ .join('|')
110
+
111
+ const {
112
+ all: allColumns,
113
+ endIndices,
114
+ startIndices
115
+ } = useMemo<DeclaredColumns>(() => {
116
+ const declared: DeclaredColumns = { all: [], endIndices: [], startIndices: [] }
117
+ columnElements.forEach((element, index) => {
118
+ if (!React.isValidElement<DataGridColumnProps>(element)) return
119
+ const { defaultWidth, maxWidth, minWidth, pin } = element.props
120
+ declared.all.push({ id: index, defaultWidth, maxWidth, minWidth })
121
+ if (pin === 'start') declared.startIndices.push(index)
122
+ else if (pin === 'end') declared.endIndices.push(index)
123
+ })
124
+ return declared
125
+ // `declaredKey` is the value-based identity of `columnElements` — a fresh JSX array every
126
+ // render, so its own reference can't be the dependency.
127
+ // eslint-disable-next-line react-hooks/exhaustive-deps
128
+ }, [declaredKey])
129
+
130
+ const offsets = useColumnGeometry(allColumns, containerRef)
131
+
132
+ // Cumulative width of the end-pinned columns after this one, keyed by column index — how far
133
+ // from the viewport's trailing edge each one needs to sit so they stack correctly.
134
+ const endPinnedInfo = useMemo(() => {
135
+ const info = new Map<number, PinnedColumnInfo>()
136
+ let right = 0
137
+ for (let i = endIndices.length - 1; i >= 0; i--) {
138
+ const index = endIndices[i]
139
+ if (index == null) continue
140
+ const width = offsets.get(index)?.width ?? 0
141
+ info.set(index, { index, rightOffset: right })
142
+ right += width
143
+ }
144
+ return info
145
+ }, [endIndices, offsets])
146
+
147
+ useEffect(() => {
148
+ const container = containerRef.current
149
+ if (!container || endPinnedInfo.size === 0) return
150
+
151
+ let frame: number | null = null
152
+ // The wrappers this effect has written inline styles onto, so they can be handed back
153
+ // unstyled when it tears down — un-pinning a column at runtime (or unmounting this behavior
154
+ // while the grid stays) otherwise left its cells stuck at their last scroll offset.
155
+ const styled = new Set<HTMLElement>()
156
+
157
+ const sync = () => {
158
+ frame = null
159
+ const clientWidth = container.clientWidth
160
+ const scrollLeft = container.scrollLeft
161
+ const rows = container.querySelectorAll<HTMLElement>('[role="row"]')
162
+ rows.forEach((row) => {
163
+ endPinnedInfo.forEach(({ index, rightOffset }) => {
164
+ const cell = row.querySelector<HTMLElement>(colIndexSelector(index))
165
+ const wrapper = cell?.parentElement
166
+ if (!wrapper) return
167
+ const offset = offsets.get(index)
168
+ if (!offset) return
169
+ const rightEdge = offset.offset + offset.width
170
+ const delta = clientWidth - rightOffset - rightEdge + scrollLeft
171
+ wrapper.style.transform = `translateX(${delta}px)`
172
+ // Above the unpinned cells it now overlaps. Set here rather than in the generated
173
+ // stylesheet below because the virtualizer writes the wrapper's own `z-index` inline,
174
+ // which no plain rule can outrank without `!important`.
175
+ wrapper.style.zIndex = '2'
176
+ styled.add(wrapper)
177
+ })
178
+ })
179
+ }
180
+
181
+ const scheduleSync = () => {
182
+ if (frame == null) frame = requestAnimationFrame(sync)
183
+ }
184
+
185
+ scheduleSync()
186
+ container.addEventListener('scroll', scheduleSync, { passive: true })
187
+ const resizeObserver =
188
+ typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(scheduleSync)
189
+ resizeObserver?.observe(container)
190
+ const mutationObserver =
191
+ typeof MutationObserver === 'undefined' ? null : new MutationObserver(scheduleSync)
192
+ mutationObserver?.observe(container, { childList: true, subtree: true })
193
+
194
+ return () => {
195
+ if (frame != null) cancelAnimationFrame(frame)
196
+ container.removeEventListener('scroll', scheduleSync)
197
+ resizeObserver?.disconnect()
198
+ mutationObserver?.disconnect()
199
+ styled.forEach((wrapper) => {
200
+ wrapper.style.transform = ''
201
+ wrapper.style.zIndex = ''
202
+ })
203
+ }
204
+ }, [containerRef, endPinnedInfo, offsets])
205
+
206
+ if (startIndices.length === 0 && endIndices.length === 0) return null
207
+
208
+ const rules: string[] = []
209
+
210
+ if (startIndices.length > 0) {
211
+ const lastStartIndex = Math.max(...startIndices)
212
+ rules.push(
213
+ `.${instanceClassName}.datagrid-scrolled-start ${colIndexSelector(lastStartIndex)} { box-shadow: var(--datagrid-pin-cue-shadow-start); }`
214
+ )
215
+ }
216
+
217
+ if (endIndices.length > 0) {
218
+ const firstEndIndex = Math.min(...endIndices)
219
+ rules.push(
220
+ `.${instanceClassName}.datagrid-scrolled-end ${colIndexSelector(firstEndIndex)} { box-shadow: var(--datagrid-pin-cue-shadow-end); }`
221
+ )
222
+ }
223
+
224
+ return <style>{rules.join('\n')}</style>
225
+ }
@@ -0,0 +1,48 @@
1
+ import React, { ReactElement, ReactNode } from 'react'
2
+ import classNames from 'classnames'
3
+ import { Key, Row as AriaRow } from 'react-aria-components'
4
+
5
+ export interface DataGridRowProps<T> {
6
+ /**
7
+ * `DataGridCell` elements, or a render function called once per column with that column's
8
+ * data — required when the row's parent `DataGridBody` uses the `items`/render-function form.
9
+ */
10
+ children: ReactNode | ((column: T) => ReactElement)
11
+ /**
12
+ * A string of all className you want applied to the row.
13
+ */
14
+ className?: string
15
+ /**
16
+ * A list of column data objects, rendered via the function form of `children`.
17
+ */
18
+ columns?: Iterable<T>
19
+ /**
20
+ * The unique id of the row, referenced by `selectedKeys`/`disabledKeys`.
21
+ */
22
+ id?: Key
23
+ /**
24
+ * A string representation of the row's contents, used for typeahead.
25
+ */
26
+ textValue?: string
27
+ }
28
+
29
+ /**
30
+ * A row within `DataGrid`. Unlike `Table`'s sub-parts, this renders directly — it's a thin
31
+ * wrapper over react-aria-components' own `Row`.
32
+ */
33
+ export const DataGridRow = <T extends object>({
34
+ children,
35
+ className,
36
+ columns,
37
+ id,
38
+ textValue
39
+ }: DataGridRowProps<T>) => (
40
+ <AriaRow
41
+ className={classNames('datagrid-row', className)}
42
+ columns={columns}
43
+ id={id}
44
+ textValue={textValue}
45
+ >
46
+ {children}
47
+ </AriaRow>
48
+ )
@@ -0,0 +1,74 @@
1
+ import React, { useRef } from 'react'
2
+ import { AriaCheckboxProps, useCheckbox } from 'react-aria'
3
+ import { useToggleState } from 'react-stately'
4
+ import { CheckboxContext, CheckboxProps, useSlottedContext } from 'react-aria-components'
5
+
6
+ export interface DataGridSelectionCellProps {
7
+ /**
8
+ * A string of all className you want applied to the checkbox, replacing the default
9
+ * `check-input`.
10
+ */
11
+ className?: string
12
+ }
13
+
14
+ /**
15
+ * Reads the selection checkbox props react-aria-components' own `Row`/`TableHeader` publish on
16
+ * `CheckboxContext`'s `selection` slot — the same ones its `<Checkbox slot="selection">` would
17
+ * consume, already carrying the localized label, the row-header `aria-labelledby`, and the
18
+ * selected/disabled/indeterminate state.
19
+ *
20
+ * Reading them off that context (rather than calling react-aria's `useTableSelectionCheckbox`/
21
+ * `useTableSelectAllCheckbox` against `TableStateContext` ourselves, as this file used to) is what
22
+ * keeps this working when more than one copy of `react-aria` is installed. Those hooks resolve the
23
+ * grid's id through a module-scoped `WeakMap` that only `useTable` populates, in whichever copy
24
+ * ran it — always the copy bundled inside `react-aria-components` here, since its `Table` is what
25
+ * calls `useTable`. `react-aria-components` pins `react-aria` to an exact version, so a consumer
26
+ * whose own `react-aria` resolves anywhere else in our `^` range gets a second copy and the lookup
27
+ * throws `Error: Unknown grid` on every selection cell. Letting react-aria-components compute the
28
+ * props in its own copy sidesteps that entirely, and drops the duplicated `rowKey` this component
29
+ * used to need — the context is already scoped to the row it's rendered in, so a `rowKey` that
30
+ * disagreed with the surrounding `DataGridRow` can no longer silently toggle a different row.
31
+ */
32
+ const useSelectionCheckboxProps = (component: string): AriaCheckboxProps => {
33
+ const props = useSlottedContext(CheckboxContext, 'selection') as CheckboxProps | null | undefined
34
+ if (!props) {
35
+ throw new Error(`${component} must be rendered inside a DataGrid with selection enabled.`)
36
+ }
37
+ return props as AriaCheckboxProps
38
+ }
39
+
40
+ /**
41
+ * Selection checkbox for one `DataGrid` row — place inside a `DataGridCell`.
42
+ *
43
+ * react-aria-components' own `Checkbox` always visually hides its native `<input>` behind a custom
44
+ * indicator the caller must supply. This renders a plain native `<input className="check-input">`
45
+ * instead, wired to the same state, so it picks up chassis-css's existing checkbox styling for
46
+ * free with no new CSS of its own.
47
+ */
48
+ export const DataGridSelectionCell = ({ className }: DataGridSelectionCellProps) => {
49
+ const checkboxProps = useSelectionCheckboxProps('DataGridSelectionCell')
50
+ return <DataGridCheckboxInput checkboxProps={checkboxProps} className={className} />
51
+ }
52
+
53
+ /**
54
+ * "Select all" checkbox for `DataGrid`'s header row — place inside a `DataGridColumn`. See
55
+ * `DataGridSelectionCell` for why this bypasses react-aria-components' own `Checkbox`.
56
+ */
57
+ export const DataGridSelectAllCell = ({ className }: DataGridSelectionCellProps) => {
58
+ const checkboxProps = useSelectionCheckboxProps('DataGridSelectAllCell')
59
+ return <DataGridCheckboxInput checkboxProps={checkboxProps} className={className} />
60
+ }
61
+
62
+ const DataGridCheckboxInput = ({
63
+ checkboxProps,
64
+ className
65
+ }: {
66
+ checkboxProps: AriaCheckboxProps
67
+ className?: string
68
+ }) => {
69
+ const toggleState = useToggleState(checkboxProps)
70
+ const ref = useRef<HTMLInputElement>(null)
71
+ const { inputProps } = useCheckbox(checkboxProps, toggleState, ref)
72
+
73
+ return <input {...inputProps} className={className ?? 'check-input'} ref={ref} />
74
+ }
@@ -0,0 +1,30 @@
1
+ import { TableLayout } from 'react-aria-components'
2
+
3
+ interface StickyColumnNode {
4
+ type: string
5
+ index: number
6
+ colIndex?: number | null
7
+ }
8
+
9
+ /**
10
+ * `DataGrid`'s own subclass of react-aria-components' `TableLayout`, overriding the (protected,
11
+ * hardcoded-`false` upstream) `isStickyColumn` hook so a `pin="start"` `DataGridColumn` uses the
12
+ * layout engine's own sticky-positioning machinery — the same mechanism that already makes the
13
+ * header row pin to the top (`buildTableHeader` sets `layoutInfo.isSticky = true` unconditionally
14
+ * there). The subclass exists instead of a pure-CSS `position: sticky` override because
15
+ * react-aria-components' own `TableLayout` never overrides `isStickyColumn`, so no column becomes
16
+ * visually sticky by default — and because the engine, not CSS, owns each cell wrapper's `left`.
17
+ *
18
+ * `pin="end"` isn't handled here — the engine's sticky positioning is hardcoded to the leading
19
+ * edge (`position: sticky; left: ...`, mirrored to `right` only for RTL *direction*, never per
20
+ * column), so there's no upstream mechanism to reuse for a trailing pin in an LTR grid. See
21
+ * `DataGrid.scss`'s `nth-last-child`-based rules for that half instead.
22
+ */
23
+ export class DataGridTableLayout<T> extends TableLayout<T> {
24
+ protected isStickyColumn(node: StickyColumnNode): boolean {
25
+ const colIndex = node.type === 'column' ? node.index : (node.colIndex ?? node.index)
26
+ if (colIndex == null) return false
27
+ const column = this.collection.columns[colIndex]
28
+ return (column?.props as { pin?: 'start' | 'end' } | undefined)?.pin === 'start'
29
+ }
30
+ }
@@ -0,0 +1,14 @@
1
+ export { DataGrid } from './DataGrid'
2
+ export type { DataGridProps } from './DataGrid'
3
+ export { DataGridBody } from './DataGridBody'
4
+ export type { DataGridBodyProps } from './DataGridBody'
5
+ export { DataGridCell } from './DataGridCell'
6
+ export type { DataGridCellProps } from './DataGridCell'
7
+ export { DataGridColumn } from './DataGridColumn'
8
+ export type { DataGridColumnProps } from './DataGridColumn'
9
+ export { DataGridHeader } from './DataGridHeader'
10
+ export type { DataGridHeaderProps } from './DataGridHeader'
11
+ export { DataGridRow } from './DataGridRow'
12
+ export type { DataGridRowProps } from './DataGridRow'
13
+ export { DataGridSelectAllCell, DataGridSelectionCell } from './DataGridSelectionCell'
14
+ export type { DataGridSelectionCellProps } from './DataGridSelectionCell'