@patternfly/react-data-view 6.4.0-prerelease.11 → 6.4.0-prerelease.13

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.
@@ -16,6 +16,8 @@ export interface DataViewTextFilterProps extends SearchInputProps {
16
16
  trimValue?: boolean;
17
17
  /** Custom OUIA ID */
18
18
  ouiaId?: string;
19
+ /** Enable keyboard shortcut (/) to focus the filter. Defaults to true. */
20
+ enableShortcut?: boolean;
19
21
  }
20
22
  export declare const DataViewTextFilter: FC<DataViewTextFilterProps>;
21
23
  export default DataViewTextFilter;
@@ -13,9 +13,37 @@ var __rest = (this && this.__rest) || function (s, e) {
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
14
  exports.DataViewTextFilter = void 0;
15
15
  const jsx_runtime_1 = require("react/jsx-runtime");
16
+ const react_1 = require("react");
16
17
  const react_core_1 = require("@patternfly/react-core");
17
18
  const DataViewTextFilter = (_a) => {
18
- var { filterId, title, value = '', onChange, onClear = () => onChange === null || onChange === void 0 ? void 0 : onChange(undefined, ''), showToolbarItem, trimValue = true, ouiaId = 'DataViewTextFilter' } = _a, props = __rest(_a, ["filterId", "title", "value", "onChange", "onClear", "showToolbarItem", "trimValue", "ouiaId"]);
19
+ var { filterId, title, value = '', onChange, onClear = () => onChange === null || onChange === void 0 ? void 0 : onChange(undefined, ''), showToolbarItem, trimValue = true, ouiaId = 'DataViewTextFilter', enableShortcut = true } = _a, props = __rest(_a, ["filterId", "title", "value", "onChange", "onClear", "showToolbarItem", "trimValue", "ouiaId", "enableShortcut"]);
20
+ (0, react_1.useEffect)(() => {
21
+ if (!enableShortcut) {
22
+ return;
23
+ }
24
+ const handleKeyDown = (event) => {
25
+ // Only handle "/" key when not typing in an input, textarea, or contenteditable element
26
+ if (event.key === '/' && !event.ctrlKey && !event.metaKey && !event.altKey) {
27
+ const target = event.target;
28
+ const isInputElement = target.tagName === 'INPUT' ||
29
+ target.tagName === 'TEXTAREA' ||
30
+ target.isContentEditable;
31
+ // Only focus if the filter is visible and we're not already in an input field
32
+ if (showToolbarItem && !isInputElement) {
33
+ // Find the input element by its ID (searchInputId prop)
34
+ const inputElement = document.getElementById(filterId);
35
+ if (inputElement) {
36
+ event.preventDefault();
37
+ inputElement.focus();
38
+ }
39
+ }
40
+ }
41
+ };
42
+ window.addEventListener('keydown', handleKeyDown);
43
+ return () => {
44
+ window.removeEventListener('keydown', handleKeyDown);
45
+ };
46
+ }, [showToolbarItem, filterId, enableShortcut]);
19
47
  return ((0, jsx_runtime_1.jsx)(react_core_1.ToolbarFilter, { "data-ouia-component-id": ouiaId, labels: value.length > 0 ? [{ key: title, node: value }] : [], deleteLabel: () => onChange === null || onChange === void 0 ? void 0 : onChange(undefined, ''), categoryName: title, showToolbarItem: showToolbarItem, children: (0, jsx_runtime_1.jsx)(react_core_1.SearchInput, Object.assign({ searchInputId: filterId, value: value, onChange: (e, inputValue) => onChange === null || onChange === void 0 ? void 0 : onChange(e, trimValue ? inputValue.trim() : inputValue), onClear: onClear, placeholder: `Filter by ${title}`, "aria-label": `${title !== null && title !== void 0 ? title : filterId} filter`, "data-ouia-component-id": `${ouiaId}-input` }, props)) }, ouiaId));
20
48
  };
21
49
  exports.DataViewTextFilter = DataViewTextFilter;
@@ -1 +1 @@
1
- export {};
1
+ import '@testing-library/jest-dom';
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const jsx_runtime_1 = require("react/jsx-runtime");
7
7
  const react_1 = require("@testing-library/react");
8
+ require("@testing-library/jest-dom");
8
9
  const DataViewTextFilter_1 = __importDefault(require("./DataViewTextFilter"));
9
10
  const DataViewToolbar_1 = __importDefault(require("../DataViewToolbar"));
10
11
  describe('DataViewTextFilter component', () => {
@@ -19,4 +20,86 @@ describe('DataViewTextFilter component', () => {
19
20
  const { container } = (0, react_1.render)((0, jsx_runtime_1.jsx)(DataViewToolbar_1.default, { filters: (0, jsx_runtime_1.jsx)(DataViewTextFilter_1.default, Object.assign({}, defaultProps)) }));
20
21
  expect(container).toMatchSnapshot();
21
22
  });
23
+ it('should focus the search input when "/" key is pressed and filter is visible', () => {
24
+ (0, react_1.render)((0, jsx_runtime_1.jsx)(DataViewToolbar_1.default, { filters: (0, jsx_runtime_1.jsx)(DataViewTextFilter_1.default, Object.assign({}, defaultProps, { showToolbarItem: true })) }));
25
+ const input = document.getElementById('test-filter');
26
+ expect(input).toBeInTheDocument();
27
+ // Simulate pressing "/" key by creating and dispatching a KeyboardEvent
28
+ const keyEvent = new KeyboardEvent('keydown', {
29
+ key: '/',
30
+ code: 'Slash',
31
+ bubbles: true,
32
+ cancelable: true,
33
+ });
34
+ window.dispatchEvent(keyEvent);
35
+ // Check that the input has focus
36
+ expect(document.activeElement).toBe(input);
37
+ });
38
+ it('should not focus the search input when "/" key is pressed if filter is not visible', () => {
39
+ (0, react_1.render)((0, jsx_runtime_1.jsx)(DataViewToolbar_1.default, { filters: (0, jsx_runtime_1.jsx)(DataViewTextFilter_1.default, Object.assign({}, defaultProps, { showToolbarItem: false })) }));
40
+ const input = document.getElementById('test-filter');
41
+ // Simulate pressing "/" key
42
+ const keyEvent = new KeyboardEvent('keydown', {
43
+ key: '/',
44
+ code: 'Slash',
45
+ bubbles: true,
46
+ cancelable: true,
47
+ });
48
+ window.dispatchEvent(keyEvent);
49
+ if (input) {
50
+ expect(document.activeElement).not.toBe(input);
51
+ }
52
+ });
53
+ it('should not focus the search input when "/" key is pressed while typing in another input', () => {
54
+ const { container } = (0, react_1.render)((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("input", { "data-testid": "other-input" }), (0, jsx_runtime_1.jsx)(DataViewToolbar_1.default, { filters: (0, jsx_runtime_1.jsx)(DataViewTextFilter_1.default, Object.assign({}, defaultProps, { showToolbarItem: true })) })] }));
55
+ const otherInput = container.querySelector('[data-testid="other-input"]');
56
+ // Focus the other input first
57
+ otherInput.focus();
58
+ expect(document.activeElement).toBe(otherInput);
59
+ // Simulate pressing "/" key while focused on the other input
60
+ // The event target should be the input element
61
+ const keyEvent = new KeyboardEvent('keydown', {
62
+ key: '/',
63
+ code: 'Slash',
64
+ bubbles: true,
65
+ cancelable: true,
66
+ });
67
+ Object.defineProperty(keyEvent, 'target', {
68
+ value: otherInput,
69
+ enumerable: true,
70
+ });
71
+ window.dispatchEvent(keyEvent);
72
+ // The search input should not be focused since we're already in an input field
73
+ expect(document.activeElement).toBe(otherInput);
74
+ });
75
+ it('should not focus the search input when enableShortcut is false', () => {
76
+ (0, react_1.render)((0, jsx_runtime_1.jsx)(DataViewToolbar_1.default, { filters: (0, jsx_runtime_1.jsx)(DataViewTextFilter_1.default, Object.assign({}, defaultProps, { showToolbarItem: true, enableShortcut: false })) }));
77
+ const input = document.getElementById('test-filter');
78
+ expect(input).toBeInTheDocument();
79
+ // Simulate pressing "/" key
80
+ const keyEvent = new KeyboardEvent('keydown', {
81
+ key: '/',
82
+ code: 'Slash',
83
+ bubbles: true,
84
+ cancelable: true,
85
+ });
86
+ window.dispatchEvent(keyEvent);
87
+ // The input should not be focused since the shortcut is disabled
88
+ expect(document.activeElement).not.toBe(input);
89
+ });
90
+ it('should focus the search input when enableShortcut is true (default)', () => {
91
+ (0, react_1.render)((0, jsx_runtime_1.jsx)(DataViewToolbar_1.default, { filters: (0, jsx_runtime_1.jsx)(DataViewTextFilter_1.default, Object.assign({}, defaultProps, { showToolbarItem: true })) }));
92
+ const input = document.getElementById('test-filter');
93
+ expect(input).toBeInTheDocument();
94
+ // Simulate pressing "/" key
95
+ const keyEvent = new KeyboardEvent('keydown', {
96
+ key: '/',
97
+ code: 'Slash',
98
+ bubbles: true,
99
+ cancelable: true,
100
+ });
101
+ window.dispatchEvent(keyEvent);
102
+ // The input should be focused since the shortcut is enabled by default
103
+ expect(document.activeElement).toBe(input);
104
+ });
22
105
  });
