@patternfly/react-data-view 6.4.0-prerelease.2 → 6.4.0-prerelease.3

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 (32) hide show
  1. package/dist/cjs/DataViewTable/DataViewTable.d.ts +4 -0
  2. package/dist/cjs/DataViewTable/DataViewTable.js +21 -1
  3. package/dist/cjs/DataViewTableBasic/DataViewTableBasic.d.ts +2 -0
  4. package/dist/cjs/DataViewTableBasic/DataViewTableBasic.js +2 -2
  5. package/dist/cjs/DataViewTableHead/DataViewTableHead.d.ts +2 -0
  6. package/dist/cjs/DataViewTableHead/DataViewTableHead.js +5 -4
  7. package/dist/cjs/DataViewTh/DataViewTh.d.ts +32 -0
  8. package/dist/cjs/DataViewTh/DataViewTh.js +222 -0
  9. package/dist/esm/DataViewTable/DataViewTable.d.ts +4 -0
  10. package/dist/esm/DataViewTable/DataViewTable.js +21 -1
  11. package/dist/esm/DataViewTableBasic/DataViewTableBasic.d.ts +2 -0
  12. package/dist/esm/DataViewTableBasic/DataViewTableBasic.js +2 -2
  13. package/dist/esm/DataViewTableHead/DataViewTableHead.d.ts +2 -0
  14. package/dist/esm/DataViewTableHead/DataViewTableHead.js +5 -4
  15. package/dist/esm/DataViewTh/DataViewTh.d.ts +32 -0
  16. package/dist/esm/DataViewTh/DataViewTh.js +215 -0
  17. package/dist/tsconfig.tsbuildinfo +1 -1
  18. package/package.json +6 -6
  19. package/patternfly-docs/content/extensions/data-view/examples/Table/DataViewTableResizableColumnsExample.tsx +155 -0
  20. package/patternfly-docs/content/extensions/data-view/examples/Table/Table.md +50 -14
  21. package/src/DataViewCheckboxFilter/__snapshots__/DataViewCheckboxFilter.test.tsx.snap +0 -2
  22. package/src/DataViewFilters/__snapshots__/DataViewFilters.test.tsx.snap +0 -2
  23. package/src/DataViewTable/DataViewTable.tsx +48 -27
  24. package/src/DataViewTable/__snapshots__/DataViewTable.test.tsx.snap +10 -18
  25. package/src/DataViewTableBasic/DataViewTableBasic.tsx +4 -1
  26. package/src/DataViewTableBasic/__snapshots__/DataViewTableBasic.test.tsx.snap +20 -20
  27. package/src/DataViewTableHead/DataViewTableHead.tsx +24 -23
  28. package/src/DataViewTableHead/__snapshots__/DataViewTableHead.test.tsx.snap +15 -15
  29. package/src/DataViewTableTree/__snapshots__/DataViewTableTree.test.tsx.snap +25 -41
  30. package/src/DataViewTextFilter/__snapshots__/DataViewTextFilter.test.tsx.snap +0 -3
  31. package/src/DataViewTh/DataViewTh.tsx +342 -0
  32. package/src/DataViewToolbar/__snapshots__/DataViewToolbar.test.tsx.snap +0 -10
