Skip to content

Commit

Permalink
✨ onClickOut or onEscKey to handle modal #137
Browse files Browse the repository at this point in the history
  • Loading branch information
trydofor committed Dec 28, 2024
1 parent eabc359 commit 4ae4465
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 113 deletions.
6 changes: 6 additions & 0 deletions .changeset/itchy-lizards-talk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@fessional/razor-common": patch
"@fessional/razor-mobile": patch
---

✨ onClickOut or onEscKey to handle modal #137
12 changes: 4 additions & 8 deletions layers/common/composables/UseApiRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,10 @@ export function useApiRoute(ops?: ApiRouteOptions) {

const opt = { ...ops, ...op };

const oq = flatArray(ops.onRequest, op?.onRequest);
if (oq.length >= 1) opt.onRequest = oq;
const oqe = flatArray(ops.onRequestError, op?.onRequestError);
if (oqe.length >= 1) opt.onRequestError = oqe;
const or = flatArray(ops.onResponse, op?.onResponse);
if (or.length >= 1) opt.onResponse = or;
const ore = flatArray(ops.onResponseError, op?.onResponseError);
if (ore.length >= 1) opt.onResponseError = ore;
opt.onRequest = flatArray(ops.onRequest, op?.onRequest);
opt.onRequestError = flatArray(ops.onRequestError, op?.onRequestError);
opt.onResponse = flatArray(ops.onResponse, op?.onResponse);
opt.onResponseError = flatArray(ops.onResponseError, op?.onResponseError);

return opt;
}
Expand Down
44 changes: 44 additions & 0 deletions layers/common/composables/UseClickoutKeydown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { KeyFilter, MaybeElementRef } from '@vueuse/core';
import { onKeyDown, onClickOutside } from '@vueuse/core';

/**
* listen PointerEvent or KeyboardEvent
*
* - click outside of the element
* - keydown, default `Escape`
*/
export function useClickoutKeydown(eleRef: MaybeElementRef, keys: KeyFilter = 'Escape', handler: (evt: PointerEvent | KeyboardEvent) => void) {
let clickoutStub: (() => void) | null = null;
let keydownStub: (() => void) | null = null;

function active(force: boolean = false) {
if (clickoutStub == null) {
clickoutStub = onClickOutside(eleRef, handler);
}
else if (force) {
clickoutStub();
clickoutStub = onClickOutside(eleRef, handler);
}

if (keydownStub == null) {
keydownStub = onKeyDown(keys, handler);
}
else if (force) {
keydownStub();
keydownStub = onKeyDown(keys, handler);
}
}

function inactive() {
if (clickoutStub != null) {
clickoutStub();
clickoutStub = null;
}
if (keydownStub != null) {
keydownStub();
keydownStub = null;
}
}

return { active, inactive };
}
77 changes: 77 additions & 0 deletions layers/common/tests/UseClickoutKeydown.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { describe, it, expect, vi, afterEach } from 'vitest';
import { ref } from 'vue';
import { onClickOutside, onKeyDown } from '@vueuse/core';
import { useClickoutKeydown } from '../composables/UseClickoutKeydown';

vi.mock('@vueuse/core', () => ({
onClickOutside: vi.fn((eleRef, handler) => {
const callback = (evt: MouseEvent) => handler(evt);
document.addEventListener('click', callback);
return () => document.removeEventListener('click', callback);
}),
onKeyDown: vi.fn((keys, handler) => {
const callback = (evt: KeyboardEvent) => handler(evt);
document.addEventListener('keydown', callback);
return () => document.removeEventListener('keydown', callback);
}),
}));

describe('useClickoutKeydown', () => {
afterEach(() => {
vi.clearAllMocks();
});

it('should activate and listen for click outside events', () => {
const handler = vi.fn();
const eleRef = ref(document.createElement('div'));
const { active, inactive } = useClickoutKeydown(eleRef, 'Escape', handler);

active();

// Simulate click outside
const event = new MouseEvent('click', { bubbles: true });
document.dispatchEvent(event);

expect(onClickOutside).toHaveBeenCalledWith(eleRef, expect.any(Function));
expect(handler).toHaveBeenCalledWith(event);

inactive();

// Simulate click after inactive
document.dispatchEvent(new MouseEvent('click', { bubbles: true }));
expect(handler).toHaveBeenCalledTimes(1); // No additional calls
});

it('should activate and listen for keydown events', () => {
const handler = vi.fn();
const eleRef = ref(document.createElement('div'));
const { active, inactive } = useClickoutKeydown(eleRef, 'Escape', handler);

active();

// Simulate keydown
const event = new KeyboardEvent('keydown', { key: 'Escape' });
document.dispatchEvent(event);

expect(onKeyDown).toHaveBeenCalledWith('Escape', expect.any(Function));
expect(handler).toHaveBeenCalledWith(event);

inactive();

// Simulate keydown after inactive
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
expect(handler).toHaveBeenCalledTimes(1); // No additional calls
});

it('should force rebind listeners when active is called with force=true', () => {
const handler = vi.fn();
const eleRef = ref(document.createElement('div'));
const { active } = useClickoutKeydown(eleRef, 'Escape', handler);

active();
active(true); // Force rebinding

expect(onClickOutside).toHaveBeenCalledTimes(2);
expect(onKeyDown).toHaveBeenCalledTimes(2);
});
});
77 changes: 1 addition & 76 deletions layers/common/tests/safe-converter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { safeString, safeNumber, safeInt, safeBigint, safeBoolean, safeBoolTof, safeBoolNum, safeValues, safeKeys,
safeEntries, safeJson, safeObjMap, safeArrSet, safeMapObj,
safeSetArr, safeArray, safeValue, safeConvert,
flatArray, flatObject, mergeObject } from '../utils/safe-converter';
flatArray } from '../utils/safe-converter';