@@ -5,13 +5,13 @@ export interface DataViewThResizableProps {
5
5
  isResizable?: boolean;
6
6
  /** 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. */
7
7
  onResize?: (event: ReactMouseEvent | MouseEvent | ReactKeyboardEvent | KeyboardEvent | TouchEvent, id: string | number | undefined, width: number) => void;
8
- /** Width of the column */
8
+ /** Starting width in pixels of the column */
9
9
  width?: number;
10
- /** Minimum width of the column */
10
+ /** Minimum resize width in pixels of the column */
11
11
  minWidth?: number;
12
- /** Increment for keyboard navigation */
12
+ /** Increment in pixels for keyboard navigation */
13
13
  increment?: number;
14
- /** Increment for keyboard navigation while shift is held */
14
+ /** Increment in pixels for keyboard navigation while shift is held */
15
15
  shiftIncrement?: number;
16
16
  /** Provides an accessible name for the resizable column via a human readable string. */
17
17
  resizeButtonAriaLabel?: string;
@@ -216,7 +216,14 @@ const DataViewTh = (_a) => {
216
216
  onResize && onResize(e, thProps === null || thProps === void 0 ? void 0 : thProps.id, newSize);
217
217
  };
218
218
  const resizableContent = ((0, jsx_runtime_1.jsxs)(react_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { "aria-live": "polite", className: "pf-v6-screen-reader", children: screenReaderText }), (0, jsx_runtime_1.jsx)(react_core_1.Button, { ref: resizeButtonRef, variant: "plain", hasNoPadding: true, icon: (0, jsx_runtime_1.jsx)(ResizeIcon, {}), onMouseDown: handleMousedown, onKeyDown: handleKeys, onTouchStart: handleTouchStart, "aria-label": resizeButtonAriaLabel, className: classes.dataViewResizableButton })] }));
