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

feat(react): add toast component #2279

Closed
wants to merge 15 commits into from

Conversation

takurinton
Copy link

@takurinton takurinton commented Apr 7, 2023

#2192

@atomiks

supports

  • Pauses closing on hover, focus and window blur
  • Supports hotkey to jump to toast viewport
  • Pressing esc when focused on toast closes it
  • Fully customizable toast components, positions, delay times, and animations
  • Global values can be specified from the Provider and reflected in individual toasts.
  • Globally defined values can be updated by individual toasts
    • If global and toast values do not exist, default values will be applied
  • Fully follow the ARIA design pattern

not supported

  • Supports closing via swipe gesture
  • Toast with close button
  • Default toast (nothing is rendered when render function is empty)

Since this library is called "Floating UI", we can and should do more than just support elements that use anchor positioning — for example, a non-anchored floating Dialog or Drawer component are already possible to build using the existing interaction primitives, but a component like a Toast or anything draggable isn't despite them also being "floating" elements.

I agree with you. I was wanting a toast component that uses floating-ui.
I created the prototype as an experiment.

usage

Brief description of how it is used (how it is intended to be used)

(edited)

export const Main = () => (
  <ToastProvider placement="top-center">
    <Component />
  </ToastProvider>
);

const Component = () => {
  const {toast} = useToast();

  return (
    <div>
      <button
        onClick={() =>
          toast({
            placement: 'top',
            render: () => <div>Hello</div>,
          })
        }
      >
        Add Toast
      </button>
  </div>
  );
};

worries

  • I currently have this implementation in the visual test directory, is that correct?
  • Should I create hooks and incorporate swipe gestures into floating-ui?
  • I need assistance as my knowledge of accessibility is a little lacking.
  • The API to manipulate the toast is not yet fully prepared here

@netlify
Copy link

netlify bot commented Apr 7, 2023

Deploy Preview for vibrant-gates-22c214 canceled.

Name Link
🔨 Latest commit cfb60e0
🔍 Latest deploy log https://app.netlify.com/sites/vibrant-gates-22c214/deploys/6439239cb33b7600088e55fe

@rollingversions
Copy link

There is no change log for this pull request yet.

Create a changelog

@atomiks
Copy link
Collaborator

atomiks commented Apr 7, 2023

What do you think of Chakra UI's custom component API, Radix's, or Sonner?

@mihkeleidast, @FezVrasta any thoughts?

One thing we should do is make sure we only provide functional styles and no other (decorative) styling. i.e. just the position/stacking, to be in line with the rest of how the library works.

const toast = useToast();
return (
  <button 
    onClick={() => {
      toast({
        placement: 'top',
        render: () => <div>Hello</div>
      });
    }} 
  />
);

I currently have this implementation in the visual test directory, is that correct?

Yep!

Should I create hooks and incorporate swipe gestures into floating-ui?

Don't worry about this for now

I need assistance as my knowledge of accessibility is a little lacking.

I'll try to review these parts in a sec

@takurinton
Copy link
Author

Thanks @atomiks

What do you think of Chakra UI's custom component API, Radix's, or Sonner?

I have used react-toast-notification's as a reference here.
However, development of this library has stopped, and we certainly feel it makes sense to refer to a library that is still active today.

One thing we should do is make sure we only provide functional styles and no other (decorative) styling

I think this is correct, and a chakra-ui interface might be nice.

@atomiks
Copy link
Collaborator

atomiks commented Apr 7, 2023

I just realized an absolute flexbox column won't work with transitions to adjust toast positions (no layout animation, without framer-motion at least)... so translateY(...) to adjust the position of toasts in a stack as toasts are removed from the stack is required. I imagine the logic would be:

  • Enter: check the height of all toasts in the stack to add it to the end
  • Leave: calculate the leaving toast's height, then propagate the change to all other entered toasts as necessary

@FezVrasta
Copy link
Member

Depends, if you make the toast disappear by first making it invisible and then reducing its height until it reaches 0 you can use flexbox

@takurinton
Copy link
Author

@FezVrasta

Thanks comment.
One question I have is that the current toast has the following structure, with useFloating in the <ul> section.

<ul role="region" aria-live="polite">
  <li role="status" aria-atomic="true">
     Toast content
  </li>
</ul>

