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

Option for Toast Swipe Left? #28

Closed
58bits opened this issue Jan 2, 2023 · 5 comments
Closed

Option for Toast Swipe Left? #28

58bits opened this issue Jan 2, 2023 · 5 comments

Comments

@58bits
Copy link

58bits commented Jan 2, 2023

Hi and thanks a lot for the excellent project. We've created several configurable wrapper functional components around your examples, and it's all working great.

One thing we recently tried to do was create options / props for positioning Toasts in either 'top-right', 'bottom-right', 'top-left' and 'bottom-left'.

We've got this working by modifying the keyframes and animation options in thailwind.config.js - as follows:

/** @type {import('tailwindcss').Config} */

const defaultTheme = require("tailwindcss/defaultTheme")

module.exports = {
  darkMode: 'class',
  content: [
    "./app/**/*.{js,ts,jsx,tsx}",
  ],
  plugins: [
    require('@tailwindcss/typography'),
    require('@tailwindcss/forms'),
    require("tailwindcss-radix")(),
  ],
  variants: {
    extend: {},
  },
  theme: {
    ...defaultTheme,
    fontFamily: {
      ...defaultTheme.fontFamily,
      display: ["Roboto", ...defaultTheme.fontFamily.sans],
      sans: ["Inter", ...defaultTheme.fontFamily.sans],
      mono: ["Source Code Pro", ...defaultTheme.fontFamily.mono],
    },
    gridTemplateColumns: {
      'auto-fit-280': 'repeat(auto-fill, minmax(280px, 1fr))',
      'auto-fit-320': 'repeat(auto-fill, minmax(320px, 1fr))',
      'auto-fit-480': 'repeat(auto-fill, minmax(480px, 1fr))',
    },
    extend: {
      boxShadow: {
        slider: "0 0 0 5px rgba(0, 0, 0, 0.3)",
      },
      keyframes: {
        // Dropdown menu
        "scale-in": {
          "0%": { opacity: 0, transform: "scale(0)" },
          "100%": { opacity: 1, transform: "scale(1)" },
        },
        "slide-down": {
          "0%": { opacity: 0, transform: "translateY(-10px)" },
          "100%": { opacity: 1, transform: "translateY(0)" },
        },
        "slide-up": {
          "0%": { opacity: 0, transform: "translateY(10px)" },
          "100%": { opacity: 1, transform: "translateY(0)" },
        },
        // Tooltip
        "slide-up-fade": {
          "0%": { opacity: 0, transform: "translateY(2px)" },
          "100%": { opacity: 1, transform: "translateY(0)" },
        },
        "slide-right-fade": {
          "0%": { opacity: 0, transform: "translateX(-2px)" },
          "100%": { opacity: 1, transform: "translateX(0)" },
        },
        "slide-down-fade": {
          "0%": { opacity: 0, transform: "translateY(-2px)" },
          "100%": { opacity: 1, transform: "translateY(0)" },
        },
        "slide-left-fade": {
          "0%": { opacity: 0, transform: "translateX(2px)" },
          "100%": { opacity: 1, transform: "translateX(0)" },
        },
        // Navigation menu
        "enter-from-right": {
          "0%": { transform: "translateX(200px)", opacity: 0 },
          "100%": { transform: "translateX(0)", opacity: 1 },
        },
        "enter-from-left": {
          "0%": { transform: "translateX(-200px)", opacity: 0 },
          "100%": { transform: "translateX(0)", opacity: 1 },
        },
        "exit-to-right": {
          "0%": { transform: "translateX(0)", opacity: 1 },
          "100%": { transform: "translateX(200px)", opacity: 0 },
        },
        "exit-to-left": {
          "0%": { transform: "translateX(0)", opacity: 1 },
          "100%": { transform: "translateX(-200px)", opacity: 0 },
        },
        "scale-in-content": {
          "0%": { transform: "rotateX(-30deg) scale(0.9)", opacity: 0 },
          "100%": { transform: "rotateX(0deg) scale(1)", opacity: 1 },
        },
        "scale-out-content": {
          "0%": { transform: "rotateX(0deg) scale(1)", opacity: 1 },
          "100%": { transform: "rotateX(-10deg) scale(0.95)", opacity: 0 },
        },
        "fade-in": {
          "0%": { opacity: 0 },
          "100%": { opacity: 1 },
        },
        "fade-out": {
          "0%": { opacity: 1 },
          "100%": { opacity: 0 },
        },
        // Toast
        "toast-hide": {
          "0%": { opacity: 1 },
          "100%": { opacity: 0 },
        },
        "toast-slide-in-right": {
          "0%": { transform: `translateX(calc(100% + 1rem))` },
          "100%": { transform: "translateX(0)" },
        },
        "toast-slide-in-left": {
          "0%": { transform: `translateX(calc(-1 * (100% + 1rem)))` },
          "100%": { transform: "translateX(0)" },
        },
        "toast-slide-in-bottom": {
          "0%": { transform: `translateY(calc(100% + 1rem))` },
          "100%": { transform: "translateY(0)" },
        },
        // Leave toast-swipe-out for compatibility - swipe out right
        "toast-swipe-out": {
          "0%": { transform: "translateX(var(--radix-toast-swipe-end-x))" },
          "100%": {
            transform: `translateX(calc(100% + 1rem))`,
          },
        },
        "toast-swipe-out-right": {
          "0%": { transform: "translateX(var(--radix-toast-swipe-end-x))" },
          "100%": {
            transform: `translateX(calc(100% + 1rem))`,
          },
        },
        "toast-swipe-out-left": {
          "0%": { transform: "translateX(var(--radix-toast-swipe-end-x))" },
          "100%": {
            transform: `translateX(calc(-1 * (100% + 1rem)))`,
          },
        },
      },
      animation: {
        // Dropdown menu
        "scale-in": "scale-in 0.2s ease-in-out",
        "slide-down": "slide-down 0.6s cubic-bezier(0.16, 1, 0.3, 1)",
        "slide-up": "slide-up 0.6s cubic-bezier(0.16, 1, 0.3, 1)",
        // Tooltip
        "slide-up-fade": "slide-up-fade 0.4s cubic-bezier(0.16, 1, 0.3, 1)",
        "slide-right-fade":
          "slide-right-fade 0.4s cubic-bezier(0.16, 1, 0.3, 1)",
        "slide-down-fade": "slide-down-fade 0.4s cubic-bezier(0.16, 1, 0.3, 1)",
        "slide-left-fade": "slide-left-fade 0.4s cubic-bezier(0.16, 1, 0.3, 1)",
        // Navigation menu
        "enter-from-right": "enter-from-right 0.25s ease",
        "enter-from-left": "enter-from-left 0.25s ease",
        "exit-to-right": "exit-to-right 0.25s ease",
        "exit-to-left": "exit-to-left 0.25s ease",
        "scale-in-content": "scale-in-content 0.2s ease",
        "scale-out-content": "scale-out-content 0.2s ease",
        "fade-in": "fade-in 0.2s ease",
        "fade-out": "fade-out 0.2s ease",
        // Toast
        "toast-hide": "toast-hide 100ms ease-in forwards",
        "toast-slide-in-right":
          "toast-slide-in-right 150ms cubic-bezier(0.16, 1, 0.3, 1)",
        "toast-slide-in-left":
          "toast-slide-in-left 150ms cubic-bezier(0.16, 1, 0.3, 1)",
        "toast-slide-in-bottom":
          "toast-slide-in-bottom 150ms cubic-bezier(0.16, 1, 0.3, 1)",
        // Leave toast-swipe-out for compatibility - swipe out right.
        "toast-swipe-out": "toast-swipe-out 100ms ease-out forwards",
        "toast-swipe-out-right": "toast-swipe-out-right 100ms ease-out forwards",
        "toast-swipe-out-left": "toast-swipe-out-left 100ms ease-out forwards",
      },
    },
  },
}