219
- return ((0, jsx_runtime_1.jsx)(react_table_1.Th, Object.assign({}, thProps, props, { style: width > 0 ? { minWidth: width } : undefined, ref: thRef, modifier: "truncate", className: dataViewThClassName }, (isResizable && { additionalContent: resizableContent }), { children: content })));
219
+ const classNames = [];
220
+ if (thProps === null || thProps === void 0 ? void 0 : thProps.className) {
221
+ classNames.push(thProps.className);
222
+ }
223
+ if (dataViewThClassName) {
224
+ classNames.push(dataViewThClassName);
225
+ }
226
+ return ((0, jsx_runtime_1.jsx)(react_table_1.Th, Object.assign({ modifier: "truncate" }, thProps, props, { ref: thRef, style: width > 0 ? Object.assign(Object.assign({}, thProps === null || thProps === void 0 ? void 0 : thProps.style), { minWidth: width }) : thProps === null || thProps === void 0 ? void 0 : thProps.style, className: classNames.length > 0 ? classNames.join(' ') : undefined }, (isResizable && { additionalContent: resizableContent }), { children: content })));
220
227
  };
221
228
  exports.DataViewTh = DataViewTh;
222
229
  exports.default = exports.DataViewTh;
@@ -16,6 +16,8 @@ export interface DataViewTextFilterProps extends SearchInputProps {
16
16
  trimValue?: boolean;
17
17
  /** Custom OUIA ID */
18
18
  ouiaId?: string;
19
+ /** Enable keyboard shortcut (/) to focus the filter. Defaults to true. */
20
+ enableShortcut?: boolean;
19
21
  }
20
22
  export declare const DataViewTextFilter: FC<DataViewTextFilterProps>;
21
23
  export default DataViewTextFilter;
@@ -10,9 +10,37 @@ var __rest = (this && this.__rest) || function (s, e) {
10
10
  return t;
11
11
  };