describe('safeConvert', () => {
it('should return converted value when input is valid', () => {
Expand Down Expand Up @@ -627,78 +627,3 @@ describe('flatArray', () => {
expect(result).toEqual([]);
});
});

describe('flatObject', () => {
it('should flatten objects with scalar values', () => {
const result = flatObject('key', { key: 1 }, { key: 2 }, { key: 3 });
expect(result).toEqual([1, 2, 3]);
});

it('should flatten objects with array values', () => {
const result = flatObject('key', { key: [1, 2] }, { key: [3, 4] });
expect(result).toEqual([1, 2, 3, 4]);
});

it('should handle mixed scalar and array values', () => {
const result = flatObject('key', { key: 1 }, { key: [2, 3] }, { key: 4 });
expect(result).toEqual([1, 2, 3, 4]);
});

it('should handle objects with null or undefined values', () => {
const result = flatObject('key', { key: 1 }, { key: null }, { key: undefined }, { key: [2, null] });
expect(result).toEqual([1, 2]);
});

it('should return an empty array if no objects are provided', () => {
const result = flatObject('key');
expect(result).toEqual([]);
});

it('should handle an empty array value', () => {
const result = flatObject('key', { key: [] }, { key: [1, 2] });
expect(result).toEqual([1, 2]);
});

it('should handle an object key with nested arrays', () => {
const result = flatObject('key', { key: [1, [2, 3]] });
expect(result).toEqual([1, [2, 3]]); // flatObject does not deeply flatten
});
});

describe('mergeObject', () => {
it('should merge scalar values into an object property', () => {
const obj = { key: 1 };
mergeObject('key', obj, { key: 2 }, { key: 3 });
expect(obj).toEqual({ key: [1, 2, 3] });
});

it('should merge array values into an object property', () => {
const obj = { key: [1, 2] };
mergeObject('key', obj, { key: [3, 4] });
expect(obj).toEqual({ key: [1, 2, 3, 4] });
});

it('should merge mixed scalar and array values', () => {
const obj = { key: 1 };
mergeObject('key', obj, { key: [2, 3] }, { key: 4 });
expect(obj).toEqual({ key: [1, 2, 3, 4] });
});

it('should update object property to scalar if only one value remains', () => {
const obj = { key: [1] };
mergeObject('key', obj, { key: null });
expect(obj).toEqual({ key: 1 });
});

it('should handle null or undefined values gracefully', () => {
const obj = { key: 1 };
mergeObject('key', obj, { key: null }, { key: undefined });
expect(obj).toEqual({ key: 1 });
});

it('should not modify the object if no additional values are provided', () => {
const obj = { key: 1 };
mergeObject('key', obj);
expect(obj).toEqual({ key: 1 });
});
});
30 changes: 1 addition & 29 deletions layers/common/utils/safe-converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ export function safeSetArr<T>(set?: Maybe<Set<T>>): T[] {
}

/**
* flat value T (not array) or its array to array,
* flat value T or its array to array,
* - `1, [2, 3], 4 => [1, 2, 3, 4]`
* - `[], [1, 2], [undefined], 3 => [1, 2, 3]`
*/
Expand All @@ -354,31 +354,3 @@ export function flatArray<T>(...items: Array<undefined | null | T | T[]>): T[] {
}
return arr;
}

/**
* flat obj's property T (not array) by its key to array
* - size = 0, key or value is null
* - size = 1, property should be T
* - size > 1, property should be T[]
*/
export function flatObject<T>(key: string, ...items: Array<{ [K in typeof key]?: T | T[] }>): T[] {
const its = items.map(it => it?.[key]).filter(it => it != null);
return flatArray<T>(...its);
}

/**
* merge obj's property T (not array) with items by key
* - size = 0, key or value is null
* - size = 1, property should be T
* - size > 1, property should be T[]
*/
export function mergeObject<T>(key: string, obj: { [K in typeof key]?: T | T[] }, ...items: Array<{ [K in typeof key]?: T | T[] }>) {
const its = items.map(it => it?.[key]).filter(it => it != null);
const arr = flatArray<T>(obj[key], ...its);
if (arr.length == 1) {
obj[key] = arr[0];
}
else if (arr.length > 1) {
obj[key] = arr;
}
}

0 comments on commit 4ae4465

Please sign in to comment.