Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Utility hooks additions #867

Merged
merged 8 commits into from
Feb 10, 2025
59 changes: 59 additions & 0 deletions src/core/hooks/composeEventHandlers/composeEventHandlers.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import composeEventHandlers from './index';

const OnlyOriginalHandlerWithPreventDefault = ({
checkForDefaultPrevented = true
}: {
checkForDefaultPrevented?: boolean;
}) => {
const originalClickHandler = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
// we prevent default, so we should not see our handler
console.log('RETURNING_ORIGINAL_HANDLER');
};
const ourClickHandler = () => {
// This won't be triggered because we prevent default in the original handler
console.log('RETURNING_OUR_HANDLER');

console.log('RETURN_OUR_HANDLER_WITH_CHECK_FOR_DEFAULT_PREVENTED');
};
const composedHandleClick = composeEventHandlers(
originalClickHandler,
ourClickHandler,
{ checkForDefaultPrevented }
);

return <button onClick={composedHandleClick}>Click Me</button>;
};

describe('composeEventHandlers', () => {
let consoleLogSpy: jest.SpyInstance;

beforeEach(() => {
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
});

afterEach(() => {
consoleLogSpy.mockRestore();
});

test('should compose event handlers', () => {
render(<OnlyOriginalHandlerWithPreventDefault />);
const button = screen.getByText('Click Me');
fireEvent.click(button);
expect(consoleLogSpy).toHaveBeenCalledWith('RETURNING_ORIGINAL_HANDLER');
expect(consoleLogSpy).not.toHaveBeenCalledWith('RETURNING_OUR_HANDLER');
});

test('should compose event handlers with checkForDefaultPrevented false', () => {
// if checkForDefaultPrevented is false, we should see our handler
render(<OnlyOriginalHandlerWithPreventDefault checkForDefaultPrevented={false} />);
const button = screen.getByText('Click Me');
fireEvent.click(button);
expect(consoleLogSpy).toHaveBeenCalledWith('RETURNING_ORIGINAL_HANDLER');

// even if the event is prevented, we should see our handler - the function completely ignores the event prevent default
expect(consoleLogSpy).toHaveBeenCalledWith('RETURN_OUR_HANDLER_WITH_CHECK_FOR_DEFAULT_PREVENTED');
});
});
22 changes: 22 additions & 0 deletions src/core/hooks/composeEventHandlers/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
type EventHandler<E = React.SyntheticEvent> = (event: E) => void;

function composeEventHandlers<E extends React.SyntheticEvent>(
originalEventHandler?: EventHandler<E>,
ourEventHandler?: EventHandler<E>,
{ checkForDefaultPrevented = true } = {}
) {
// Returns a function that handles the event
return function handleEvent(event: E) {
// If the original event handler is defined, call it
if (typeof originalEventHandler === 'function') {
originalEventHandler(event);
}

// If the default prevented flag is false or the event is not prevented, call our event handler
if (checkForDefaultPrevented === false || !event.defaultPrevented) {
return ourEventHandler?.(event);
}
};
}

export default composeEventHandlers;
14 changes: 14 additions & 0 deletions src/core/hooks/useLayoutEffect/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// A simple wrapper for useLayoutEffect that does not throw an error on server components

import { useLayoutEffect as ReactUseLayoutEffect } from 'react';

// When we use useLayoutEffect in a server component, it will throw an error.
// One of the hooks that do not work on server components
// wrapping it this way and using it will make sure errors are not thrown and fail silently
// If it's server, we just return a noop function (no operation)

// Radix UI does it this way - https://github.com/radix-ui/primitives/blob/main/packages/react/use-layout-effect/src/use-layout-effect.tsx
//
const useLayoutEffect = globalThis.document ? ReactUseLayoutEffect : () => {};

export default useLayoutEffect;
18 changes: 18 additions & 0 deletions src/core/hooks/useLayoutEffect/useLayoutEffect.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import useLayoutEffect from './index';
import { render } from '@testing-library/react';

const TestComponent = () => {
// invoke useLayoutEffect and check if the component still mounts as expected
useLayoutEffect(() => {
// mounts
}, []);
return <div>Hello</div>;
};

describe('useLayoutEffect', () => {
test('Test for SSR environment and check if the component still mounts as expected', (done) => {
render(<TestComponent />);
done();
});
kotAPI marked this conversation as resolved.
Show resolved Hide resolved
});
17 changes: 9 additions & 8 deletions src/core/primitives/Toggle/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import React, { useState } from 'react';

import Primitive from '~/core/primitives/Primitive';
import composeEventHandlers from '~/core/hooks/composeEventHandlers';

export interface TogglePrimitiveProps {
defaultPressed?: boolean;
pressed?: boolean;
children?: React.ReactNode;
className?: string;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
onKeyDown?: (event: React.KeyboardEvent<HTMLButtonElement>) => void;
label?: string;
disabled?: boolean;
onPressedChange?: (isPressed: boolean) => void;
Expand All @@ -28,7 +31,7 @@ const TogglePrimitive = ({
const isControlled = controlledPressed !== undefined;
const isPressed = isControlled ? controlledPressed : uncontrolledPressed;

const handlePressed = () => {
const handleTogglePressed = composeEventHandlers(props.onClick, () => {
if (disabled) {
return;
}
Expand All @@ -38,14 +41,12 @@ const TogglePrimitive = ({
setUncontrolledPressed(updatedPressed);
}
onPressedChange(updatedPressed);
};
});

const handleKeyDown = (event: React.KeyboardEvent) => {
// TODO: Should these be handled by the browser?
// Or should we add these functionalities inside the ButtonPrimitive?
const handleKeyDown = (event: any) => {
if (event.key === ' ' || event.key === 'Enter') {
event.preventDefault();
handlePressed();
handleTogglePressed(event);
}
};
kotAPI marked this conversation as resolved.
Show resolved Hide resolved

Expand All @@ -54,8 +55,8 @@ const TogglePrimitive = ({
ariaAttributes['aria-disabled'] = disabled ? 'true' : 'false';

return <Primitive.button
onClick={handlePressed}
onKeyDown={handleKeyDown}
onClick={composeEventHandlers(props.onClick, handleTogglePressed)}
onKeyDown={composeEventHandlers(props.onKeyDown, handleKeyDown)}
data-state={isPressed ? 'on' : 'off'}
disabled={disabled}
{...ariaAttributes}
Expand Down
Loading