12
12
  import { jsx as _jsx } from "react/jsx-runtime";
13
+ import { useEffect } from 'react';
13
14
  import { SearchInput, ToolbarFilter } from '@patternfly/react-core';
14
15
  export const DataViewTextFilter = (_a) => {
15
- var { filterId, title, value = '', onChange, onClear = () => onChange === null || onChange === void 0 ? void 0 : onChange(undefined, ''), showToolbarItem, trimValue = true, ouiaId = 'DataViewTextFilter' } = _a, props = __rest(_a, ["filterId", "title", "value", "onChange", "onClear", "showToolbarItem", "trimValue", "ouiaId"]);
16
+ var { filterId, title, value = '', onChange, onClear = () => onChange === null || onChange === void 0 ? void 0 : onChange(undefined, ''), showToolbarItem, trimValue = true, ouiaId = 'DataViewTextFilter', enableShortcut = true } = _a, props = __rest(_a, ["filterId", "title", "value", "onChange", "onClear", "showToolbarItem", "trimValue", "ouiaId", "enableShortcut"]);
17
+ useEffect(() => {
18
+ if (!enableShortcut) {
19
+ return;
20
+ }
21
+ const handleKeyDown = (event) => {
22
+ // Only handle "/" key when not typing in an input, textarea, or contenteditable element
23
+ if (event.key === '/' && !event.ctrlKey && !event.metaKey && !event.altKey) {
24
+ const target = event.target;
25
+ const isInputElement = target.tagName === 'INPUT' ||
26
+ target.tagName === 'TEXTAREA' ||
27
+ target.isContentEditable;
28
+ // Only focus if the filter is visible and we're not already in an input field
29
+ if (showToolbarItem && !isInputElement) {
30
+ // Find the input element by its ID (searchInputId prop)
31
+ const inputElement = document.getElementById(filterId);
32
+ if (inputElement) {
33
+ event.preventDefault();
34
+ inputElement.focus();
35
+ }
36
+ }
37
+ }
38
+ };
39
+ window.addEventListener('keydown', handleKeyDown);
40
+ return () => {
41
+ window.removeEventListener('keydown', handleKeyDown);
42
+ };
43
+ }, [showToolbarItem, filterId, enableShortcut]);
16
44
  return (_jsx(ToolbarFilter, { "data-ouia-component-id": ouiaId, labels: value.length > 0 ? [{ key: title, node: value }] : [], deleteLabel: () => onChange === null || onChange === void 0 ? void 0 : onChange(undefined, ''), categoryName: title, showToolbarItem: showToolbarItem, children: _jsx(SearchInput, Object.assign({ searchInputId: filterId, value: value, onChange: (e, inputValue) => onChange === null || onChange === void 0 ? void 0 : onChange(e, trimValue ? inputValue.trim() : inputValue), onClear: onClear, placeholder: `Filter by ${title}`, "aria-label": `${title !== null && title !== void 0 ? title : filterId} filter`, "data-ouia-component-id": `${ouiaId}-input` }, props)) }, ouiaId));
17
45
  };
18
46
  export default DataViewTextFilter;
@@ -1 +1 @@
1
- export {};
1
+ import '@testing-library/jest-dom';
@@ -1,5 +1,6 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { render } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
3
4
  import DataViewTextFilter from './DataViewTextFilter';
4
5
  import DataViewToolbar from '../DataViewToolbar';
