react-aria and Typescript building a popover component error with value passing - reactjs

Morning, all.
I have an issue I have been struggling with for a little while. I have been using react-aria to build a popover in Storybook. This popover is built in two components, the first one is the popover itself, this works fine:
import { StyledPopover } from './PopOver.styles';
import {
DismissButton,
FocusScope,
mergeProps,
useDialog,
useModal,
useOverlay,
} from 'react-aria';
import React, { forwardRef } from 'react';
import { useObjectRef } from '#react-aria/utils';
export interface PopoverProps {
title: string;
children: React.ReactNode;
isOpen: boolean;
onClose: () => void;
}
const Popover = React.forwardRef<HTMLDivElement, PopoverProps>(
({ title, children, isOpen, onClose }, ref) => {
const forwardRef = useObjectRef(ref);
// Handle interacting outside the dialog and pressing
// the Escape key to close the modal.
const { overlayProps } = useOverlay(
{
onClose,
isOpen,
isDismissable: true,
},
forwardRef
);
// Hide content outside the modal from screen readers.
const { modalProps } = useModal();
// Get props for the dialog and its title
const { dialogProps, titleProps } = useDialog({}, forwardRef);
return (
<FocusScope restoreFocus>
<StyledPopover
{...mergeProps(overlayProps, dialogProps, modalProps)}
ref={ref}
>
<h3 {...titleProps} style={{ marginTop: 0 }}>
{title}
</h3>
{children}
<DismissButton onDismiss={onClose} />
</StyledPopover>
</FocusScope>
);
}
);
export { Popover };
Then I have the the button and the state:
import React from 'react';
import {
OverlayContainer,
useOverlayPosition,
useOverlayTrigger,
} from 'react-aria';
import { Button } from '../Button';
import { useOverlayTriggerState } from 'react-stately';
import { Popover } from 'Atoms/Popover/Popover';
export interface PopoverButtonProps {
title: string;
buttonText: string;
children: React.ReactNode;
disabled: boolean;
}
const PopoverButton = ({
buttonText,
title,
children,
disabled,
}: PopoverButtonProps) => {
const state: any = useOverlayTriggerState({});
const triggerRef = React.useRef<HTMLButtonElement>(null);
const overlayRef: any = React.useRef<HTMLDivElement>(null);
// Get props for the trigger and overlay. This also handles
// hiding the overlay when a parent element of the trigger scrolls
// (which invalidates the popover positioning).
const { triggerProps, overlayProps } = useOverlayTrigger(
{ type: 'dialog' },
state,
triggerRef
);
// Get popover positioning props relative to the trigger
const { overlayProps: positionProps } = useOverlayPosition({
targetRef: triggerRef,
overlayRef,
placement: 'top',
offset: 5,
isOpen: state.isOpen,
});
//
// const handleOnClick = (e: any) => {
// console.log(triggerProps);
// triggerProps.onPress && triggerProps.onPress(e);
// };
console.log(triggerProps);
return (
<>
<Button
onClick={(e: React.MouseEvent<HTMLInputElement>) =>
triggerProps.onPress && triggerProps.onPress(e)
}
style="secondary"
size="small"
disabled={disabled}
>
{buttonText}
</Button>
{state.isOpen && (
<OverlayContainer>
<Popover
{...overlayProps}
{...positionProps}
ref={overlayRef}
title={title}
isOpen={state.isOpen}
onClose={state.close}
>
{children}
</Popover>
</OverlayContainer>
)}
</>
);
};
export { PopoverButton };
Now, react-aria useButton takes an onPress, not an onClick.
So, in my Button component I have casted the onClick like this:
import classNames from 'classnames';
import { forwardRef } from 'react';
import { StyledButton } from './Button.styles';
import { useButton } from 'react-aria';
import { useObjectRef } from '#react-aria/utils';
import React from 'react';
export interface ButtonProps
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'style'> {
children: React.ReactNode;
type?: 'submit' | 'button' | 'reset';
style?: 'primary' | 'secondary' | 'icon' | 'text';
size?: 'large' | 'medium' | 'small';
block?: boolean;
disabled?: boolean;
testId?: string;
onPress?: () => void;
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
children,
type,
style = 'primary',
size = 'large',
block = false,
disabled = false,
testId,
onPress,
...props
},
ref
) => {
const classes = classNames(style, `btn-${size}`, {
block,
});
const objRef = useObjectRef(ref);
const { buttonProps } = useButton({ onPress }, objRef);
return (
<StyledButton
{...buttonProps}
className={classes}
onClick={onPress}
type={type}
disabled={disabled}
data-testid={testId}
ref={ref}
{...props}
>
{children}
</StyledButton>
);
}
);
export { Button };
In my popoverButton component, I am passing (e) as it is required for react-aria:
onClick={(e: React.MouseEvent<HTMLInputElement>) =>
triggerProps.onPress && triggerProps.onPress(e)
}
However, I am getting these two errors -
on (e) -
Argument of type 'MouseEventHandler<HTMLButtonElement>' is not assignable to parameter of type 'PressEvent'.
Type 'MouseEventHandler<HTMLButtonElement>' is missing the following properties from type 'PressEvent': type, pointerType, target, shiftKey, and 3 more.ts(2345)
onClick -
Type '(e: React.MouseEventHandler<HTMLButtonElement>) => void | undefined' is not assignable to type 'MouseEventHandler<HTMLButtonElement>'.
Types of parameters 'e' and 'event' are incompatible.
Type 'MouseEvent<HTMLButtonElement, MouseEvent>' is not assignable to type 'MouseEventHandler<HTMLButtonElement>'.
Type 'MouseEvent<HTMLButtonElement, MouseEvent>' provides no match for the signature '(event: MouseEvent<HTMLButtonElement, MouseEvent>): void'.ts(2322)
index.d.ts(1446, 9): The expected type comes from property 'onClick' which is declared here on type 'IntrinsicAttributes & ButtonProps & RefAttributes<HTMLButtonElement>'
Now, the button works and the popup does appear, however it only disappears when clicking the button when it should dismiss when clicking anywhere on the screen. I think this is down to the issues I currently have with the onClick and onPress?
Any ideas?

