Skip to content

Commit f58ab23

Browse files
Wendellwendellhu
andauthored
feat: support ctrl n/p on mac (#650)
* feat: support ctrl n/p on mac * test: mock platform * chore: change doc * test: ignore file coverage on platformUtil * fix: fix mac platform on Firefox Co-authored-by: wendellhu <[email protected]>
1 parent 6b3dda7 commit f58ab23

File tree

4 files changed

+71
-55
lines changed

4 files changed

+71
-55
lines changed

src/OptionList.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type {
1717
} from './interface';
1818
import type { RawValueType, FlattenOptionsType } from './interface/generator';
1919
import { fillFieldNames } from './utils/valueUtil';
20+
import { isPlatformMac } from './utils/platformUtil';
2021

2122
export interface OptionListProps<OptionsType extends object[]> {
2223
prefixCls: string;
@@ -183,16 +184,24 @@ const OptionList: React.RefForwardingComponent<
183184
// ========================= Keyboard =========================
184185
React.useImperativeHandle(ref, () => ({
185186
onKeyDown: (event) => {
186-
const { which } = event;
187+
const { which, ctrlKey } = event;
187188
switch (which) {
188-
// >>> Arrow keys
189+
// >>> Arrow keys & ctrl + n/p on Mac
190+
case KeyCode.N:
191+
case KeyCode.P:
189192
case KeyCode.UP:
190193
case KeyCode.DOWN: {
191194
let offset = 0;
192195
if (which === KeyCode.UP) {
193196
offset = -1;
194197
} else if (which === KeyCode.DOWN) {
195198
offset = 1;
199+
} else if (isPlatformMac() && ctrlKey) {
200+
if (which === KeyCode.N) {
201+
offset = 1;
202+
} else if (which === KeyCode.P) {
203+
offset = -1;
204+
}
196205
}
197206

198207
if (offset !== 0) {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function isPlatformMac() {
2+
return true;
3+
}

src/utils/platformUtil.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/* istanbul ignore file */
2+
export function isPlatformMac(): boolean {
3+
return /(mac\sos|macintosh)/i.test(navigator.appVersion);
4+
}

tests/OptionList.test.tsx

Lines changed: 53 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@ import { mount } from 'enzyme';
22
import KeyCode from 'rc-util/lib/KeyCode';
33
import { act } from 'react-dom/test-utils';
44
import React from 'react';
5-
import OptionList, { OptionListProps, RefOptionListProps } from '../src/OptionList';
5+
import type { OptionListProps, RefOptionListProps } from '../src/OptionList';
6+
import OptionList from '../src/OptionList';
67
import { injectRunAllTimers } from './utils/common';
7-
import { OptionsType } from '../src/interface';
8+
import type { OptionsType } from '../src/interface';
89
import { flattenOptions } from '../src/utils/valueUtil';
910

11+
jest.mock('../src/utils/platformUtil');
12+
1013
describe('OptionList', () => {
1114
injectRunAllTimers(jest);
1215

@@ -103,6 +106,39 @@ describe('OptionList', () => {
103106
);
104107
});
105108

109+
// mocked how we detect running platform in test environment
110+
it('special key operation on Mac', () => {
111+
const onActiveValue = jest.fn();
112+
const listRef = React.createRef<RefOptionListProps>();
113+
mount(
114+
generateList({
115+
options: [{ value: '1' }, { value: '2' }],
116+
onActiveValue,
117+
ref: listRef,
118+
}),
119+
);
120+
121+
onActiveValue.mockReset();
122+
act(() => {
123+
listRef.current.onKeyDown({ which: KeyCode.N, ctrlKey: true } as any);
124+
});
125+
expect(onActiveValue).toHaveBeenCalledWith(
126+
'2',
127+
expect.anything(),
128+
expect.objectContaining({ source: 'keyboard' }),
129+
);
130+
131+
onActiveValue.mockReset();
132+
act(() => {
133+
listRef.current.onKeyDown({ which: KeyCode.P, ctrlKey: true } as any);
134+
});
135+
expect(onActiveValue).toHaveBeenCalledWith(
136+
'1',
137+
expect.anything(),
138+
expect.objectContaining({ source: 'keyboard' }),
139+
);
140+
});
141+
106142
it('hover to active', () => {
107143
const onActiveValue = jest.fn();
108144
const wrapper = mount(
@@ -113,10 +149,7 @@ describe('OptionList', () => {
113149
);
114150

115151
onActiveValue.mockReset();
116-
wrapper
117-
.find('.rc-select-item-option')
118-
.last()
119-
.simulate('mouseMove');
152+
wrapper.find('.rc-select-item-option').last().simulate('mouseMove');
120153
expect(onActiveValue).toHaveBeenCalledWith(
121154
'2',
122155
expect.anything(),
@@ -125,10 +158,7 @@ describe('OptionList', () => {
125158

126159
// Same item not repeat trigger
127160
onActiveValue.mockReset();
128-
wrapper
129-
.find('.rc-select-item-option')
130-
.last()
131-
.simulate('mouseMove');
161+
wrapper.find('.rc-select-item-option').last().simulate('mouseMove');
132162
expect(onActiveValue).not.toHaveBeenCalled();
133163
});
134164

@@ -140,29 +170,24 @@ describe('OptionList', () => {
140170
);
141171

142172
const preventDefault = jest.fn();
143-
wrapper
144-
.find('.rc-select-item-option')
145-
.last()
146-
.simulate('mouseDown', {
147-
preventDefault,
148-
});
173+
wrapper.find('.rc-select-item-option').last().simulate('mouseDown', {
174+
preventDefault,
175+
});
149176

150177
expect(preventDefault).toHaveBeenCalled();
151178
});
152179

153180
it('Data attributes should be set correct', () => {
154181
const wrapper = mount(
155182
generateList({
156-
options: [{ value: '1', label: 'my-label' }, { value: '2', 'data-num': '123' }],
183+
options: [
184+
{ value: '1', label: 'my-label' },
185+
{ value: '2', 'data-num': '123' },
186+
],
157187
}),
158188
);
159189

160-
expect(
161-
wrapper
162-
.find('.rc-select-item-option')
163-
.last()
164-
.prop('data-num'),
165-
).toBe('123');
190+
expect(wrapper.find('.rc-select-item-option').last().prop('data-num')).toBe('123');
166191
});
167192

168193
it('should render title defaultly', () => {
@@ -171,12 +196,7 @@ describe('OptionList', () => {
171196
options: [{ value: '1', label: 'my-label' }],
172197
}),
173198
);
174-
expect(
175-
wrapper
176-
.find('.rc-select-item-option')
177-
.first()
178-
.prop('title'),
179-
).toBe('my-label');
199+
expect(wrapper.find('.rc-select-item-option').first().prop('title')).toBe('my-label');
180200
});
181201

182202
it('should render title', () => {
@@ -185,12 +205,7 @@ describe('OptionList', () => {
185205
options: [{ value: '1', label: 'my-label', title: 'title' }],
186206
}),
187207
);
188-
expect(
189-
wrapper
190-
.find('.rc-select-item-option')
191-
.first()
192-
.prop('title'),
193-
).toBe('title');
208+
expect(wrapper.find('.rc-select-item-option').first().prop('title')).toBe('title');
194209
});
195210

196211
it('should not render title when title is empty string', () => {
@@ -199,12 +214,7 @@ describe('OptionList', () => {
199214
options: [{ value: '1', label: 'my-label', title: '' }],
200215
}),
201216
);
202-
expect(
203-
wrapper
204-
.find('.rc-select-item-option')
205-
.first()
206-
.prop('title'),
207-
).toBe('');
217+
expect(wrapper.find('.rc-select-item-option').first().prop('title')).toBe('');
208218
});
209219

210220
it('should render title from label when title is undefined', () => {
@@ -213,12 +223,7 @@ describe('OptionList', () => {
213223
options: [{ value: '1', label: 'my-label', title: undefined }],
214224
}),
215225
);
216-
expect(
217-
wrapper
218-
.find('.rc-select-item-option')
219-
.first()
220-
.prop('title'),
221-
).toBe('my-label');
226+
expect(wrapper.find('.rc-select-item-option').first().prop('title')).toBe('my-label');
222227
});
223228

224229
it('should not render title defaultly when label is ReactNode', () => {
@@ -227,11 +232,6 @@ describe('OptionList', () => {
227232
options: [{ value: '1', label: <div>label</div> }],
228233
}),
229234
);
230-
expect(
231-
wrapper
232-
.find('.rc-select-item-option')
233-
.first()
234-
.prop('title'),
235-
).toBe(undefined);
235+
expect(wrapper.find('.rc-select-item-option').first().prop('title')).toBe(undefined);
236236
});
237237
});

0 commit comments

Comments
 (0)