5
6
  describe('DataViewTextFilter component', () => {
@@ -14,4 +15,86 @@ describe('DataViewTextFilter component', () => {
14
15
  const { container } = render(_jsx(DataViewToolbar, { filters: _jsx(DataViewTextFilter, Object.assign({}, defaultProps)) }));
15
16
  expect(container).toMatchSnapshot();
16
17
  });
18
+ it('should focus the search input when "/" key is pressed and filter is visible', () => {
19
+ render(_jsx(DataViewToolbar, { filters: _jsx(DataViewTextFilter, Object.assign({}, defaultProps, { showToolbarItem: true })) }));
20
+ const input = document.getElementById('test-filter');
21
+ expect(input).toBeInTheDocument();
22
+ // Simulate pressing "/" key by creating and dispatching a KeyboardEvent
23
+ const keyEvent = new KeyboardEvent('keydown', {
24
+ key: '/',
25
+ code: 'Slash',
26
+ bubbles: true,
27
+ cancelable: true,
28
+ });
29
+ window.dispatchEvent(keyEvent);
30
+ // Check that the input has focus
31
+ expect(document.activeElement).toBe(input);
32
+ });
33
+ it('should not focus the search input when "/" key is pressed if filter is not visible', () => {
34
+ render(_jsx(DataViewToolbar, { filters: _jsx(DataViewTextFilter, Object.assign({}, defaultProps, { showToolbarItem: false })) }));
35
+ const input = document.getElementById('test-filter');
36
+ // Simulate pressing "/" key
37
+ const keyEvent = new KeyboardEvent('keydown', {
38
+ key: '/',
39
+ code: 'Slash',
40
+ bubbles: true,
41
+ cancelable: true,
42
+ });
43
+ window.dispatchEvent(keyEvent);
44
+ if (input) {
45
+ expect(document.activeElement).not.toBe(input);
46
+ }
47
+ });
48
+ it('should not focus the search input when "/" key is pressed while typing in another input', () => {
49
+ const { container } = render(_jsxs("div", { children: [_jsx("input", { "data-testid": "other-input" }), _jsx(DataViewToolbar, { filters: _jsx(DataViewTextFilter, Object.assign({}, defaultProps, { showToolbarItem: true })) })] }));
50
+ const otherInput = container.querySelector('[data-testid="other-input"]');
51
+ // Focus the other input first
52
+ otherInput.focus();
53
+ expect(document.activeElement).toBe(otherInput);
54
+ // Simulate pressing "/" key while focused on the other input
55
+ // The event target should be the input element
56
+ const keyEvent = new KeyboardEvent('keydown', {
57
+ key: '/',
58
+ code: 'Slash',
59
+ bubbles: true,
60
+ cancelable: true,
61
+ });
62
+ Object.defineProperty(keyEvent, 'target', {
63
+ value: otherInput,
64
+ enumerable: true,
65
+ });
66
+ window.dispatchEvent(keyEvent);
67
+ // The search input should not be focused since we're already in an input field
68
+ expect(document.activeElement).toBe(otherInput);
69
+ });
70
+ it('should not focus the search input when enableShortcut is false', () => {
71
+ render(_jsx(DataViewToolbar, { filters: _jsx(DataViewTextFilter, Object.assign({}, defaultProps, { showToolbarItem: true, enableShortcut: false })) }));
72
+ const input = document.getElementById('test-filter');
73
+ expect(input).toBeInTheDocument();
74
+ // Simulate pressing "/" key
75
+ const keyEvent = new KeyboardEvent('keydown', {
76
+ key: '/',
77
+ code: 'Slash',
78
+ bubbles: true,
79
+ cancelable: true,
80
+ });
81
+ window.dispatchEvent(keyEvent);
82
+ // The input should not be focused since the shortcut is disabled
83
+ expect(document.activeElement).not.toBe(input);
84
+ });
85
+ it('should focus the search input when enableShortcut is true (default)', () => {
86
+ render(_jsx(DataViewToolbar, { filters: _jsx(DataViewTextFilter, Object.assign({}, defaultProps, { showToolbarItem: true })) }));
87
+ const input = document.getElementById('test-filter');
88
+ expect(input).toBeInTheDocument();
89
+ // Simulate pressing "/" key
90
+ const keyEvent = new KeyboardEvent('keydown', {
91
+ key: '/',
92
+ code: 'Slash',
93
+ bubbles: true,
94
+ cancelable: true,
95
+ });
96
+ window.dispatchEvent(keyEvent);
97
+ // The input should be focused since the shortcut is enabled by default
98
+ expect(document.activeElement).toBe(input);
99
+ });
17
100
  });