@@ -0,0 +1,342 @@
1
+ import {
2
+ FC,
3
+ useState,
4
+ MouseEvent as ReactMouseEvent,
5
+ TouchEvent as ReactTouchEvent,
6
+ KeyboardEvent as ReactKeyboardEvent,
7
+ ReactNode,
8
+ useCallback,
9
+ useRef,
10
+ useEffect,
11
+ Fragment
12
+ } from 'react';
13
+ import { Th, ThProps } from '@patternfly/react-table';
14
+ import { Button, getLanguageDirection } from '@patternfly/react-core';
15
+ import { createUseStyles } from 'react-jss';
16
+
17
+ import tableCellPaddingBlockEnd from '@patternfly/react-tokens/dist/esm/c_table_cell_PaddingBlockEnd';
18
+ import tableCellPaddingInlineEnd from '@patternfly/react-tokens/dist/esm/c_table_cell_PaddingInlineEnd';
19
+ import globalFontSizeBodyDefault from '@patternfly/react-tokens/dist/esm/t_global_font_size_body_default';
20
+
21
+ const ResizeIcon = () => (
22
+ <svg
23
+ className="pf-v6-svg"
24
+ viewBox="0 0 1024 1024"
25
+ fill="currentColor"
26
+ aria-hidden="true"
27
+ role="img"
28
+ width="1em"
29
+ height="1em"
30
+ >
31
+ <path
32
+ fillRule="evenodd"
33
+ d="M52.7,529.8l190.5,161.9c18.6,15.9,50.5,4.7,50.5-17.8v-324c0-22.4-31.8-33.6-50.5-17.8L52.7,494.2c-11.5,9.8-11.5,25.8,0,35.6ZM480.8,12.9h62.4v998.3h-62.4V12.9ZM971.3,529.8l-190.5,161.9c-18.6,15.9-50.5,4.7-50.5-17.8v-324c0-22.4,31.8-33.6,50.5-17.8l190.5,161.9c11.5,9.8,11.5,25.8,0,35.6Z"
34
+ />
35
+ </svg>
36
+ );
37
+
38
+ const useStyles = createUseStyles({
39
+ dataViewResizeableTh: {
40
+ [tableCellPaddingInlineEnd.name]: `calc(${globalFontSizeBodyDefault.var} * 2)`
41
+ },
42
+ dataViewResizableButton: {
43
+ position: 'absolute',
44
+ insetInlineEnd: `calc(${globalFontSizeBodyDefault.var} / 2)`,
45
+ insetBlockEnd: tableCellPaddingBlockEnd.var,
46
+ cursor: 'grab',
47
+ '&:active': {
48
+ cursor: 'grabbing'
49
+ }
50
+ }
51
+ });
52
+ export interface DataViewThResizableProps {
53
+ /** Whether the column is resizable */
54
+ isResizable?: boolean;
55
+ /** Callback after the column is resized. Returns the triggering event, the column id passed in via cell props, and the new width of the column. */
56
+ onResize?: (
57
+ event: ReactMouseEvent | MouseEvent | ReactKeyboardEvent | KeyboardEvent | TouchEvent,
58
+ id: string | number | undefined,
59
+ width: number
60
+ ) => void;
61
+ /** Width of the column */
62
+ width?: number;
63
+ /** Minimum width of the column */
64
+ minWidth?: number;
65
+ /** Increment for keyboard navigation */
66
+ increment?: number;
67
+ /** Increment for keyboard navigation while shift is held */
68
+ shiftIncrement?: number;
69
+ /** Provides an accessible name for the resizable column via a human readable string. */
70
+ resizeButtonAriaLabel?: string;
71
+ /** Screenreader text that gets announced when the column is resized. */
72
+ screenReaderText?: string;
73
+ }
74
+ export interface DataViewThProps {
75
+ /** Cell content */
76
+ content: ReactNode;
77
+ /** Resizable props */
78
+ resizableProps?: DataViewThResizableProps;
79
+ /** @hide Indicates whether the table has resizable columns */
80
+ hasResizableColumns?: boolean;
81
+ /** Props passed to Th */
82
+ thProps?: ThProps;
83
+ }
84
+
85
+ export const DataViewTh: FC<DataViewThProps> = ({
86
+ content,
87
+ resizableProps = {},
88
+ hasResizableColumns = false,
89
+ thProps,
90
+ ...props
91
+ }: DataViewThProps) => {
92
+ const thRef = useRef<HTMLTableCellElement>(null);
93
+
94
+ const [width, setWidth] = useState(resizableProps?.width ? resizableProps.width : 0);
95
+ // Tracks the current column width for the onResize callback, because the width state is not updated until after the resize is complete
96
+ const trackedWidth = useRef(0);
97
+ const classes = useStyles();
98
+
99
+ const isResizable = resizableProps?.isResizable || false;
100
+ const increment = resizableProps?.increment || 5;
101
+ const shiftIncrement = resizableProps?.shiftIncrement || 25;
102
+ const resizeButtonAriaLabel = resizableProps?.resizeButtonAriaLabel;
103
+ const onResize = resizableProps?.onResize || undefined;
104
+ const screenReaderText = resizableProps?.screenReaderText || `Column ${width.toFixed(0)} pixels`;
105
+ const dataViewThClassName = isResizable ? classes.dataViewResizeableTh : undefined;
106
+
107
+ const resizeButtonRef = useRef<HTMLButtonElement>(null);
108
+ const setInitialVals = useRef(true);
109
+ const dragOffset = useRef(0);
110
+ const isResizing = useRef(false);
111
+ const isInView = useRef(true);
112
+
113
+ if (isResizable && !resizeButtonAriaLabel) {
114
+ // eslint-disable-next-line no-console
115
+ console.warn(
116
+ 'DataViewTh: Missing resizeButtonAriaLabel. An aria label must be passed to each resizable column to provide a context specific label for its resize button.'
117
+ );
118
+ }
119
+
120
+ useEffect(() => {
121
+ if (!isResizable) {
122
+ return;
123
+ }
124
+
125
+ const observed = resizeButtonRef.current;
126
+ const observer = new IntersectionObserver(
127
+ ([entry]) => {
128
+ isInView.current = entry.isIntersecting;
129
+ },
130
+ { threshold: 0.3 }
131
+ );
132
+
133
+ if (observed) {
134
+ observer.observe(observed);
135
+ }
136
+
137
+ return () => {
138
+ if (observed) {
139
+ observer.unobserve(observed);
140
+ }
141
+ };
142
+ }, []);
143
+
144
+ useEffect(() => {
145
+ if ((isResizable || hasResizableColumns) && setInitialVals.current && width === 0) {
146
+ setWidth(thRef.current?.getBoundingClientRect().width || 0);
147
+ setInitialVals.current = false;
148
+ }
149
+ }, [isResizable, hasResizableColumns, setInitialVals]);
150
+
151
+ const setDragOffset = (e: ReactMouseEvent | ReactTouchEvent) => {
152
+ const isRTL = getLanguageDirection(thRef.current as HTMLElement) === 'rtl';
153
+ const startingMousePos = 'clientX' in e ? e.clientX : e.touches[0].clientX;
154
+ const startingColumnPos =
155
+ (isRTL ? thRef.current?.getBoundingClientRect().left : thRef.current?.getBoundingClientRect().right) || 0;
156
+
157
+ dragOffset.current = startingColumnPos - startingMousePos;
158
+ };
159
+
160
+ const handleMousedown = (e: ReactMouseEvent) => {
161
+ e.stopPropagation();
162
+ e.preventDefault();
163
+ document.addEventListener('mousemove', callbackMouseMove);
164
+ document.addEventListener('mouseup', callbackMouseUp);
165
+
166
+ // When a user begins resizing a column, set the drag offset so the drag motion aligns with the column edge
167
+ if (dragOffset.current === 0) {
168
+ setDragOffset(e);
169
+ }
170
+
171
+ isResizing.current = true;
172
+ };
173
+
174
+ const handleMouseMove = (e: MouseEvent) => {
175
+ const mousePos = e.clientX;
176
+
177
+ handleControlMove(e, dragOffset.current + mousePos);
178
+ };
179
+
180
+ const handleTouchStart = (e: ReactTouchEvent) => {
181
+ e.stopPropagation();
182
+ document.addEventListener('touchmove', callbackTouchMove, { passive: false });
183
+ document.addEventListener('touchend', callbackTouchEnd);
184
+
185
+ // When a user begins resizing a column, set the drag offset so the drag motion aligns with the column edge
186
+ if (dragOffset.current === 0) {
187
+ setDragOffset(e);
188
+ }
189
+
190
+ isResizing.current = true;
191
+ };
192
+
193
+ const handleTouchMove = (e: TouchEvent) => {
194
+ e.preventDefault();
195
+ e.stopImmediatePropagation();
196
+ const touchPos = e.touches[0].clientX;
197
+
198
+ handleControlMove(e, touchPos);
199
+ };
200
+
201
+ const handleControlMove = (e: MouseEvent | TouchEvent, controlPosition: number) => {
202
+ e.stopPropagation();
203
+
204
+ if (!isResizing.current) {
205
+ return;
206
+ }
207
+
208
+ const columnRect = thRef.current?.getBoundingClientRect();
209
+ if (columnRect === undefined) {
210
+ return;
211
+ }
212
+
213
+ const mousePos = controlPosition;
214
+ const isRTL = getLanguageDirection(thRef.current as HTMLElement) === 'rtl';
215
+ let newSize = isRTL ? columnRect?.right - mousePos : mousePos - columnRect?.left;
216
+
217
+ // Prevent the column from shrinking below a specified minimum width
218
+ if (resizableProps.minWidth && newSize < resizableProps.minWidth) {
219
+ newSize = resizableProps.minWidth;
220
+ }
221
+
222
+ thRef.current?.style.setProperty('min-width', newSize + 'px');
223
+ trackedWidth.current = newSize;
224
+ setWidth(newSize);
225
+ };
226
+
227
+ const handleMouseup = (e: MouseEvent) => {
228
+ if (!isResizing.current) {
229
+ return;
230
+ }
231
+ // Reset the isResizing and dragOffset values to their initial states
232
+ isResizing.current = false;
233
+ dragOffset.current = 0;
234
+
235
+ // Call the onResize callback with the new width
236
+ onResize && onResize(e, thProps?.id, trackedWidth.current);
237
+
238
+ // Handle scroll into view when column drag button is moved off screen
239
+ if (resizeButtonRef.current && !isInView.current) {
240
+ resizeButtonRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
241
+ }
242
+
243
+ document.removeEventListener('mousemove', callbackMouseMove);
244
+ document.removeEventListener('mouseup', callbackMouseUp);
245
+ };
246
+
247
+ const handleTouchEnd = (e: TouchEvent) => {
248
+ e.stopPropagation();
249
+ if (!isResizing) {
250
+ return;
251
+ }
252
+ // Reset the isResizing and dragOffset values to their initial states
253
+ isResizing.current = false;
254
+ dragOffset.current = 0;
255
+
256
+ // Call the onResize callback with the new width
257
+ onResize && onResize(e, thProps?.id, trackedWidth.current);
258
+
259
+ document.removeEventListener('touchmove', callbackTouchMove);
260
+ document.removeEventListener('touchend', callbackTouchEnd);
261
+ };
262
+
263
+ const callbackMouseMove = useCallback(handleMouseMove, []);
264
+ const callbackTouchEnd = useCallback(handleTouchEnd, []);
265
+ const callbackTouchMove = useCallback(handleTouchMove, []);
266
+ const callbackMouseUp = useCallback(handleMouseup, []);
267
+
268
+ const handleKeys = (e: ReactKeyboardEvent) => {
269
+ const key = e.key;
270
+
271
+ if (key === 'Tab') {
272
+ isResizing.current = false;
273
+ }
274
+
275
+ if (key !== 'ArrowLeft' && key !== 'ArrowRight') {
276
+ return;
277
+ }
278
+ e.preventDefault();
279
+
280
+ isResizing.current = true;
281
+ const isRTL = getLanguageDirection(thRef.current as HTMLElement) === 'rtl';
282
+ const columnRect = thRef.current?.getBoundingClientRect();
283
+
284
+ let newSize = columnRect?.width || 0;
285
+ let delta = 0;
286
+ const _increment = e.shiftKey ? shiftIncrement : increment;
287
+
288
+ if (key === 'ArrowRight') {
289
+ if (isRTL) {
290
+ delta = -_increment;
291
+ } else {
292
+ delta = _increment;
293
+ }
294
+ } else if (key === 'ArrowLeft') {
295
+ if (isRTL) {
296
+ delta = _increment;
297
+ } else {
298
+ delta = -_increment;
299
+ }
300
+ }
301
+ newSize = newSize + delta;
302
+
303
+ thRef.current?.style.setProperty('min-width', newSize + 'px');
304
+ setWidth(newSize);
305
+ onResize && onResize(e, thProps?.id, newSize);
306
+ };
307
+
308
+ const resizableContent = (
309
+ <Fragment>
310
+ <div aria-live="polite" className="pf-v6-screen-reader">
311
+ {screenReaderText}
312
+ </div>
313
+ <Button
314
+ ref={resizeButtonRef}
315
+ variant="plain"
316
+ hasNoPadding
317
+ icon={<ResizeIcon />}
318
+ onMouseDown={handleMousedown}
319
+ onKeyDown={handleKeys}
320
+ onTouchStart={handleTouchStart}
321
+ aria-label={resizeButtonAriaLabel}
322
+ className={classes.dataViewResizableButton}
323
+ />
324
+ </Fragment>
325
+ );
326
+
327
+ return (
328
+ <Th
329
+ {...thProps}
330
+ {...props}
331
+ style={width > 0 ? { minWidth: width } : undefined}
332
+ ref={thRef}
333
+ modifier="truncate"
334
+ className={dataViewThClassName}
335
+ {...(isResizable && { additionalContent: resizableContent })}
336
+ >
337
+ {content}
338
+ </Th>
339
+ );
340
+ };
341
+
342
+ export default DataViewTh;
@@ -106,7 +106,6 @@ exports[`DataViewToolbar component should render correctly 1`] = `
106
106
  class="pf-v6-c-pagination__nav-control pf-m-first"
107
107
  >
108
108
  <button
109
- aria-disabled="false"
110
109
  aria-label="Go to first page"
111
110
  class="pf-v6-c-button pf-m-plain"
112
111
  data-action="first"
@@ -139,7 +138,6 @@ exports[`DataViewToolbar component should render correctly 1`] = `
139
138
  class="pf-v6-c-pagination__nav-control"
