Skip to content

Commit 0a9a1fa

Browse files
authored
Merge pull request #594 from CodyWMitchell/filter-keyboard-shortcut
fix: handle keyboard shortcut for text filter
2 parents f77fddd + 7aa56a1 commit 0a9a1fa

File tree

2 files changed

+187
-22
lines changed

2 files changed

+187
-22
lines changed

packages/module/src/DataViewTextFilter/DataViewTextFilter.test.tsx

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { render } from '@testing-library/react';
2+
import '@testing-library/jest-dom';
23
import DataViewTextFilter, { DataViewTextFilterProps } from './DataViewTextFilter';
34
import DataViewToolbar from '../DataViewToolbar';
45

@@ -20,4 +21,132 @@ describe('DataViewTextFilter component', () => {
2021
/>);
2122
expect(container).toMatchSnapshot();
2223
});
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+
});
23152
});
Lines changed: 58 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FC } from 'react';
1+
import { FC, useEffect } from 'react';
22
import { SearchInput, SearchInputProps, ToolbarFilter, ToolbarFilterProps } from '@patternfly/react-core';
33

44
/** extends SearchInputProps */
@@ -17,6 +17,8 @@ export interface DataViewTextFilterProps extends SearchInputProps {
1717
trimValue?: boolean;
1818
/** Custom OUIA ID */
1919
ouiaId?: string;
20+
/** Enable keyboard shortcut (/) to focus the filter. Defaults to true. */
21+
enableShortcut?: boolean;
2022
}
2123

2224
export const DataViewTextFilter: FC<DataViewTextFilterProps> = ({
@@ -28,27 +30,61 @@ export const DataViewTextFilter: FC<DataViewTextFilterProps> = ({
2830
showToolbarItem,
2931
trimValue = true,
3032
ouiaId = 'DataViewTextFilter',
33+
enableShortcut = true,
3134
...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+
};
5389

5490
export default DataViewTextFilter;

0 commit comments

Comments
 (0)