@@ -5,13 +5,13 @@ export interface DataViewThResizableProps {
5
5
  isResizable?: boolean;
6
6
  /** 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. */
7
7
  onResize?: (event: ReactMouseEvent | MouseEvent | ReactKeyboardEvent | KeyboardEvent | TouchEvent, id: string | number | undefined, width: number) => void;
8
- /** Width of the column */
8
+ /** Starting width in pixels of the column */
9
9
  width?: number;
10
- /** Minimum width of the column */
10
+ /** Minimum resize width in pixels of the column */
11
11
  minWidth?: number;
12
- /** Increment for keyboard navigation */
12
+ /** Increment in pixels for keyboard navigation */
13
13
  increment?: number;
14
- /** Increment for keyboard navigation while shift is held */
14
+ /** Increment in pixels for keyboard navigation while shift is held */
15
15
  shiftIncrement?: number;
16
16
  /** Provides an accessible name for the resizable column via a human readable string. */
17
17
  resizeButtonAriaLabel?: string;
@@ -210,6 +210,13 @@ export const DataViewTh = (_a) => {
210
210
  onResize && onResize(e, thProps === null || thProps === void 0 ? void 0 : thProps.id, newSize);
211
211
  };
212
212
  const resizableContent = (_jsxs(Fragment, { children: [_jsx("div", { "aria-live": "polite", className: "pf-v6-screen-reader", children: screenReaderText }), _jsx(Button, { ref: resizeButtonRef, variant: "plain", hasNoPadding: true, icon: _jsx(ResizeIcon, {}), onMouseDown: handleMousedown, onKeyDown: handleKeys, onTouchStart: handleTouchStart, "aria-label": resizeButtonAriaLabel, className: classes.dataViewResizableButton })] }));
213
- return (_jsx(Th, Object.assign({}, thProps, props, { style: width > 0 ? { minWidth: width } : undefined, ref: thRef, modifier: "truncate", className: dataViewThClassName }, (isResizable && { additionalContent: resizableContent }), { children: content })));
213
+ const classNames = [];
214
+ if (thProps === null || thProps === void 0 ? void 0 : thProps.className) {
215
+ classNames.push(thProps.className);
216
+ }
217
+ if (dataViewThClassName) {
218
+ classNames.push(dataViewThClassName);
219
+ }
220
+ return (_jsx(Th, Object.assign({ modifier: "truncate" }, thProps, props, { ref: thRef, style: width > 0 ? Object.assign(Object.assign({}, thProps === null || thProps === void 0 ? void 0 : thProps.style), { minWidth: width }) : thProps === null || thProps === void 0 ? void 0 : thProps.style, className: classNames.length > 0 ? classNames.join(' ') : undefined }, (isResizable && { additionalContent: resizableContent }), { children: content })));
214
221
  };
215
222
  export default DataViewTh;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patternfly/react-data-view",
3
- "version": "6.4.0-prerelease.11",
3
+ "version": "6.4.0-prerelease.13",
4
4
  "description": "Data view used for Red Hat projects.",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -1,4 +1,5 @@
1
1
  import { render } from '@testing-library/react';
2
+ import '@testing-library/jest-dom';
2
3
  import DataViewTextFilter, { DataViewTextFilterProps } from './DataViewTextFilter';
3
4
  import DataViewToolbar from '../DataViewToolbar';
4
5
 
@@ -20,4 +21,132 @@ describe('DataViewTextFilter component', () => {
20
21
  />);
21
22
  expect(container).toMatchSnapshot();
22
23
  });