Related

Storybook. Why I am getting as JSON in controls when set props parameter as boolean | 'string'?

Now, I am making new storybook with typescript.
This is my component and it's props
import React, { ChangeEvent, FocusEvent, ReactElement, useEffect, useRef } from 'react';
import classNames from 'classnames';
export type CheckboxProps = {
id?: string;
name?: string;
className?: string;
value?: string | string[] | number;
tabIndex?: number;
isValid?: boolean;
checked?: boolean | 'indeterminate';
required?: boolean;
disabled?: boolean;
onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
onBlur?: (event: FocusEvent<HTMLInputElement>) => void;
};
const Checkbox = (props: CheckboxProps): ReactElement => {
const { id, name, className, value, tabIndex, isValid = true, checked, required, disabled, onChange, onBlur } = props;
const checkboxElement = useRef<HTMLInputElement>(null);
useEffect(() => {
if (checkboxElement.current !== null) {
checkboxElement.current.indeterminate = checked === 'indeterminate';
}
}, [checked]);
return (
<input
id={id}
className={classNames('form-check-input', className, !isValid ? 'is-invalid' : '')}
ref={checkboxElement}
type="checkbox"
name={name}
value={value}
checked={checked === 'indeterminate' || checked}
required={required}
tabIndex={tabIndex}
disabled={disabled}
onChange={onChange}
onBlur={onBlur}
/>
);
};
export default Checkbox;
And this is .stories.tsx file.
import React from 'react';
import { ComponentStory, ComponentMeta } from '#storybook/react';
import 'bootstrap/dist/css/bootstrap.css';
import Checkbox from './Checkbox';
export default {
title: 'Components/Blocks/Checkboxs/Checkbox',
component: Checkbox
} as ComponentMeta<typeof Checkbox>;
const Template: ComponentStory<typeof Checkbox> = args => <Checkbox {...args} />;
export const CheckboxComponent = Template.bind({});
In here, checked parameter is boolean | string.
But in storybook control, I can see checked value is Set object , and its' value is JSON.
So users can't know which value should enter to there.
How can I show expected props values in storybook control?