So I can't manipulate open, is this structure wrong? Should I manage <li> with useFloating?
Also, is it possible to operate open or close on a listRef like the current one that holds <li> refs?

Comment on lines +152 to +170
if (placements[0] === 'top' || placements[0] === 'bottom') {
return {
margin: '0 auto',
top: placements[0] === 'top' ? '20px' : undefined,
bottom: placements[0] === 'bottom' ? '20px' : undefined,
left:
placements[1] === undefined
? 0
: placements[1] === 'start'
? '20px'
: 'auto',
right:
placements[1] === undefined
? 0
: placements[1] === 'end'
? '20px'
: 'auto',
};
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@atomiks
I implemented to get the position of toast.
This doesn't look very smart, is it best?
I would be happy to use the floating-ui feature to specify the position relative to the viewport, but I can't think of anything. Any suggestions?

@takurinton
Copy link
Author

takurinton commented Apr 10, 2023

Currently, this is how it behaves locally for me.
The toast add and remove appears to be implemented correctly.

I feel that the style is a bit forced and a11y is not guaranteed in some areas, but I feel that it is generally good.

Screen.Recording.2023-04-10.at.18.59.44.mov

@atomiks
There are several points that I would like to confirm.

  • Should I be able to specify the position from a global context as in Configuring toast globally?
  • Is it sufficient to specify the same placement as that of the placement in floting-ui using the toast function?
  • Do I need to attach useFloating to each individual toast? (I think this is necessary if you use useDismiss to close the toast)

@atomiks
Copy link
Collaborator

atomiks commented Apr 10, 2023

Should we be able to specify the position from a global context as in Configuring toast globally?

I imagine this would be useful yes

Is it sufficient to specify the same placement as that of the placement in floting-ui using the toast function?

The placement strings should be all bottom and all top sides (e.g. top, top-end, bottom-start, etc). right and left sides don't apply since the toasts are always vertically stacked.

Do we need to attach useFloating to each individual toast? (I think this is necessary if you use useDismiss to close the toast)

For transition styles to work properly yes, however I'm not sure if pressing esc should dismiss all of them simultaneously or the most recent (or old) one only.


Some notes on the API design:

  • There shouldn't be any Tailwind styling outside the render function (like for the li and ul nodes), only inline styles, since they're functional. The consumer should specify all decorative styles themselves externally
  • Transitions should be external as well with full customizability - the max-height should start transitioning out as the same time as opacity but it seems there's a delay atm?
  • The user should be able to customize delay time
  • We somehow need to incorporate FloatingFocusManager for cases where the toast is interactive (e.g. has an Undo button inside it for instance)

@takurinton
Copy link
Author

Thank you @atomiks !!!
I'll make some revisions regarding that.

I have some more questions:

  • Should we also apply useFloating to the entire element (the ul node)?
    • I feel like it's unnecessary as long as we apply it to the li nodes, but what do you think?
    • I believe this depends on which toast you want to close as you say
  • Should toast really be navigable with tabs and closed with esc? For example, react-toastify only allows focusing on the close button, while chakra-ui doesn't seem to allow focusing at all. What do you think??

@takurinton
Copy link
Author

current location

You can use placement to specify the position. You can specify top,top-start,top-end,bottom,bottom-start,bottom-end.
Example

<button 
  onClick={() => {
    toast({
      placement: 'top',
      render: () => <div>Hello</div>
    });
  }} 
/>

Added deley to the toast function argument to allow individual adjustment of the time until the toast closes.
Example

<button 
  onClick={() => {
    toast({
      deley: 1000
      render: () => <div>Hello</div>
    });
  }} 
/>

Next, I made the transition fully customizable. This allows you to pass the same properties that you can pass to UseTransitionStylesProps. This is due to the useTransitionStyle when specifying open and close animations in the toast component.
If you write a partial, only that will be applied, and the default values will be applied for the other parts.
Example

<button 
  onClick={() => {
    toast({
      transition: {
        duration: { open: 500, close: 1000 }
      },
      render: () => <div>Hello</div>
    });
  }} 
/>

The render function can specify an onClose callback, which can be used to close the toast at the implementer's timing.
Example

<button 
  onClick={() => {
    toast({
      render: (onClose) => (
        <div style={{ display: 'flex' }}>
          <div>hello</div>
          <button onClick={onClose}>close</button>
        </div>
      )
    });
  }} 
/>

It would be useful to be able to specify these functions globally via Provider. I will work on that.
Also, clearAll, close, id, . . etc., it would be useful if APIs such as clearAll, close, id, ... etc. are provided, so I will consider their implementation as well.

@takurinton
Copy link
Author

takurinton commented Apr 12, 2023

properties can now be passed from the provider.
If it is defined in a toast function, it will take precedence, otherwise the global value will be referenced.
If undefined for both provider and toast functions, the default value is applied.
Example

export const Main = () => (
  // This provider will specify the `top-center` placement for all toasts.
  <ToastProvider placement="top-center">
    <Component />
  </ToastProvider>
);

const Component = () => {
  const {toast} = useToast();

  return (
    <div>
      <button
        onClick={() =>
          toast({
            // This will be placed at `top` because the `placement` specified by the toast function has priority.
            placement: 'top',
            render: () => <div>Hello</div>,
          })
        }
      >
        Add Toast to top
      </button>
      <button
        onClick={() =>
          // This will specify the `top-center` specified by the provider, 
          // since `placement` is not specified in the toast function.
          toast({
            render: () => <div>Hello</div>,
          })
        }
      >
        Add Toast to top-center
      </button>
    </div>
  );
};

@takurinton
Copy link
Author

takurinton commented Apr 13, 2023

Enabled dismiss, which only works when focusing on toast.

I set up hooks for dismiss, but still did not define a prop like isCloseable or the associated close button.
This is because we have defined a callback called onClose in the render function, which we believe allows for flexibility on the part of the user.

});
const ref = useMergeRefs([propRef, refs.setFloating]);
const dismiss = useDismiss(context, {
enabled: focus,
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enable this only during focus.
Failure to do so will cause an unintended toast to close when you press esc.

Comment on lines +351 to +352
order={['floating']}
initialFocus={-1}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When new toast is displayed, the focus is on it by default. Then, when you press esc, the newest toast will close automatically, and then the focus is removed so that no toast will close when you press esc.
Therefore, the toast closes only when the focus is intentionally set to floating element.

@takurinton
Copy link
Author

The closeAll function has been created. This allows you to close all toasts.
Example

const Component = () => {
  const {closeAll} = useToast();

  return <Button onClick={closeAll}>Close All</Button>;
};

Next, the render callback in the toast function can now accept an id.
This is useful when you want to do something with an id. The following can do the same thing.
Example

const Component = () => {
  const {close} = useToast();

  return (
    <Button
      onClick={() =>
        toast({
          render: (id, onClose) => (
            <div className="w-[300px] bg-white p-4 mb-4 rounded-lg shadow-lg">
              Hello 
              {/* same behavior */}
              <button onClick={onClose}>close using onClose</button>
              <button onClick={() => close(id)}>close using close function</button>
            </div>
          ),
        })
      }
    >
    Add Toast
  </Button>
};

@takurinton
Copy link
Author

Added autoClose prop.
If this is set to false, the toast will not close automatically. default true.
Example

<button 
  onClick={() => {
    toast({
      autoClose: false,
      render: () => <div>Hello</div>
    });
  }} 
/>

@takurinton takurinton requested a review from atomiks April 14, 2023 10:11
@atomiks
Copy link
Collaborator

atomiks commented Apr 19, 2023

@takurinton sorry for the delay looking into this and thank you so much for working on it. I will take a proper look and review soon

@atomiks
Copy link
Collaborator

atomiks commented Apr 23, 2023

I've had the chance to dive deeper into this, and I really appreciate the effort you've put into it. However, there are quite a few aspects that need to be adjusted to align better with the library's goals (and I'm personally not even sure how this will work — it needs some more exploration), but I believe your contribution has potential.

I won't be able to merge it at the moment, but feel free to continue developing this as an external package for others to use in the meantime. Integrating it seamlessly with the other hooks and components in the library is challenging in terms of how it will be designed, and could take some time to figure out the best API, but I will work on it eventually.

Thank you :))

@takurinton
Copy link
Author

@atomiks
Thanks for looking this!! And sorry for the many things I have not done.
I look forward to a future implementation of floating-ui with absolute positioning relative to the viewport!
Thanks again!

@atomiks
Copy link
Collaborator

atomiks commented Oct 14, 2023

Currently closing old unmerged PRs :)

@atomiks atomiks closed this Oct 14, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants