Skip to content

Commit 1c793f8

Browse files
fix: account for min/max clamping in keyboard handling (#41)
1 parent e011e37 commit 1c793f8

File tree

4 files changed

+104
-101
lines changed

4 files changed

+104
-101
lines changed

packages/vue-split-panel/src/SplitPanel.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ const { handleKeydown } = useKeyboard(sizePercentage, collapsed, {
6666
collapsible: () => props.collapsible,
6767
primary: () => props.primary,
6868
orientation: () => props.orientation,
69+
minSizePercentage,
70+
maxSizePercentage,
6971
});
7072
7173
const { isDragging, handleDblClick } = usePointer(collapsed, sizePercentage, sizePixels, {
Lines changed: 96 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import type { ComputedRef } from 'vue';
12
import { describe, expect, it, vi } from 'vitest';
2-
import { ref } from 'vue';
3+
import { computed, ref } from 'vue';
34
import { useKeyboard } from './use-keyboard';
45

56
describe('useKeyboard', () => {
@@ -9,15 +10,27 @@ describe('useKeyboard', () => {
910
return event;
1011
};
1112

13+
// Helper to build options with defaults and optional overrides
14+
const createOptions = (override: Partial<{
15+
disabled: boolean;
16+
collapsible: boolean;
17+
primary: 'start' | 'end';
18+
orientation: 'horizontal' | 'vertical';
19+
minSizePercentage: ComputedRef<number>;
20+
maxSizePercentage: ComputedRef<number | undefined>;
21+
}> = {}) => ({
22+
disabled: override.disabled ?? false,
23+
collapsible: override.collapsible ?? true,
24+
primary: override.primary ?? 'start',
25+
orientation: override.orientation ?? 'horizontal',
26+
minSizePercentage: override.minSizePercentage ?? computed(() => 0),
27+
maxSizePercentage: override.maxSizePercentage ?? computed(() => void 0),
28+
});
29+
1230
it('should return handleKeydown function', () => {
1331
const sizePercentage = ref(50);
1432
const collapsed = ref(false);
15-
const options = {
16-
disabled: false,
17-
collapsible: true,
18-
primary: 'start' as const,
19-
orientation: 'horizontal' as const,
20-
};
33+
const options = createOptions();
2134

2235
const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options);
2336

@@ -27,12 +40,7 @@ describe('useKeyboard', () => {
2740
it('should do nothing when disabled', () => {
2841
const sizePercentage = ref(50);
2942
const collapsed = ref(false);
30-
const options = {
31-
disabled: true,
32-
collapsible: true,
33-
primary: 'start' as const,
34-
orientation: 'horizontal' as const,
35-
};
43+
const options = createOptions({ disabled: true });
3644

3745
const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options);
3846
const event = createMockKeyboardEvent('ArrowRight');
@@ -47,12 +55,7 @@ describe('useKeyboard', () => {
4755
it('should decrease size on ArrowLeft when primary is start', () => {
4856
const sizePercentage = ref(50);
4957
const collapsed = ref(false);
50-
const options = {
51-
disabled: false,
52-
collapsible: true,
53-
primary: 'start' as const,
54-
orientation: 'horizontal' as const,
55-
};
58+
const options = createOptions();
5659

5760
const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options);
5861
const event = createMockKeyboardEvent('ArrowLeft');
@@ -66,12 +69,7 @@ describe('useKeyboard', () => {
6669
it('should increase size on ArrowRight when primary is start', () => {
6770
const sizePercentage = ref(50);
6871
const collapsed = ref(false);
69-
const options = {
70-
disabled: false,
71-
collapsible: true,
72-
primary: 'start' as const,
73-
orientation: 'horizontal' as const,
74-
};
72+
const options = createOptions();
7573

7674
const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options);
7775
const event = createMockKeyboardEvent('ArrowRight');
@@ -85,12 +83,7 @@ describe('useKeyboard', () => {
8583
it('should increase size on ArrowLeft when primary is end', () => {
8684
const sizePercentage = ref(50);
8785
const collapsed = ref(false);
88-
const options = {
89-
disabled: false,
90-
collapsible: true,
91-
primary: 'end' as const,
92-
orientation: 'horizontal' as const,
93-
};
86+
const options = createOptions({ primary: 'end' });
9487

9588
const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options);
9689
const event = createMockKeyboardEvent('ArrowLeft');
@@ -105,12 +98,7 @@ describe('useKeyboard', () => {
10598
it('should decrease size on ArrowUp when primary is start', () => {
10699
const sizePercentage = ref(50);
107100
const collapsed = ref(false);
108-
const options = {
109-
disabled: false,
110-
collapsible: true,
111-
primary: 'start' as const,
112-
orientation: 'vertical' as const,
113-
};
101+
const options = createOptions({ orientation: 'vertical' });
114102

115103
const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options);
116104
const event = createMockKeyboardEvent('ArrowUp');
@@ -123,12 +111,7 @@ describe('useKeyboard', () => {
123111
it('should increase size on ArrowDown when primary is start', () => {
124112
const sizePercentage = ref(50);
125113
const collapsed = ref(false);
126-
const options = {
127-
disabled: false,
128-
collapsible: true,
129-
primary: 'start' as const,
130-
orientation: 'vertical' as const,
131-
};
114+
const options = createOptions({ orientation: 'vertical' });
132115

133116
const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options);
134117
const event = createMockKeyboardEvent('ArrowDown');
@@ -143,12 +126,7 @@ describe('useKeyboard', () => {
143126
it('should change by 10 when shift key is pressed', () => {
144127
const sizePercentage = ref(50);
145128
const collapsed = ref(false);
146-
const options = {
147-
disabled: false,
148-
collapsible: true,
149-
primary: 'start' as const,
150-
orientation: 'horizontal' as const,
151-
};
129+
const options = createOptions();
152130

153131
const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options);
154132
const event = createMockKeyboardEvent('ArrowRight', true);
@@ -163,12 +141,7 @@ describe('useKeyboard', () => {
163141
it('should set to 0 on Home when primary is start', () => {
164142
const sizePercentage = ref(50);
165143
const collapsed = ref(false);
166-
const options = {
167-
disabled: false,
168-
collapsible: true,
169-
primary: 'start' as const,
170-
orientation: 'horizontal' as const,
171-
};
144+
const options = createOptions();
172145

173146
const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options);
174147
const event = createMockKeyboardEvent('Home');
@@ -181,12 +154,7 @@ describe('useKeyboard', () => {
181154
it('should set to 100 on End when primary is start', () => {
182155
const sizePercentage = ref(50);
183156
const collapsed = ref(false);
184-
const options = {
185-
disabled: false,
186-
collapsible: true,
187-
primary: 'start' as const,
188-
orientation: 'horizontal' as const,
189-
};
157+
const options = createOptions();
190158

191159
const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options);
192160
const event = createMockKeyboardEvent('End');
@@ -199,12 +167,7 @@ describe('useKeyboard', () => {
199167
it('should set to 100 on Home when primary is end', () => {
200168
const sizePercentage = ref(50);
201169
const collapsed = ref(false);
202-
const options = {
203-
disabled: false,
204-
collapsible: true,
205-
primary: 'end' as const,
206-
orientation: 'horizontal' as const,
207-
};
170+
const options = createOptions({ primary: 'end' });
208171

209172
const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options);
210173
const event = createMockKeyboardEvent('Home');
@@ -219,12 +182,7 @@ describe('useKeyboard', () => {
219182
it('should toggle collapsed state on Enter when collapsible is true', () => {
220183
const sizePercentage = ref(50);
221184
const collapsed = ref(false);
222-
const options = {
223-
disabled: false,
224-
collapsible: true,
225-
primary: 'start' as const,
226-
orientation: 'horizontal' as const,
227-
};
185+
const options = createOptions();
228186

229187
const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options);
230188
const event = createMockKeyboardEvent('Enter');
@@ -237,12 +195,7 @@ describe('useKeyboard', () => {
237195
it('should not toggle collapsed state on Enter when collapsible is false', () => {
238196
const sizePercentage = ref(50);
239197
const collapsed = ref(false);
240-
const options = {
241-
disabled: false,
242-
collapsible: false,
243-
primary: 'start' as const,
244-
orientation: 'horizontal' as const,
245-
};
198+
const options = createOptions({ collapsible: false });
246199

247200
const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options);
248201
const event = createMockKeyboardEvent('Enter');
@@ -257,12 +210,7 @@ describe('useKeyboard', () => {
257210
it('should clamp size to 0 minimum', () => {
258211
const sizePercentage = ref(2);
259212
const collapsed = ref(false);
260-
const options = {
261-
disabled: false,
262-
collapsible: true,
263-
primary: 'start' as const,
264-
orientation: 'horizontal' as const,
265-
};
213+
const options = createOptions();
266214

267215
const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options);
268216
const event = createMockKeyboardEvent('ArrowLeft', true);
@@ -275,12 +223,7 @@ describe('useKeyboard', () => {
275223
it('should clamp size to 100 maximum', () => {
276224
const sizePercentage = ref(98);
277225
const collapsed = ref(false);
278-
const options = {
279-
disabled: false,
280-
collapsible: true,
281-
primary: 'start' as const,
282-
orientation: 'horizontal' as const,
283-
};
226+
const options = createOptions();
284227

285228
const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options);
286229
const event = createMockKeyboardEvent('ArrowRight', true);
@@ -294,12 +237,7 @@ describe('useKeyboard', () => {
294237
it('should ignore non-handled keys', () => {
295238
const sizePercentage = ref(50);
296239
const collapsed = ref(false);
297-
const options = {
298-
disabled: false,
299-
collapsible: true,
300-
primary: 'start' as const,
301-
orientation: 'horizontal' as const,
302-
};
240+
const options = createOptions();
303241

304242
const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options);
305243
const event = createMockKeyboardEvent('KeyA');
@@ -309,4 +247,65 @@ describe('useKeyboard', () => {
309247
expect(sizePercentage.value).toBe(50);
310248
expect(event.preventDefault).not.toHaveBeenCalled();
311249
});
250+
251+
describe('custom min/max size percentages', () => {
252+
it('respects a custom minimum size percentage', () => {
253+
const sizePercentage = ref(25);
254+
const collapsed = ref(false);
255+
const options = createOptions({ minSizePercentage: computed(() => 20) });
256+
257+
const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options);
258+
259+
const event = new KeyboardEvent('keydown', { key: 'ArrowLeft' });
260+
handleKeydown(event);
261+
262+
expect(sizePercentage.value).toBe(24); // still above min -> decremented
263+
264+
for (let i = 0; i < 10; i++) handleKeydown(new KeyboardEvent('keydown', { key: 'ArrowLeft' }));
265+
266+
expect(sizePercentage.value).toBe(20); // clamped to min
267+
});
268+
269+
it('respects a custom maximum size percentage', () => {
270+
const sizePercentage = ref(75);
271+
const collapsed = ref(false);
272+
const options = createOptions({ maxSizePercentage: computed(() => 80) });
273+
274+
const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options);
275+
276+
const event = new KeyboardEvent('keydown', { key: 'ArrowRight' });
277+
handleKeydown(event);
278+
279+
expect(sizePercentage.value).toBe(76);
280+
281+
for (let i = 0; i < 10; i++) handleKeydown(new KeyboardEvent('keydown', { key: 'ArrowRight' }));
282+
283+
expect(sizePercentage.value).toBe(80); // clamped to max
284+
});
285+
286+
it('clamps both min and max simultaneously', () => {
287+
const sizePercentage = ref(40);
288+
const collapsed = ref(false);
289+
const options = createOptions({ minSizePercentage: computed(() => 30), maxSizePercentage: computed(() => 60) });
290+
291+
const { handleKeydown } = useKeyboard(sizePercentage, collapsed, options);
292+
293+
// Grow past max with shift
294+
handleKeydown(new KeyboardEvent('keydown', { key: 'ArrowRight', shiftKey: true })); // +10 => 50
295+
expect(sizePercentage.value).toBe(50);
296+
297+
handleKeydown(new KeyboardEvent('keydown', { key: 'ArrowRight', shiftKey: true })); // +10 => 60
298+
expect(sizePercentage.value).toBe(60);
299+
300+
handleKeydown(new KeyboardEvent('keydown', { key: 'ArrowRight', shiftKey: true })); // attempt +10 => 70 -> clamp 60
301+
expect(sizePercentage.value).toBe(60);
302+
303+
// Shrink past min with shift
304+
handleKeydown(new KeyboardEvent('keydown', { key: 'ArrowLeft', shiftKey: true })); // -10 => 50
305+
expect(sizePercentage.value).toBe(50);
306+
307+
for (let i = 0; i < 10; i++) handleKeydown(new KeyboardEvent('keydown', { key: 'ArrowLeft', shiftKey: true }));
308+
expect(sizePercentage.value).toBe(30);
309+
});
310+
});
312311
});

packages/vue-split-panel/src/composables/use-keyboard.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { MaybeRefOrGetter, Ref } from 'vue';
1+
import type { ComputedRef, MaybeRefOrGetter, Ref } from 'vue';
22
import type { Orientation, Primary } from '../types';
33
import { clamp } from '@vueuse/core';
44
import { toValue } from 'vue';
@@ -8,6 +8,8 @@ export interface UseKeyboardOptions {
88
collapsible: MaybeRefOrGetter<boolean>;
99
primary: MaybeRefOrGetter<Primary | undefined>;
1010
orientation: MaybeRefOrGetter<Orientation>;
11+
minSizePercentage: ComputedRef<number>;
12+
maxSizePercentage: ComputedRef<number | undefined>;
1113
}
1214

1315
export const useKeyboard = (sizePercentage: Ref<number>, collapsed: Ref<boolean>, options: UseKeyboardOptions) => {
@@ -47,7 +49,7 @@ export const useKeyboard = (sizePercentage: Ref<number>, collapsed: Ref<boolean>
4749
collapsed.value = !collapsed.value;
4850
}
4951

50-
sizePercentage.value = clamp(newPosition, 0, 100);
52+
sizePercentage.value = clamp(newPosition, options.minSizePercentage.value, options.maxSizePercentage.value ?? 100);
5153
}
5254
};
5355

packages/vue-split-panel/src/composables/use-pointer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export interface UsePointerOptions {
1717
dividerEl: MaybeRefOrGetter<HTMLElement | null>;
1818
panelEl: MaybeRefOrGetter<HTMLElement | null>;
1919
componentSize: ComputedRef<number>;
20-
minSizePixels: ComputedRef<number | undefined>;
20+
minSizePixels: ComputedRef<number>;
2121
snapPixels: ComputedRef<number[]>;
2222
}
2323

@@ -35,7 +35,7 @@ export const usePointer = (collapsed: Ref<boolean>, sizePercentage: Ref<number>,
3535
newPositionInPixels = options.componentSize.value - newPositionInPixels;
3636
}
3737

38-
if (toValue(options.collapsible) && options.minSizePixels.value !== undefined && toValue(options.collapseThreshold) !== undefined) {
38+
if (toValue(options.collapsible) && toValue(options.collapseThreshold) !== undefined) {
3939
let threshold: number;
4040

4141
if (thresholdLocation === 'collapse') threshold = options.minSizePixels.value - (toValue(options.collapseThreshold) ?? 0);

0 commit comments

Comments
 (0)