24
+
25
+ it('should focus the search input when "/" key is pressed and filter is visible', () => {
26
+ render(<DataViewToolbar
27
+ filters={
28
+ <DataViewTextFilter {...defaultProps} showToolbarItem={true} />
29
+ }
30
+ />);
31
+
32
+ const input = document.getElementById('test-filter') as HTMLInputElement;
33
+ expect(input).toBeInTheDocument();
34
+
35
+ // Simulate pressing "/" key by creating and dispatching a KeyboardEvent
36
+ const keyEvent = new KeyboardEvent('keydown', {
37
+ key: '/',
38
+ code: 'Slash',
39
+ bubbles: true,
40
+ cancelable: true,
41
+ });
42
+ window.dispatchEvent(keyEvent);
43
+
44
+ // Check that the input has focus
45
+ expect(document.activeElement).toBe(input);
46
+ });
47
+
48
+ it('should not focus the search input when "/" key is pressed if filter is not visible', () => {
49
+ render(<DataViewToolbar
50
+ filters={
51
+ <DataViewTextFilter {...defaultProps} showToolbarItem={false} />
52
+ }
53
+ />);
54
+
55
+ const input = document.getElementById('test-filter') as HTMLInputElement;
56
+
57
+ // Simulate pressing "/" key
58
+ const keyEvent = new KeyboardEvent('keydown', {
59
+ key: '/',
60
+ code: 'Slash',
61
+ bubbles: true,
62
+ cancelable: true,
63
+ });
64
+ window.dispatchEvent(keyEvent);
65
+
66
+ if (input) {
67
+ expect(document.activeElement).not.toBe(input);
68
+ }
69
+ });
70
+
71
+ it('should not focus the search input when "/" key is pressed while typing in another input', () => {
72
+ const { container } = render(
73
+ <div>
74
+ <input data-testid="other-input" />
75
+ <DataViewToolbar
76
+ filters={
77
+ <DataViewTextFilter {...defaultProps} showToolbarItem={true} />
78
+ }
79
+ />
80
+ </div>
81
+ );
82
+
83
+ const otherInput = container.querySelector('[data-testid="other-input"]') as HTMLInputElement;
84
+
85
+ // Focus the other input first
86
+ otherInput.focus();
87
+ expect(document.activeElement).toBe(otherInput);
88
+
89
+ // Simulate pressing "/" key while focused on the other input
90
+ // The event target should be the input element
91
+ const keyEvent = new KeyboardEvent('keydown', {
92
+ key: '/',
93
+ code: 'Slash',
94
+ bubbles: true,
95
+ cancelable: true,
96
+ });
97
+ Object.defineProperty(keyEvent, 'target', {
98
+ value: otherInput,
99
+ enumerable: true,
100
+ });
101
+ window.dispatchEvent(keyEvent);
102
+
103
+ // The search input should not be focused since we're already in an input field
104
+ expect(document.activeElement).toBe(otherInput);
105
+ });
106
+
107
+ it('should not focus the search input when enableShortcut is false', () => {
108
+ render(<DataViewToolbar
109
+ filters={
110
+ <DataViewTextFilter {...defaultProps} showToolbarItem={true} enableShortcut={false} />
111
+ }
112
+ />);
113
+
114
+ const input = document.getElementById('test-filter') as HTMLInputElement;
115
+ expect(input).toBeInTheDocument();
116
+
117
+ // Simulate pressing "/" key
118
+ const keyEvent = new KeyboardEvent('keydown', {
119
+ key: '/',
120
+ code: 'Slash',
121
+ bubbles: true,
122
+ cancelable: true,
123
+ });
124
+ window.dispatchEvent(keyEvent);
125
+
126
+ // The input should not be focused since the shortcut is disabled
127
+ expect(document.activeElement).not.toBe(input);
128
+ });
129
+
130
+ it('should focus the search input when enableShortcut is true (default)', () => {
131
+ render(<DataViewToolbar
132
+ filters={
133
+ <DataViewTextFilter {...defaultProps} showToolbarItem={true} />
134
+ }
135
+ />);
136
+
137
+ const input = document.getElementById('test-filter') as HTMLInputElement;
138
+ expect(input).toBeInTheDocument();
139
+
140
+ // Simulate pressing "/" key
141
+ const keyEvent = new KeyboardEvent('keydown', {
142
+ key: '/',
143
+ code: 'Slash',
144
+ bubbles: true,
145
+ cancelable: true,
146
+ });
147
+ window.dispatchEvent(keyEvent);
148
+
149
+ // The input should be focused since the shortcut is enabled by default
150
+ expect(document.activeElement).toBe(input);
151
+ });
23
152
  });
@@ -1,4 +1,4 @@
1
- import { FC } from 'react';
1
+ import { FC, useEffect } from 'react';
2
2
  import { SearchInput, SearchInputProps, ToolbarFilter, ToolbarFilterProps } from '@patternfly/react-core';
3
3
 
4
4
  /** extends SearchInputProps */
@@ -17,6 +17,8 @@ export interface DataViewTextFilterProps extends SearchInputProps {
17
17
  trimValue?: boolean;
18
18
  /** Custom OUIA ID */
19
19
  ouiaId?: string;
20
+ /** Enable keyboard shortcut (/) to focus the filter. Defaults to true. */
21
+ enableShortcut?: boolean;
20
22
  }
21
23
 