This is all working fine, and we can now position and animate the entrance and close of the Toast component to any corner.

There's just one problem. The direction of the pointer 'drag' and data attribute for 'move' event data-swipe='move' has its drag direction configured at the provider level - as in the docs here...

https://www.radix-ui.com/docs/primitives/components/toast#provider - with the swipeDirection prop and enum (for "right" | "left" | "up" | "down").

For example ToastPrimitive.Provider swipeDirection='right'

Which means we're not sure whether it's possible to configure this at the ToastPrimitive.Root level (which we wrap in our configurable component) to change the direction of the manual 'drag' and 'move' of the Toast.

Are we going about this the wrong way? Or is there no easy solution?

For interest, our complete Toast component implementation is here...

https://github.com/infonomic/remix.infonomic.io/blob/develop/app/ui/components/notifications/toast.tsx

Suggestions or thoughts greatly appreciated.

And again - thanks for the great project - and Happy New Year!

@ecklf
Copy link
Owner

ecklf commented Jan 2, 2023

Hey there 👋🏻 ,

In terms of making it work based on the defined swipe direction we could try adding data-swipe-direction (https://www.radix-ui.com/docs/primitives/components/toast#root) to the plugin, so you wouldn't need to re-adjust the component based on whatever you set in the provider. Regarding the provider itself I'd ask in the radix repo. I don't see a way you can configure it within root, but on the other hand it makes sense to have consistent behavior for ux purposes?

@58bits
Copy link
Author

58bits commented Jan 2, 2023

Ah nicely spotted - they set a data attribute for data-swipe-direction .

And so as you suggest - I guess the plugin could be updated to detect this?

I struggle to see how a swipe up or swipe down Toast would work on larger screens - but why not I suppose?

Correct me if I'm wrong - but I think swipe direction (the 'drag' direction to clear the Toast) and position are separate. It would still be nice to have an 'in-app' option for position of top or bottom. There may be a valid a use case for a Toast being used in a different way - depending on the functional area of the application and the UI over which it appears. Mobile would also likely be set to 'bottom' as it is now, but swipe could be left, right, or down.

In terms of UX consistency - I agree too that a change in something like swipe direction would be unlikely within a single application.

So not sure what this means then - as the changes we've made sort of do what we need - as long as we set the provider to an appropriate swipe direction.

And now I'm not so sure if following data-swipe-direction makes sense - unless we could say that the animation direction can always be computed from the data-swipe-direction - i.e. the entrance animation would also be opposite of the data-swipe-direction?

data-swipe-direction="right" => animate entrance left
data-swipe-direction="left" => animate entrance right
data-swipe-direction="up" => animate entrance down
data-swipe-direction="down" => animate entrance up

The problem with this is right away is that mobile wouldn't work for animate entrance up, with swipe left or right to clear (which is arguably an expected behavior)

I think I've just managed to confuse myself. Heh.

@ecklf
Copy link
Owner

ecklf commented Jan 2, 2023

Published a new release with an example for swipe direction here: https://github.com/ecklf/tailwindcss-radix/blob/main/demo/components/toast.tsx#L37-L40

Let me know if that helps with what you want to achieve 👍🏻

@58bits
Copy link
Author

58bits commented Jan 2, 2023

Okay thanks! Will take a look!

Have also taken a proper look at how you derive selectors from named data attributes here...

Object.keys(namedDataAttributes).forEach((attributeName) => {

Very nice.

@58bits
Copy link
Author

58bits commented Jan 2, 2023

Hi again - okay I've taken a look and done some tests. I think having the extra selector for data-swipe-direction is great - and adds a level of configurability.

Also having had the time now (without having a six-year-old pulling at my sleeve), I've understood the Radix settings a bit better now as well. The primitive setting here, <ToastPrimitive.Provider swipeDirection='right'> - might also be called swipeDetection - since what it's doing, is determining which mouse drag direction, or mobile gesture direction is used to trigger the end animation. And certainly as it is now - it would be up to the developer to ensure that this setting, and the corresponding animation agree (i.e. don't swipe right, and trigger a left exit animation).

For the new demo - again - great that the new selector for swipe direction is being shown... but... if I may say, I think on mobile - swiping right or left is the more 'expected' UX to clear the notification. Of course that's just a preference, and easy to change.

In terms of the tailwind.config.js settings for transition and animation settings- and again, as a preference I suppose (and granted that the 'left' and 'up' options would be far less likely) - we've configured ours as follows...

 keyframes: {
    // Toast
    "toast-hide": {
      "0%": { opacity: 1 },
      "100%": { opacity: 0 },
    },
    "toast-slide-in-right": {
      "0%": { transform: `translateX(calc(100% + 1rem))` },
      "100%": { transform: "translateX(0)" },
    },
    "toast-slide-in-left": {
      "0%": { transform: `translateX(calc(-1 * (100% + 1rem)))` },
      "100%": { transform: "translateX(0)" },
    },
    "toast-slide-in-bottom": {
      "0%": { transform: `translateY(calc(100% + 1rem))` },
      "100%": { transform: "translateY(0)" },
    },
    "toast-slide-in-top": {
       "0%": { transform: `translateY(-1 * (calc(100% + 1rem)))` },
       "100%": { transform: "translateY(0)" },
     },
    // Leave toast-swipe-out for compatibility - swipe out right
    "toast-swipe-out": {
      "0%": { transform: "translateX(var(--radix-toast-swipe-end-x))" },
      "100%": {
        transform: `translateX(calc(100% + 1rem))`,
      },
    },
    "toast-swipe-out-right": {
      "0%": { transform: "translateX(var(--radix-toast-swipe-end-x))" },
      "100%": {
        transform: `translateX(calc(100% + 1rem))`,
      },
    },
    "toast-swipe-out-left": {
      "0%": { transform: "translateX(var(--radix-toast-swipe-end-x))" },
      "100%": {
        transform: `translateX(calc(-1 * (100% + 1rem)))`,
      },
    },
    "toast-swipe-out-down": {
      "0%": { transform: "translateY(var(--radix-toast-swipe-end-y))" },
      "100%": {
        transform: `translateY(calc(100% + 1rem))`,
      },
    },
    "toast-swipe-out-up": {
      "0%": { transform: "translateY(var(--radix-toast-swipe-end-y))" },
      "100%": {
        transform: `translateY(calc(-1 * (100% + 1rem)))`,
      },
    },
  },
animation: {
    // Toast
    "toast-hide": "toast-hide 100ms ease-in forwards",
    "toast-slide-in-right":
      "toast-slide-in-right 150ms cubic-bezier(0.16, 1, 0.3, 1)",
    "toast-slide-in-left":
      "toast-slide-in-left 150ms cubic-bezier(0.16, 1, 0.3, 1)",
    "toast-slide-in-bottom":
      "toast-slide-in-bottom 150ms cubic-bezier(0.16, 1, 0.3, 1)",
     "toast-slide-in-top":
          "toast-slide-in-top 150ms cubic-bezier(0.16, 1, 0.3, 1)",
    // Leave toast-swipe-out for compatibility - swipe out right.
    "toast-swipe-out": "toast-swipe-out 100ms ease-out forwards",
    "toast-swipe-out-right": "toast-swipe-out-right 100ms ease-out forwards",
    "toast-swipe-out-left": "toast-swipe-out-left 100ms ease-out forwards",
    "toast-swipe-out-down": "toast-swipe-out-down 100ms ease-out forwards",
    "toast-swipe-out-up": "toast-swipe-out-up 100ms ease-out forwards",
  },

Hope some of this helps. If you think this is an okay approach I'd be glad to submit a pull request. Whatever you feel is best. Thanks again for the amazing project, and hope that 2023 is off to a good start for you and yours ;-)

@58bits 58bits closed this as completed Jan 2, 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

No branches or pull requests

2 participants