Skip to content
Merged
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
18 changes: 3 additions & 15 deletions packages/react/src/components/DateInput/useDateInputKeyboard.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
'use client';

import { KeyboardEventHandler, RefObject, useEffect } from 'react';
import { KeyboardEventHandler, RefObject } from 'react';

import { DateInputFormatter } from './DateInput.types';

import { DateInputSelection } from './useDateInputSelection';
import { DateInputState } from './useDateInputState';
import { isDigit } from './utils';

import { useEvent } from '../../hooks';
import { useElementEventListener, useEvent } from '../../hooks';

/** The keys the input acts on itself. Everything else is left to the browser and to the form around it. */
const CONSUMED_KEYS = new Set([
Expand Down Expand Up @@ -274,19 +274,7 @@ export const useDateInputKeyboard = ({
}
});

useEffect(() => {
const node = ref.current;

if (!node) {
return;
}

node.addEventListener('beforeinput', onBeforeInput);

return () => {
node.removeEventListener('beforeinput', onBeforeInput);
};
}, [onBeforeInput]);
useElementEventListener(ref, 'beforeinput', onBeforeInput);

return { onKeyDown };
};
48 changes: 25 additions & 23 deletions packages/react/src/components/PhoneField/PhoneField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,16 @@ import {
PhoneFieldPatch,
} from './PhoneField.functions';

