@chassis-ui/react 0.1.0 → 0.1.2
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/README.md +11 -0
- package/dist/index.d.ts +682 -237
- package/dist/index.js +2 -2
- package/dist/style.css +1 -1
- package/package.json +32 -25
- package/src/components/accordion/Accordion.tsx +2 -2
- package/src/components/accordion/AccordionItem.tsx +6 -6
- package/src/components/autocomplete/Autocomplete.tsx +4 -4
- package/src/components/avatar/Avatar.tsx +5 -2
- package/src/components/breadcrumb/Breadcrumb.tsx +11 -2
- package/src/components/button/Button.tsx +7 -4
- package/src/components/button-group/ButtonGroup.tsx +2 -2
- package/src/components/calendar/Calendar.tsx +56 -42
- package/src/components/calendar/CalendarMonthGrid.tsx +3 -1
- package/src/components/calendar/CalendarMonthYearPicker.tsx +4 -2
- package/src/components/calendar/CalendarYearGrid.tsx +5 -3
- package/src/components/calendar/RangeCalendar.scss +8 -8
- package/src/components/calendar/RangeCalendar.tsx +56 -44
- package/src/components/calendar/index.ts +1 -0
- package/src/components/calendar/labels.tsx +71 -0
- package/src/components/card/Card.tsx +4 -4
- package/src/components/card/CardGroup.tsx +1 -1
- package/src/components/card/CardImage.tsx +1 -1
- package/src/components/carousel/Carousel.tsx +20 -9
- package/src/components/carousel/CarouselControlButton.tsx +1 -1
- package/src/components/carousel/CarouselPlayPause.tsx +1 -1
- package/src/components/carousel/carouselEngine.ts +3 -3
- package/src/components/checkbox/Checkbox.tsx +12 -11
- package/src/components/checkbox/CheckboxGroup.tsx +1 -1
- package/src/components/chip/Chip.tsx +5 -4
- package/src/components/chip-input/ChipInput.tsx +12 -4
- package/src/components/chip-input/ChipList.tsx +2 -2
- package/src/components/close-button/CloseButton.tsx +2 -2
- package/src/components/color-input/ColorInput.tsx +2 -2
- package/src/components/combobox/Combobox.tsx +2 -2
- package/src/components/datagrid/DataGrid.scss +203 -0
- package/src/components/datagrid/DataGrid.tsx +259 -0
- package/src/components/datagrid/DataGridBody.tsx +162 -0
- package/src/components/datagrid/DataGridCell.tsx +47 -0
- package/src/components/datagrid/DataGridColumn.tsx +109 -0
- package/src/components/datagrid/DataGridHeader.tsx +47 -0
- package/src/components/datagrid/DataGridPinBehavior.tsx +225 -0
- package/src/components/datagrid/DataGridRow.tsx +48 -0
- package/src/components/datagrid/DataGridSelectionCell.tsx +74 -0
- package/src/components/datagrid/DataGridTableLayout.ts +30 -0
- package/src/components/datagrid/index.ts +14 -0
- package/src/components/datagrid/useColumnGeometry.ts +161 -0
- package/src/components/datepicker/ClearButton.tsx +4 -1
- package/src/components/datepicker/DatePicker.scss +2 -2
- package/src/components/datepicker/DatePicker.tsx +179 -147
- package/src/components/datepicker/DateRangePicker.tsx +107 -89
- package/src/components/datepicker/renderDatePickerShell.tsx +1 -1
- package/src/components/drawer/Drawer.tsx +3 -1
- package/src/components/file-input/FileInput.tsx +2 -2
- package/src/components/floating-input/FloatingInput.tsx +8 -8
- package/src/components/form/renderFormCheck.tsx +3 -3
- package/src/components/form-field/FormField.tsx +8 -8
- package/src/components/form-field/renderFormField.tsx +14 -3
- package/src/components/grid/Col.tsx +1 -1
- package/src/components/grid/GridItem.tsx +1 -1
- package/src/components/grid/Row.tsx +1 -1
- package/src/components/input-group/InputGroup.tsx +2 -2
- package/src/components/list/List.tsx +5 -5
- package/src/components/menu/Menu.tsx +10 -0
- package/src/components/menu/MenuSubmenu.tsx +3 -3
- package/src/components/menu/MenuSubmenuBack.tsx +1 -1
- package/src/components/menu/MenuToggle.tsx +2 -2
- package/src/components/modal/Modal.tsx +5 -3
- package/src/components/navbar/Navbar.tsx +1 -1
- package/src/components/navbar/NavbarBrand.tsx +5 -2
- package/src/components/notification/NotificationStack.tsx +1 -1
- package/src/components/otp-input/OtpBox.tsx +1 -1
- package/src/components/otp-input/OtpInput.tsx +12 -11
- package/src/components/pagination/Pagination.tsx +2 -2
- package/src/components/pagination/PaginationItem.tsx +23 -4
- package/src/components/popover/Popover.tsx +40 -14
- package/src/components/progress/Progress.tsx +1 -1
- package/src/components/progress/ProgressBar.tsx +1 -1
- package/src/components/radio/Radio.tsx +2 -2
- package/src/components/radio/RadioGroup.tsx +1 -1
- package/src/components/select/Select.tsx +8 -8
- package/src/components/skeleton/Skeleton.tsx +1 -1
- package/src/components/spinner/Spinner.tsx +2 -2
- package/src/components/stepper/Stepper.tsx +5 -5
- package/src/components/switch/Switch.tsx +3 -3
- package/src/components/table/Table.css +2 -2
- package/src/components/table/Table.tsx +30 -7
- package/src/components/text-input/TextInput.tsx +2 -2
- package/src/components/textarea/Textarea.tsx +2 -2
- package/src/components/toast/ToastBody.tsx +1 -1
- package/src/components/toast/ToastHeader.tsx +2 -2
- package/src/components/toast/Toaster.tsx +1 -1
- package/src/components/tooltip/Tooltip.tsx +23 -7
- package/src/hooks/useAutoDismiss.ts +4 -1
- package/src/hooks/useDialogElement.ts +10 -0
- package/src/hooks/useFloatingOverlay.ts +12 -2
- package/src/hooks/usePagination.ts +6 -2
- package/src/index.ts +226 -0
- package/src/types.ts +26 -26
- package/src/utils/breakpoints.ts +1 -1
- package/src/utils/devWarning.ts +58 -0
- package/src/utils/direction.ts +14 -0
- package/src/utils/focusRedirect.ts +10 -16
- package/src/utils/polymorphic.ts +18 -1
- package/src/utils/suppressFocusRingGlobally.ts +22 -7
- 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'
|