react/typescript 'string | undefined' is not assignable to type 'string' even I already checked the type

Button
import React from 'react';
import './button.css';
interface ButtonProps {
primary?: boolean;
backgroundColor?: string;
size?: 'small' | 'medium' | 'large';
label: string;
onClick?: () => void;
}
export const Button = ({
primary = false,
size = 'medium',
backgroundColor,
label,
...props
}: ButtonProps) => {
const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
return (
<button
type="button"
className={['storybook-button', `storybook-button--${size}`, mode].join(' ')}
style={{ backgroundColor }}
{...props}
>
{label}
</button>
);
};
Button.stories
import React from 'react';
import { ComponentStory, ComponentMeta } from '#storybook/react';
import { Button } from '.';
export default {
title: 'Example/Button',
component: Button,
argTypes: {
backgroundColor: { control: 'color' },
},
} as ComponentMeta<typeof Button>;
const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />;
export const Primary = Template.bind({});
Primary.args = {
primary: true,
label: 'Button',
};
...
Button.test
import { render } from '#testing-library/react';
import { Button } from '.';
import { Primary } from './Button.stories';
test('should render a button', () => {
render(<Button label="Click Me!" />);
});
test('should render a primary button', () => {
if (typeof Primary.args === 'undefined') {
throw new Error('Args is undfeined');
} else if (typeof Primary.args.label !== 'string') {
throw new Error('The type of label is not string');
} else if (Primary.args.label === undefined) {
return;
}
/**
Types of property 'label' are incompatible.
Type 'string | undefined' is not assignable to type 'string'.
Type 'undefined' is not assignable to type 'string'.
*/
render(<Primary {...Primary.args} />);
});
I wrote some test code for the button component that was made by Storybook default.
Even though I already checked the type of the label prop if the type is undefined or not before the props are delivered, it throws a syntax error 'string | undefined' is not assignable to type 'string'.
Why did I do wrong?
This is a similar question to this:
Typescript: Type 'string | undefined' is not assignable to type 'string'
You can either use a Non-Null Assersion ( ! )
OR
You can use a logical-OR Expression Operator to state an optional value ( || )
(There are other methods too, although these are often the most popular)

TypeScript Polymorphic Components in React with conditional props

