Skip to content

Commit 80b53fc

Browse files
authored
feat(resize-handle): add onChangeRejected() callback (#390)
1 parent 6a54d72 commit 80b53fc

File tree

5 files changed

+231
-53
lines changed

5 files changed

+231
-53
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "feat: add onChangeRejected() callback",
4+
"packageName": "@fluentui-contrib/react-resize-handle",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

packages/react-resize-handle/src/hooks/useResizeHandle.component-browser-spec.tsx

Lines changed: 133 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,20 @@ type PwMountFn = <HooksConfig>(
2121
) => Promise<PwMountResult>;
2222
type MountResult = Awaited<ReturnType<typeof mountTest>>;
2323

24+
function createSpyFn<T>() {
25+
const calls: { time: number; args: T[] }[] = [];
26+
const spy = (...args: T[]) => {
27+
calls.push({ time: performance.now(), args });
28+
};
29+
30+
spy.calls = calls;
31+
spy.clear = () => {
32+
calls.length = 0;
33+
};
34+
35+
return spy;
36+
}
37+
2438
async function mountTest(mount: PwMountFn, props: TestAreaProps = {}) {
2539
const component = await mount(<TestArea {...props} />);
2640

@@ -305,20 +319,14 @@ test.describe('useResizeHandle', () => {
305319

306320
test.describe('events', () => {
307321
test('events are called in order', async ({ mount, page }) => {
308-
const dragStartCalls: number[] = [];
309-
const dragEndCalls: number[] = [];
310-
const onChangeCalls: number[] = [];
322+
const onDragStart = createSpyFn();
323+
const onDragEnd = createSpyFn();
324+
const onChange = createSpyFn();
311325

312326
const result = await mountTest(mount, {
313-
onDragStart: () => {
314-
dragStartCalls.push(performance.now());
315-
},
316-
onDragEnd: () => {
317-
dragEndCalls.push(performance.now());
318-
},
319-
onChange: () => {
320-
onChangeCalls.push(performance.now());
321-
},
327+
onDragStart,
328+
onDragEnd,
329+
onChange,
322330
});
323331

324332
// Drag start
@@ -331,11 +339,11 @@ test.describe('useResizeHandle', () => {
331339

332340
await validateComponent(result, { value: '85px', eventType: 'mouse' });
333341

334-
expect(dragStartCalls.length).toBe(1);
335-
expect(dragEndCalls.length).toBe(0);
336-
expect(onChangeCalls.length).toBe(1);
342+
expect(onDragStart.calls).toHaveLength(1);
343+
expect(onDragEnd.calls).toHaveLength(0);
344+
expect(onChange.call).toHaveLength(1);
337345

338-
expect(dragStartCalls[0]).toBeLessThan(onChangeCalls[0]);
346+
expect(onDragStart.calls[0].time).toBeLessThan(onChange.calls[0].time);
339347

340348
// Drag end
341349
// --------------------
@@ -345,22 +353,17 @@ test.describe('useResizeHandle', () => {
345353

346354
await validateComponent(result, { value: '185px', eventType: 'mouse' });
347355

348-
expect(dragStartCalls.length).toBe(1);
349-
expect(dragEndCalls.length).toBe(1);
350-
expect(onChangeCalls.length).toBe(2);
351-
expect(onChangeCalls[1]).toBeLessThan(dragEndCalls[0]);
356+
expect(onDragStart.calls).toHaveLength(1);
357+
expect(onDragEnd.calls).toHaveLength(1);
358+
expect(onChange.calls).toHaveLength(2);
359+
expect(onChange.calls[1].time).toBeLessThan(onDragEnd.calls[0].time);
352360
});
353361

354362
test("exceeding `clamp()` don't fire 'onChange' events", async ({
355363
mount,
356364
}) => {
357-
const onChangeCalls: number[] = [];
358-
const result = await mountTest(mount, {
359-
onChange: (_, { value }) => {
360-
onChangeCalls.push(value);
361-
},
362-
useCSSClamp: true,
363-
});
365+
const onChange = createSpyFn();
366+
const result = await mountTest(mount, { onChange, useCSSClamp: true });
364367

365368
// Drag to the left
366369
// ---
@@ -375,15 +378,15 @@ test.describe('useResizeHandle', () => {
375378
});
376379

377380
await validateComponent(result, { value: '40px', eventType: 'mouse' });
378-
expect(onChangeCalls.length).toBeGreaterThan(0);
381+
expect(onChange.calls.length).toBeGreaterThan(0);
379382

380383
// ---
381384

382-
onChangeCalls.length = 0;
385+
onChange.clear();
383386
await result.dragEl.dragTo(result.spacerBefore);
384387

385388
await validateComponent(result, { value: '40px', eventType: 'mouse' });
386-
expect(onChangeCalls.length).toBe(0);
389+
expect(onChange.calls).toHaveLength(0);
387390

388391
// Drag to the right
389392
// ---
@@ -398,16 +401,112 @@ test.describe('useResizeHandle', () => {
398401
});
399402

400403
await validateComponent(result, { value: '400px', eventType: 'mouse' });
401-
expect(onChangeCalls.length).toBeGreaterThan(0);
404+
expect(onChange.calls.length).toBeGreaterThan(0);
402405

403406
// ---
404407

405-
onChangeCalls.length = 0;
406-
408+
onChange.clear();
407409
await result.dragEl.dragTo(result.spacerAfter);
408410

409411
await validateComponent(result, { value: '400px', eventType: 'mouse' });
410-
expect(onChangeCalls.length).toBe(0);
412+
expect(onChange.calls).toHaveLength(0);
413+
});
414+
415+
test.describe('onChangeRejected', () => {
416+
test.describe('CSS clamp', () => {
417+
test('is called with mouse', async ({ mount }) => {
418+
const onChangeRejected = createSpyFn();
419+
const result = await mountTest(mount, {
420+
onChangeRejected,
421+
useCSSClamp: true,
422+
});
423+
424+
await result.dragEl.dragTo(result.spacerBefore);
425+
426+
expect(onChangeRejected.calls).toHaveLength(1);
427+
expect(onChangeRejected.calls[0].args).toEqual([
428+
expect.any(Object),
429+
expect.objectContaining({
430+
type: 'mouse',
431+
rejectedValue: 0,
432+
value: 40,
433+
unit: 'px',
434+
}),
435+
]);
436+
437+
// ---
438+
439+
onChangeRejected.clear();
440+
await result.dragEl.dragTo(result.spacerAfter);
441+
442+
expect(onChangeRejected.calls).toHaveLength(1);
443+
expect(onChangeRejected.calls[0].args).toEqual([
444+
expect.any(Object),
445+
expect.objectContaining({
446+
type: 'mouse',
447+
rejectedValue: 480,
448+
value: 400,
449+
unit: 'px',
450+
}),
451+
]);
452+
});
453+
});
454+
455+
test('is called with keyboard', async ({ mount, page }) => {
456+
const onChangeRejected = createSpyFn();
457+
const result = await mountTest(mount, {
458+
onChangeRejected,
459+
useCSSClamp: true,
460+
});
461+
462+
await result.dragEl.focus();
463+
464+
for (let i = 0; i < 3; i++) {
465+
await page.keyboard.press('ArrowLeft');
466+
}
467+
468+
// "rejectedValue" will be always the same i.e. 20px due "defaultStep"
469+
expect(onChangeRejected.calls).toHaveLength(2);
470+
expect(onChangeRejected.calls[0].args).toEqual([
471+
expect.any(Object),
472+
expect.objectContaining({
473+
type: 'keyboard',
474+
rejectedValue: 20,
475+
value: 40,
476+
unit: 'px',
477+
}),
478+
]);
479+
expect(onChangeRejected.calls[1].args).toEqual([
480+
expect.any(Object),
481+
expect.objectContaining({
482+
type: 'keyboard',
483+
rejectedValue: 20,
484+
value: 40,
485+
unit: 'px',
486+
}),
487+
]);
488+
489+
// ---
490+
491+
onChangeRejected.clear();
492+
await result.resetEl.click();
493+
await result.dragEl.focus();
494+
495+
for (let i = 0; i < 20; i++) {
496+
await page.keyboard.press('ArrowRight');
497+
}
498+
499+
expect(onChangeRejected.calls).toHaveLength(2);
500+
expect(onChangeRejected.calls[0].args).toEqual([
501+
expect.any(Object),
502+
expect.objectContaining({
503+
type: 'keyboard',
504+
rejectedValue: 420,
505+
value: 400,
506+
unit: 'px',
507+
}),
508+
]);
509+
});
411510
});
412511
});
413512
});

packages/react-resize-handle/src/hooks/useResizeHandle.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,23 @@ export type UseResizeHandleParams = {
4848
* The maximum value in pixels that the element can be resized to. Only applicable if relative is false.
4949
*/
5050
maxValue?: number;
51+
5152
/**
5253
* A callback that will be called when the element is resized.
5354
*
54-
* @remarks The passed function should be memoization for better performance.
55+
* @remarks The passed function should be memoized for better performance.
5556
*/
5657
onChange?: EventHandler<ResizeHandleUpdateEventData>;
58+
59+
/**
60+
* Is called when a resize attempt didn't result in a change of size of the element.
61+
*
62+
* @remarks The passed function should be memoized for better performance.
63+
*/
64+
onChangeRejected?: EventHandler<
65+
ResizeHandleUpdateEventData & { rejectedValue: number }
66+
>;
67+
5768
/**
5869
* A callback that will be called when the resize operation starts.
5970
*/
@@ -94,6 +105,7 @@ export const useResizeHandle = (params: UseResizeHandleParams) => {
94105
minValue = 0,
95106
maxValue = Number.MAX_SAFE_INTEGER,
96107
onChange,
108+
onChangeRejected,
97109
onDragStart,
98110
onDragEnd,
99111
getA11ValueText = DEFAULT_GET_A11_VALUE_TEXT,
@@ -212,6 +224,11 @@ export const useResizeHandle = (params: UseResizeHandleParams) => {
212224
if (previousSize === newSize) {
213225
// If the size hasn't changed, we need to revert to the old value to keep the state and DOM in sync
214226
updateTargetElVariable(previousSize);
227+
onChangeRejected?.(eventData.event, {
228+
...eventData,
229+
value: currentValue.current,
230+
rejectedValue: newValue,
231+
});
215232

216233
return;
217234
}
@@ -220,7 +237,14 @@ export const useResizeHandle = (params: UseResizeHandleParams) => {
220237
updateElementsAttrs({ ...eventData, value: currentValue.current });
221238
}
222239
},
223-
[minValue, maxValue, relative, unitHandle, updateElementsAttrs]
240+
[
241+
minValue,
242+
maxValue,
243+
onChangeRejected,
244+
relative,
245+
unitHandle,
246+
updateElementsAttrs,
247+
]
224248
);
225249

226250
const setValue = React.useCallback(

packages/react-resize-handle/src/hooks/useResizeHandleExample.component-browser-spec.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type TestAreaProps = Pick<
99
| 'onDragStart'
1010
| 'onDragEnd'
1111
| 'onChange'
12+
| 'onChangeRejected'
1213
| 'relative'
1314
| 'minValue'
1415
| 'maxValue'
@@ -20,6 +21,7 @@ export function TestArea(props: TestAreaProps) {
2021
onDragEnd,
2122
onDragStart,
2223
onChange,
24+
onChangeRejected,
2325

2426
minValue,
2527
maxValue,
@@ -59,6 +61,7 @@ export function TestArea(props: TestAreaProps) {
5961
maxValue,
6062

6163
onChange: handleChange,
64+
onChangeRejected,
6265
onDragEnd,
6366
onDragStart,
6467
});

0 commit comments

Comments
 (0)