22
24
  export const DataViewTextFilter: FC<DataViewTextFilterProps> = ({
@@ -28,27 +30,61 @@ export const DataViewTextFilter: FC<DataViewTextFilterProps> = ({
28
30
  showToolbarItem,
29
31
  trimValue = true,
30
32
  ouiaId = 'DataViewTextFilter',
33
+ enableShortcut = true,
31
34
  ...props
32
- }: DataViewTextFilterProps) => (
33
- <ToolbarFilter
34
- key={ouiaId}
35
- data-ouia-component-id={ouiaId}
36
- labels={value.length > 0 ? [ { key: title, node: value } ] : []}
37
- deleteLabel={() => onChange?.(undefined, '')}
38
- categoryName={title}
39
- showToolbarItem={showToolbarItem}
40
- >
41
- <SearchInput
42
- searchInputId={filterId}
43
- value={value}
44
- onChange={(e, inputValue) => onChange?.(e, trimValue ? inputValue.trim() : inputValue)}
45
- onClear={onClear}
46
- placeholder={`Filter by ${title}`}
47
- aria-label={`${title ?? filterId} filter`}
48
- data-ouia-component-id={`${ouiaId}-input`}
49
- {...props}
50
- />
51
- </ToolbarFilter>
52
- );
35
+ }: DataViewTextFilterProps) => {
36
+ useEffect(() => {
37
+ if (!enableShortcut) {
38
+ return;
39
+ }
40
+
41
+ const handleKeyDown = (event: KeyboardEvent) => {
42
+ // Only handle "/" key when not typing in an input, textarea, or contenteditable element
43
+ if (event.key === '/' && !event.ctrlKey && !event.metaKey && !event.altKey) {
44
+ const target = event.target as HTMLElement;
45
+ const isInputElement = target.tagName === 'INPUT' ||
46
+ target.tagName === 'TEXTAREA' ||
47
+ target.isContentEditable;
48
+
49
+ // Only focus if the filter is visible and we're not already in an input field
50
+ if (showToolbarItem && !isInputElement) {
51
+ // Find the input element by its ID (searchInputId prop)
52
+ const inputElement = document.getElementById(filterId) as HTMLInputElement;
53
+ if (inputElement) {
54
+ event.preventDefault();
55
+ inputElement.focus();
56
+ }
57
+ }
58
+ }
59
+ };
60
+
61
+ window.addEventListener('keydown', handleKeyDown);
62
+ return () => {
63
+ window.removeEventListener('keydown', handleKeyDown);
64
+ };
65
+ }, [showToolbarItem, filterId, enableShortcut]);
66
+
67
+ return (
68
+ <ToolbarFilter
69
+ key={ouiaId}
70
+ data-ouia-component-id={ouiaId}
71
+ labels={value.length > 0 ? [ { key: title, node: value } ] : []}
72
+ deleteLabel={() => onChange?.(undefined, '')}
73
+ categoryName={title}
74
+ showToolbarItem={showToolbarItem}
75
+ >
76
+ <SearchInput
77
+ searchInputId={filterId}
78
+ value={value}
79
+ onChange={(e, inputValue) => onChange?.(e, trimValue ? inputValue.trim() : inputValue)}
80
+ onClear={onClear}
81
+ placeholder={`Filter by ${title}`}
82
+ aria-label={`${title ?? filterId} filter`}
83
+ data-ouia-component-id={`${ouiaId}-input`}
84
+ {...props}
85
+ />
86
+ </ToolbarFilter>
87
+ );
88
+ };
53
89
 
54
90
  export default DataViewTextFilter;
@@ -58,13 +58,13 @@ export interface DataViewThResizableProps {
58
58
  id: string | number | undefined,
59
59
  width: number
60
60
  ) => void;
61
- /** Width of the column */
61
+ /** Starting width in pixels of the column */
62
62
  width?: number;
63
- /** Minimum width of the column */
63
+ /** Minimum resize width in pixels of the column */
64
64
  minWidth?: number;
65
- /** Increment for keyboard navigation */
65
+ /** Increment in pixels for keyboard navigation */
66
66
  increment?: number;
67
- /** Increment for keyboard navigation while shift is held */
67
+ /** Increment in pixels for keyboard navigation while shift is held */
68
68
  shiftIncrement?: number;
69
69
  /** Provides an accessible name for the resizable column via a human readable string. */
70
70
  resizeButtonAriaLabel?: string;
@@ -324,14 +324,22 @@ export const DataViewTh: FC<DataViewThProps> = ({
324
324
  </Fragment>
325
325
  );
326
326
 
327
+ const classNames: string[] = [];
328
+ if (thProps?.className) {
329
+ classNames.push(thProps.className);
330
+ }
331
+ if (dataViewThClassName) {
332
+ classNames.push(dataViewThClassName);
333
+ }
334
+
327
335
  return (
328
336
  <Th
337
+ modifier="truncate"
329
338
  {...thProps}
330
339
  {...props}
331
- style={width > 0 ? { minWidth: width } : undefined}
332
340
  ref={thRef}
333
- modifier="truncate"
334
- className={dataViewThClassName}
341
+ style={width > 0 ? { ...thProps?.style, minWidth: width } : thProps?.style}
342
+ className={classNames.length > 0 ? classNames.join(' ') : undefined}
335
343
  {...(isResizable && { additionalContent: resizableContent })}
336
344
  >
337
345
  {content}