140
139
  >
141
140
  <button
142
- aria-disabled="false"
143
141
  aria-label="Go to previous page"
144
142
  class="pf-v6-c-button pf-m-plain"
145
143
  data-action="previous"
@@ -191,7 +189,6 @@ exports[`DataViewToolbar component should render correctly 1`] = `
191
189
  class="pf-v6-c-pagination__nav-control"
192
190
  >
193
191
  <button
194
- aria-disabled="false"
195
192
  aria-label="Go to next page"
196
193
  class="pf-v6-c-button pf-m-plain"
197
194
  data-action="next"
@@ -223,7 +220,6 @@ exports[`DataViewToolbar component should render correctly 1`] = `
223
220
  class="pf-v6-c-pagination__nav-control pf-m-last"
224
221
  >
225
222
  <button
226
- aria-disabled="false"
227
223
  aria-label="Go to last page"
228
224
  class="pf-v6-c-button pf-m-plain"
229
225
  data-action="last"
@@ -270,7 +266,6 @@ exports[`DataViewToolbar component should render correctly 1`] = `
270
266
  class="pf-v6-c-toolbar__item"
271
267
  >
272
268
  <button
273
- aria-disabled="false"
274
269
  class="pf-v6-c-button pf-m-link pf-m-inline"
275
270
  data-ouia-component-id="DataViewToolbar-clear-all-filters"
276
271
  data-ouia-component-type="PF6/Button"
@@ -391,7 +386,6 @@ exports[`DataViewToolbar component should render correctly 1`] = `
391
386
  class="pf-v6-c-pagination__nav-control pf-m-first"
