Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/sdk/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export default defineConfig(({ mode }) => ({
root: projectRoot,
resolve: {
alias: {
'@integration-components/hooks-preact': resolve(rootDir, 'packages/shared/hooks-preact/src'),
'@integration-components/core': resolve(rootDir, 'packages/shared/core/src'),
'@integration-components/types': resolve(rootDir, 'packages/shared/types/src'),
'@integration-components/utils': resolve(rootDir, 'packages/shared/utils/src'),
Expand Down
12 changes: 12 additions & 0 deletions packages/shared/hooks-preact/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "@integration-components/hooks-preact",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts",
"./*": "./src/*"
}
}
8 changes: 8 additions & 0 deletions packages/shared/hooks-preact/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@integration-components/hooks-preact",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"projectType": "library",
"sourceRoot": "packages/shared/hooks-preact/src",
"tags": ["type:shared"],
"targets": {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { render, screen } from '@testing-library/preact';
import userEvent from '@testing-library/user-event';
import { popoverUtil } from '../../components/internal/Popover/utils/popoverUtil';
import { popoverUtil } from '../../../../../src/components/internal/Popover/utils/popoverUtil';
import { ClickOutsideVariant, CONTROL_ELEMENT_PROPERTY, useClickOutside } from './useClickOutside';
import { useRef, useEffect } from 'preact/hooks';

Expand Down
130 changes: 130 additions & 0 deletions packages/shared/hooks-preact/src/element/useClickOutside.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { useCallback, useEffect, useRef } from 'preact/hooks';
import { popoverUtil } from '../../../../../src/components/internal/Popover/utils/popoverUtil';
import type { Reflexable } from '@integration-components/utils/primitives/reactive/reflex';
import type { Nullable } from '@integration-components/utils/types';
import useReflex from '../useReflex';

export const CONTROL_ELEMENT_PROPERTY: unique symbol = Symbol('__control.Elem.');

export const enum ClickOutsideVariant {
POPOVER = 'POPOVER',
DEFAULT = 'DEFAULT',
}

const onFocusout = (e: Event) => e.stopImmediatePropagation();

export const useClickOutside = <T extends Element = Element>(
rootElementRef?: Nullable<Reflexable<T>>,
callback?: (interactionKeyPressed: boolean) => void,
disableClickOutside?: boolean,
variant?: ClickOutsideVariant
) => {
const ref = useRef<Nullable<T>>(null);
const mouseDownInsideRef = useRef(false);

const handleClickOutside = useCallback(
(e: Event) => {
if (!(ref && ref.current)) return;

const eventPath: EventTarget[] = e.composedPath();

if (variant === ClickOutsideVariant.POPOVER) {
popoverUtil.closePopoversOutsideOfClick(eventPath);
} else {
let eventPathIndex = 0;
let samePath = false;
let currentElement: Element | ShadowRoot | null = eventPath[eventPathIndex] as Element | ShadowRoot;

while (currentElement instanceof Element || currentElement instanceof ShadowRoot) {
if (currentElement instanceof ShadowRoot) {
currentElement = currentElement.host;
}

if ((samePath ||= currentElement.isSameNode(ref.current))) break;

if ((currentElement as any)?.[CONTROL_ELEMENT_PROPERTY] instanceof Element) {
currentElement = (currentElement as any)[CONTROL_ELEMENT_PROPERTY];
eventPath.length = 0;
continue;
}

const nextInPath = eventPath[++eventPathIndex] as Element | ShadowRoot | undefined;
if (nextInPath) {
currentElement = nextInPath;
} else {
if (currentElement.parentElement) {
currentElement = currentElement.parentElement;
} else if (currentElement.parentNode instanceof ShadowRoot) {
currentElement = currentElement.parentNode;
} else {
currentElement = null;
}
}
}

if (callback && !samePath) callback(true);
}
},
[ref, callback, variant]
);

const handleMouseDown = useCallback((e: MouseEvent) => {
if (ref.current) {
mouseDownInsideRef.current = e.composedPath().includes(ref.current);
}
}, []);

const handleClick = useCallback(
(e: MouseEvent) => {
if (mouseDownInsideRef.current) {
mouseDownInsideRef.current = false;
} else handleClickOutside(e);
},
[handleClickOutside]
);

useEffect(() => {
if (disableClickOutside || !ref.current) return;

const element = ref.current;
const target = element.getRootNode();

if (target instanceof ShadowRoot || target instanceof Document) {
target.addEventListener('mousedown', handleMouseDown as EventListener, true);
target.addEventListener('click', handleClick as EventListener, true);

if (variant === ClickOutsideVariant.POPOVER) {
popoverUtil.add(element, callback);
}

return () => {
target.removeEventListener('mousedown', handleMouseDown as EventListener, true);
target.removeEventListener('click', handleClick as EventListener, true);

if (variant === ClickOutsideVariant.POPOVER) {
popoverUtil.remove(element);
}
};
}
}, [disableClickOutside, variant, handleMouseDown, handleClick, callback]);

return useReflex<T>(
useCallback(
(current: Nullable<T>, previous) => {
if (previous instanceof Element) {
previous.removeEventListener('focusout', onFocusout, false);
}
if (current instanceof Element) {
if (!disableClickOutside) {
current.addEventListener('focusout', onFocusout, false);
ref.current = current;
}
} else {
ref.current = null;
}
},
[disableClickOutside]
),
rootElementRef
);
};
30 changes: 30 additions & 0 deletions packages/shared/hooks-preact/src/element/useDetachedRender.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { VNode } from 'preact';
import { createPortal } from 'preact/compat';
import { useMemo, useState } from 'preact/hooks';
import type { Reflexable } from '@integration-components/utils/primitives/reactive/reflex';
import type { Nullable } from '@integration-components/utils/types';
import useReflex from '../useReflex';

const useDetachedRender = (callback: (targetElement: Element, ...args: any[]) => VNode | null, targetRef?: Nullable<Reflexable<Element>>) => {
const [render, setRender] = useState<(...args: any[]) => VNode | null>();

const renderTarget = useReflex<Element>(
useMemo(() => {
const render =
(targetElement: Element) =>
(...args: any[]) => {
const jsx = callback(targetElement, ...args);
return jsx && createPortal(jsx, targetElement);
};

setRender(undefined);

return targetElement => setRender(targetElement instanceof Element ? () => render(targetElement) : undefined);
}, [callback]),
targetRef
);

return [render, renderTarget] as const;
};

export default useDetachedRender;
33 changes: 33 additions & 0 deletions packages/shared/hooks-preact/src/element/useFocusCursor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useCallback } from 'preact/hooks';
import { ALREADY_RESOLVED_PROMISE } from '@integration-components/utils';
import type { ReflexAction } from '@integration-components/utils/primitives/reactive/reflex';
import useReflex from '../useReflex';

const useFocusCursor = (callback?: ReflexAction<Element>) => {
const finallyCallback = useCallback(
((current, previous) => {
if (previous instanceof Element) previous.setAttribute('tabindex', '-1');
if (current instanceof Element) {
current.setAttribute('tabindex', '0');
// schedule a microtask to focus the current element
ALREADY_RESOLVED_PROMISE.then(() => (current as HTMLElement)?.focus());
}
}) as ReflexAction<Element>,
[]
);

return useReflex<Element>(
useCallback(
(current, previous) => {
try {
callback?.(current, previous);
} finally {
finallyCallback(current, previous);
}
},
[callback, finallyCallback]
)
);
};

export default useFocusCursor;
Loading
Loading