import { useControlled, useEvent, useForkRef, useLatest, useMenu, useMenuVisibility } from '../../hooks';
import {
useControlled,
useElementEventListener,
useEvent,
useForkRef,
useLatest,
useMenu,
useMenuVisibility,
useRefState,
} from '../../hooks';
import { IconGlobalLineW500, IconMenuDownFillW300 } from '../../icons';
import { useDefaultProps } from '../../theming';
import { AutocompleteMenu } from '../AutocompleteMenu';
Expand Down Expand Up @@ -93,7 +102,7 @@ export const PhoneField = memo(function PhoneField({

const ref = useRef<HTMLDivElement | null>(null);
const rootRef = useForkRef(ref, inRef);
const [inputRef, setInputRefState] = useState<HTMLInputElement | null>(null);
const [inputRef, setInputRefState] = useRefState<HTMLInputElement>();
// The field never renders a textarea, so the control it reports back is always an input.
const setInputRef = useCallback((node: FormFieldInputElement | null) => {
setInputRefState(node as HTMLInputElement | null);
Expand Down Expand Up @@ -182,11 +191,13 @@ export const PhoneField = memo(function PhoneField({

event.preventDefault();

if (!inputRef) {
const input = inputRef.current;

if (!input) {
return;
}

const { selectionStart, selectionEnd, value } = inputRef;
const { selectionStart, selectionEnd, value } = input;

if (selectionStart === null || selectionEnd === null) {
return;
Expand Down Expand Up @@ -273,7 +284,7 @@ export const PhoneField = memo(function PhoneField({
onChange?.({ target: { name, value: phone } } as React.ChangeEvent<HTMLInputElement>);

requestAnimationFrame(() => {
inputRef.setSelectionRange(newSelectionStart, newSelectionEnd);
input.setSelectionRange(newSelectionStart, newSelectionEnd);
});
}
});
Expand Down Expand Up @@ -315,17 +326,8 @@ export const PhoneField = memo(function PhoneField({
}
});

useEffect(() => {
if (inputRef) {
inputRef.addEventListener('beforeinput', onBeforeInput);
inputRef.addEventListener('input', onInput);

return () => {
inputRef.removeEventListener('beforeinput', onBeforeInput);
inputRef.removeEventListener('input', onInput);
};
}
}, [inputRef]);
useElementEventListener(inputRef, 'beforeinput', onBeforeInput);
useElementEventListener(inputRef, 'input', onInput);

const onInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
onKeyDownProp?.(e);
Expand Down Expand Up @@ -362,7 +364,9 @@ export const PhoneField = memo(function PhoneField({
).text;

const onCheckCaretPosition = useEvent((e: React.SyntheticEvent<HTMLElement>) => {
if (inputRef && inputRef.selectionStart === inputRef.selectionEnd) {
const input = inputRef.current;

if (input && input.selectionStart === input.selectionEnd) {
let min = 1;

if (countries.length === 1) {
Expand All @@ -373,9 +377,9 @@ export const PhoneField = memo(function PhoneField({
}
}

if (inputRef.selectionStart !== null && inputRef.selectionStart < min) {
if (input.selectionStart !== null && input.selectionStart < min) {
e.preventDefault();
inputRef.setSelectionRange(min, min);
input.setSelectionRange(min, min);
}
}
});
Expand Down Expand Up @@ -427,9 +431,7 @@ export const PhoneField = memo(function PhoneField({
onChange?.({ target: { name, value: newValue } } as React.ChangeEvent<HTMLInputElement>);

requestAnimationFrame(() => {
if (inputRef) {
inputRef.focus();
}
inputRef.current?.focus();
});
}
};
Expand Down Expand Up @@ -483,7 +485,7 @@ export const PhoneField = memo(function PhoneField({
onExited();

setTimeout(() => {
if (inputRef !== document.activeElement) {
if (inputRef.current !== document.activeElement) {
onBlurProp?.({} as never);
}
}, 1);
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export { useCookie } from './useCookie';
export { useDebounce } from './useDebounce';
export { useDocumentEventListener } from './useDocumentEventListener';
export { useDragOver } from './useDragOver';
export { useElementEventListener } from './useElementEventListener';
export { useEnhancedEffect } from './useEnhancedEffect';
export { useEvent } from './useEvent';
export { useForceUpdate } from './useForceUpdate';
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/hooks/useElementEventListener/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useElementEventListener } from './useElementEventListener';
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Meta } from '@storybook/addon-docs/blocks';
import LinkTo from '@storybook/addon-links/react';
import { TableFunction } from '~storybook/components/TableFunction';

<Meta title="Hooks API/useElementEventListener" />

# useElementEventListener API

```js
import { useElementEventListener } from '@esfront/react';
```

<TableFunction name="useElementEventListener" />

<br />

<br />

## Demos

<ul>
<li>
<LinkTo kind="hooks-useElementEventListener" story="demo">
<code>useElementEventListener</code>
</LinkTo>
</li>
</ul>
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useRef, useState } from 'react';

import { Meta, StoryObj } from '@storybook/react-vite';

import { useElementEventListener } from './useElementEventListener';

const meta: Meta = {
tags: ['autodocs'],
title: 'Hooks/useElementEventListener',
parameters: {
references: ['useElementEventListener'],
},
};

export default meta;

type Story = StoryObj;

export const Demo: Story = {
render: function Render() {
const [count, setCount] = useState(0);

const ref = useRef<HTMLDivElement | null>(null);

useElementEventListener(ref, 'click', () => {
setCount((value) => value + 1);
});

return (
<div className="flex flex-col gap-16" style={{ alignItems: 'flex-start' }}>
<div
ref={ref}
className="body100"
style={{
padding: '16px',
border: '1px solid var(--es-mono-a-a150)',
borderRadius: '4px',
}}
>
Click me
</div>
<div className="body100">
Count of clicks on the box: <b>{count}</b>.
</div>
</div>
);
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use client';

import { RefObject, useEffect } from 'react';

import { useLatest } from '../useLatest';

/**
* The hook that sets up a function that will be called whenever the specified event is delivered to an element. The
* listener is reattached whenever the observed element changes.
* @param element A reference to an element to listen on.
* @param type A case-sensitive string representing the event type to listen for.
* @param callback The function that receives a notification when an event of the specified type occurs.
* @param options An object that specifies characteristics about the event listener.
*/
export const useElementEventListener = <K extends keyof HTMLElementEventMap>(
element: RefObject<HTMLElement | null>,
type: K,
callback: (event: HTMLElementEventMap[K]) => any,
options?: boolean | AddEventListenerOptions
) => {
const latestCallback = useLatest(callback);

useEffect(() => {
const target = element.current;

if (target) {
const onEvent = (event: HTMLElementEventMap[K]) => {
return latestCallback.current(event);
};

target.addEventListener(type, onEvent, options);

return () => {
target.removeEventListener(type, onEvent);
};
}
}, [element.current, type]);
};
14 changes: 4 additions & 10 deletions packages/react/src/hooks/useScrollPosition/useScrollPosition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { RefObject, useEffect, useState } from 'react';

import { useElementEventListener } from '../useElementEventListener';
import { useEvent } from '../useEvent';
import { useMutationObserver } from '../useMutationObserver';
import { useResizeObserver } from '../useResizeObserver';
Expand All @@ -28,6 +29,8 @@ export type UseScrollPositionResult = {

const MUTATION_OBSERVER_OPTIONS: MutationObserverInit = { childList: true, subtree: true };

const SCROLL_LISTENER_OPTIONS: AddEventListenerOptions = { passive: true };

const INITIAL_RESULT: UseScrollPositionResult = {
isScrollableX: false,
isScrollableY: false,
Expand Down Expand Up @@ -86,19 +89,10 @@ export const useScrollPosition = (

useResizeObserver(element, update);
useMutationObserver(element, update, MUTATION_OBSERVER_OPTIONS);
useElementEventListener(element, 'scroll', update, SCROLL_LISTENER_OPTIONS);

useEffect(() => {
update();

const target = element.current;

if (target) {
target.addEventListener('scroll', update, { passive: true });

return () => {
target.removeEventListener('scroll', update);
};
}
}, [element.current, threshold]);

return result;
Expand Down
Loading