392
387
  >
393
388
  <button
394
- aria-disabled="false"
395
389
  aria-label="Go to first page"
396
390
  class="pf-v6-c-button pf-m-plain"
397
391
  data-action="first"
@@ -424,7 +418,6 @@ exports[`DataViewToolbar component should render correctly 1`] = `
424
418
  class="pf-v6-c-pagination__nav-control"
425
419
  >
426
420
  <button
427
- aria-disabled="false"
428
421
  aria-label="Go to previous page"
429
422
  class="pf-v6-c-button pf-m-plain"
430
423
  data-action="previous"
@@ -476,7 +469,6 @@ exports[`DataViewToolbar component should render correctly 1`] = `
476
469
  class="pf-v6-c-pagination__nav-control"
477
470
  >
478
471
  <button
479
- aria-disabled="false"
480
472
  aria-label="Go to next page"
481
473
  class="pf-v6-c-button pf-m-plain"
482
474
  data-action="next"
@@ -508,7 +500,6 @@ exports[`DataViewToolbar component should render correctly 1`] = `
508
500
  class="pf-v6-c-pagination__nav-control pf-m-last"
509
501
  >
510
502
  <button
511
- aria-disabled="false"
512
503
  aria-label="Go to last page"
513
504
  class="pf-v6-c-button pf-m-plain"
514
505
  data-action="last"
@@ -555,7 +546,6 @@ exports[`DataViewToolbar component should render correctly 1`] = `
555
546
  class="pf-v6-c-toolbar__item"
556
547
  >
557
548
  <button
558
- aria-disabled="false"
559
549
  class="pf-v6-c-button pf-m-link pf-m-inline"
560
550
  data-ouia-component-id="DataViewToolbar-clear-all-filters"
561
551
  data-ouia-component-type="PF6/Button"