I'm trying to write a Polymorphic component in React that supports both host elements such as (div, button, etc) and custom React components.
but there's a constraint of needing whatever component is passed in to the as prop that it must have an onClick prop
here's my work in progress.
import React, { useCallback, useRef, useLayoutEffect } from 'react';
export type GaClickTrackerProps<Type extends React.ElementType> = {
as: Type;
fieldsObject: {
hitType: UniversalAnalytics.HitType; // 'event'
eventCategory: string;
eventAction: string;
eventLabel?: string | undefined;
eventValue?: number | undefined;
nonInteraction?: boolean | undefined;
};
onClick?: (...args: any[]) => any;
} & React.ComponentPropsWithoutRef<Type>;
export function GaClickTracker<Type extends React.ElementType>({
onClick,
fieldsObject,
as: Component,
...props
}: GaClickTrackerProps<Type>) {
const fieldsObjectRef = useRef(fieldsObject);
const onClickRef = useRef(onClick);
useLayoutEffect(() => {
onClickRef.current = onClick;
fieldsObjectRef.current = fieldsObject;
});
const handleClick: React.MouseEventHandler<Type> = useCallback((event) => {
onClickRef.current?.(event);
if (fieldsObjectRef.current) {
ga('send', fieldsObjectRef.current);
}
}, []);
return (
<Component onClick={handleClick} {...props} />
);
}
Instead of going into the "polymorphic component" direction, for this particular issue it would be far better to have an util function like this:
import type { MouseEventHandler } from "react";
type Fields = {
eventAction: string;
eventCategory: string;
eventLabel?: string | undefined;
eventValue?: number | undefined;
hitType: UniversalAnalytics.HitType;
nonInteraction?: boolean | undefined;
};
export const sendAnalytics =
(fields?: Fields) =>
<Type>(handler?: MouseEventHandler<Type>): MouseEventHandler<Type> =>
event => {
handler?.(event); // We call the passed handler first
// And we only call `ga` if the we have fields and the default wasn't prevented
return !event.defaultPrevented && fields
? ga("submit", fields)
: undefined;
};
And then you can use it like this:
import { sendAnalytics } from "~/utils/sendAnalytics.js";
const sendIDKAnalytics = sendAnalytics({
eventAction: "IDK",
eventCategory: "IDK",
hitType: "IDK",
});
export const TrackedButton = ({ onClick, ...props }) => (
<button onClick={sendIDKAnalytics(onClick)} {...props} />
);
Being a curried function, you could even reuse the same settings in different components if you wanted. And if you want to "Reactify" it further, you could make it a hook, like useAnalytics and go from there.
The "polymorphic" approach would make it unnecessarily complex with no real gains to DX:
import { createElement } from "react";
// This:
const Polymorphic = ({ as: Component, ...props }) => <Component {...props} />;
// Is not different from just doing this:
const Polymorphic = ({ as, ...props }) => createElement(as, props);
// And when using any of those, your code would look something like this,
// which is not ideal:
<Polymorphic as="main">
<Polymorphic as="nav">
<Polymorphic as="a" href="https://example.com">
Hello
</Polymorphic>
</Polymorphic>
</Polymorphic>;

How to add static props type for ForwardRefExoticComponent

I have a Modal FC, I need to add two static methods show and hide to it. I have no idea how to add the ModalStaticProps type for the component. So that I can assign show and hide to Modal directly without using type assertion.
import React, { ReactNode, RefAttributes } from 'react';
interface ModalProps {
title: ReactNode;
}
interface ModalStaticProps {
show(): void;
hide(): void;
}
const Modal: React.ForwardRefExoticComponent<ModalProps & RefAttributes<HTMLDivElement>> = React.forwardRef<
HTMLDivElement,
ModalProps
>(({ title }: ModalProps, ref) => {
return <div ref={ref}>{title}</div>;
});
// I want to this after add `ModalStaticProps` type
// Modal.show = () => { };
// Modal.hide = () => { };
// Not this
const ModalComponent = Modal as React.ForwardRefExoticComponent<ModalProps & RefAttributes < HTMLDivElement >> & ModalStaticProps
ModalComponent.show = () => { };
ModalComponent.hide = () => { };
function Consumer() {
return <div onClick={() => ModalComponent.show()} />
}
TypeScript Playground
It is doable.
See Properties declarations on functions. It is possible to do, in a natural way, but it might break your ref types.
Hence, I decided just use Object.assign
See example:
import React, { ReactNode, RefAttributes, ForwardRefExoticComponent } from 'react';
interface ModalProps {
title: ReactNode;
}
interface ModalStaticProps {
show(): void;
hide(): void;
}
const STATIC_PROPS: ModalStaticProps = {
show: () => { },
hide: () => { }
}
const withStaticProps = <T,>(
forwarded: ForwardRefExoticComponent<ModalProps & RefAttributes<HTMLDivElement>>,
staticProps: T
) => Object.assign(forwarded, staticProps)
const Modal = React.forwardRef<
HTMLDivElement,
ModalProps
>(({ title }: ModalProps, ref) => <div ref={ref}>{title}</div>)
const ModalComponent = withStaticProps(Modal, STATIC_PROPS)
function Consumer() {
return <div onClick={() => ModalComponent.show()} />
}
Playground
See my question regarding this problem

Creating a Custom Button in React Typescript and add onClick Event

I am trying to create a custom button component in a React Typescript project that use React Hooks and Styled components.
// Button.tsx
import React, { MouseEvent } from "react";
import styled from "styled-components";
export interface IButtonProps {
children?: React.ReactNode;
props?: any;
onClick?:
| ((event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void)
| undefined;
}
const Button: React.FC<IButtonProps> = ({
children,
onClick = () => {},
...props
}) => {
const handleOnClick = (e: MouseEvent<HTMLButtonElement>) => {
onClick(e);
};
return (
<ButtonStyles onClick={handleOnClick} {...props}>
{children}
</ButtonStyles>
);
};
export default Button;
const ButtonStyles = styled.button``;
This is the component I want to use my custom Button
// Login.tsx
import React from "react";
import Button from "../../components/Generic/Button/Button";
const Login: React.FC = () => {
const clickMe = () => {
console.log("Button Clicks");
};
return (
<div>
<Button onClick={clickMe}>My Button</Button>
</div>
);
};
export default Login;
I want to click on my custom button and want to print the console log message when I click on it. But I am getting the following error. How to resolve this issue?
Argument of type 'MouseEvent<HTMLButtonElement, MouseEvent>' is not assignable to parameter of type 'MouseEvent<HTMLButtonElement, MouseEvent<Element, MouseEvent>>'.
Type 'MouseEvent' is missing the following properties from type 'MouseEvent<Element, MouseEvent>': nativeEvent, isDefaultPrevented, isPropagationStopped, persist TS2345
16 | }) => {
17 | const handleOnClick = (e: MouseEvent<HTMLButtonElement>) => {
> 18 | onClick(e);
| ^
19 | };
20 | return (
21 | <ButtonStyles onClick={handleOnClick} {...props}>
UPDATED:
if you want to include them aria props
new
import React from "react";
export interface ButtonProps extends React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, React.AriaAttributes {}
export const ButtonPrimary:React.FC<ButtonProps> = props => {
const {children, ...rest} = props;
return (
<button {...rest}>{children}</button>
)
}
old
import * as React from "react";
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {}
/**
* Button.
*
* #param {ButtonProps} props button's props
* #returns {React.ReactElement<ButtonProps>} Button.
*/
export function Button(props: ButtonProps): React.ReactElement<ButtonProps> {
const { children, ...rest } = props;
return (
<button
{...rest}
>
{children}
</button>
);
}
Your interface IButtonProps defines the expected signature of onClick
onClick?:
| ((event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void)
| undefined;
However, the e argument to handleClick is defined differently as MouseEvent<HTMLButtonElement>
If you change the onClick signature to match your handleClick definition, that may go some way to fix it?
onClick?:
| ((event: React.MouseEvent<HTMLButtonElement>) => void)
| undefined;
// Button.tsx
import React from "react";
import styled from "styled-components";
interface IButtonProps {
children?: React.ReactNode;
props?: any;
onClick?: any;
}
const MyButton: React.FC<IButtonProps> = ({ onClick, children, ...props }) => {
return (
<ButtonStyles {...props} onClick={onClick}>
{children}
</ButtonStyles>
);
};
export default MyButton;
const ButtonStyles = styled.button``;
// Login.tsx
import React from "react";
import MyButton from "../../components/Generic/Button/Button";
const Login: React.FC = () => {
const ClickMe = () => {
console.log("Button Clicked");
};
return (
<div>
<MyButton onClick={ClickMe}>Hello MY Button</MyButton>
</div>
);
};
export default Login;
I just able to fix the issue by changing the type of onClick?: any This is not the best solution, but at least it didn't throw any error and I can perform the onClick action in my Login.tsx.

Resources