nandorojo / dripsy

🍷 Responsive, unstyled UI primitives for React Native + Web.
https://dripsy.xyz
MIT License
2.03k stars 80 forks source link

Pseudo selectors #7

Closed cmaycumber closed 3 years ago

cmaycumber commented 4 years ago

Is there a current plan to support pseudo selectors like hover, focus, etc. I know they aren't currently supported by react-native-web and are usually handled with react hooks similar to how the breakpoints are currently being handled. I think this could be a handy feature but I'm not 100% sure the best way this is currently being handled but I did read an article by Evan Bacon about handling them elegantly.

I think that a similar approach could be used to hijack the sx object being passed into the components that would key off of the basic pseudo-selectors (:hover, :focus, :active) and use the proper logic to allow for the desired effect.

Also is there any idea for what components should be implemented? I think it might be helpful to create a top-level outline of the different components that might be implemented and what it's in the scope of the design system from a library perspective.

nandorojo commented 4 years ago

Interesting proposal, I was actually playing around with this the other day.

Re pseudo selectors: It might make sense to follow the plans for the new Pressable component introduced by react native 0.63. The sx prop could optionally be a pure function that receives pressed, hovered, etc. as an argument. We could use selector keys, but I kind of like the idea of a JS approach so that it feels like it's supporting every platform universally.

nandorojo commented 4 years ago

Good idea re: the outline, would you mind moving that to a separate issue?

I think a good start could be following what Theme UI does: export JSX versions of primitive components, and then add styled H1, H2, etc..., as well as a base button, and such. I like the way theme UI isn't too opinionated, but it also has all the base components you need to build great UIs out of the box.

cmaycumber commented 4 years ago

I agree the new pressable component would definitely be the way to follow. I'm all for the JS approach I think that's a great idea. The animated value is also an interesting proposition; I think that it would be especially helpful with the addition of the pressable component as well if you wanted to animate the button on press states along with the hover.

Yes! I'll move this into a new issue and just throw some components up there in a checklist and we can add/remove items from there.

nandorojo commented 4 years ago

Just came across https://github.com/Sharcoux/rn-css, which has an interesting approach to pseudo selectors like hover, as well as web units like em and such.

Figured I'd put any other relevant repos / resources here as I figure out the optimal next steps.

nandorojo commented 4 years ago

One concern for this solution is to make sure we don't trigger unnecessary re-renders from touches, hovers, etc. https://github.com/necolas/react-native-web/issues/205#issuecomment-297038503

nandorojo commented 4 years ago

While we're at it, the sx prop function could also pass the window's height and width, since we're already computing this to build breakpoints. That way, you can use relative styles without needing to set up any hooks. We already update the state based on these, so we get it for free.

nandorojo commented 4 years ago

Another thing I like about designing in web is responsive font size. It might be out of the scope of this prop, but a responsiveFontSize function that lets you set min, max, and % of viewport size font sizes would be useful. It probably makes more sense to export this as a separate function from the library, but it could also be passed from the sx prop to keep all styles confined in just this prop.

cmaycumber commented 4 years ago

That's a helpful Github issue. Re-renders would be bad especially if it was listening for hover events on mobile devices as well.

I'm a big fan of responsive font sizes as well. I think that if we can create an elegant solution for that it would be a big plus.

I'm going to start experimenting with some of these ideas and potentially get a POC or at least a better understanding of some of the obstacles for the psuedo selectors.

nandorojo commented 4 years ago

Could setNativeProps be the solution here? It seems like we could have hover listeners that directly set the Native props instead of triggering a rerender from a state update.

From react docs: https://reactnative.dev/docs/direct-manipulation

nandorojo commented 4 years ago

Looks like the creator of RNW is chiming in on this here...https://github.com/necolas/react-native-web/issues/1708

nandorojo commented 4 years ago

I'll be sticking to using react-native-web-hooks for now, and will migrate to Pressable after the next expo update includes RNW 14's hover style for Pressable.

nandorojo commented 3 years ago

I'm not sure what the move is for now on this. Maybe we can support a hover pseudo selector on all items, and use the useHover hook. However, this means adding a decent amount of weight to every component.

On the other hand, if we wrap every element with Pressable, we are introducing yet another wrapper around every element. I would like Dripsy to handle pseudo selectors better than it does now, but I'm not exactly sure how it would work. For now, I'm probably going to stick to using Pressable and wrapping dripsy components with that.

Maybe the best we could do is re-export pressable, and theme-ify the style prop function?

cmaycumber commented 3 years ago

What do you think about adding a boolean to the createThemedComponent/styled function that wraps it in a Pressable?

nandorojo commented 3 years ago

That's an interesting idea, a simple hoverable Boolean could work. Not sure if this is a good idea, but maybe it could be a prop of the components too.

I like that it would make it explicitly clear that we have hover styles.

cmaycumber commented 3 years ago

That's an interesting idea, a simple hoverable Boolean could work. Not sure if this is a good idea, but maybe it could be a prop of the components too.

I like that it would make it explicitly clear that we have hover styles.

I think that the prop passed could be a good idea. The component could inherit most of the props from pressable so it would be super convenient to access the hovered state. For example, in a button component, the label styles could be updated from the prop that's passed.

I could definitely see this replacing some of my Pressable components atm.

nandorojo commented 3 years ago

True, I think that could work. I'm already using RNW 0.14, but since it has some breaking changes and many probably aren't using it yet, maybe we wait for expo to support it by default to keep dripsy backwards compatible with expo. I'll play around with it.

nandorojo commented 3 years ago

I think I know how we could add hover support. I'll make it happen after merging the monorepo.

The API could be a whileHover prop like framer-ui has.

cmaycumber commented 3 years ago

I think I know how we could add hover support. I'll make it happen after merging the monorepo.

The API could be a whileHover prop like framer-ui has.

That would be super sweet. I'm currently trying to figure out the best way to support disabled, pressed, and hovered through the theme entirely right now and can't find a perfect solution.

It would be cool if we could handle some of these under the hood in dripsy for sure.

nandorojo commented 3 years ago

Some ideas come to mind for how we could do that. I'll look at the monorepo today, hopefully we can get that published to production.

macieklad commented 3 years ago

In our recent project, we've came up with the following solution and it works fine for now, it is heavily inspired by the material ui library - in each component you call makeStyles or makeInteractiveStyles which provides type support and returns function that can be used to create styles based on params (it also memoizes the result).

With the "makeInteractiveStyles" variant, it also calls a hook underneath that generates all handlers required for the pressable component, and connects those states to the props inside the function, so you have dynamic, changable stylesheets, that take all of the themed values. Even better, it is fully typed so you get props and names inside the returned object, eg. classes.root shows typehints and so on. I think that connects all those problems in a great way without coupling things to dripsy itself.


export const Tag: React.FC<TagProps> = ({ onPress, children, ...props }) => {
  const { classes, handlers } = useStyles({ onPress })

  return (
    <Pressable sx={classes.root} {...(onPress && handlers)} {...props} accessibilityRole="button">
      <Text sx={classes.text}>{children}</Text>
    </Pressable>
  )
}

const useStyles = makeInteractiveStyles(
  ({ isHovered, onPress }: PropsWithInteractions<TagProps>) => ({
    root: {
      backgroundColor: pickValue('primary-400', isHovered && 'primary-600'),
      cursor: pickValue('default', onPress && 'pointer'),
      borderRadius: 100,
      px: [3, null, 4],
      py: 2,
    },
    text: {
      color: 'white',
      fontWeight: 'semibold',
      fontSize: [1, null, 2],
      lineHeight: [0, null, 2],
    },
  })
)`
cmaycumber commented 3 years ago

In our recent project, we've came up with the following solution and it works fine for now, it is heavily inspired by the material ui library - in each component you call makeStyles or makeInteractiveStyles which provides type support and returns function that can be used to create styles based on params (it also memoizes the result).

With the "makeInteractiveStyles" variant, it also calls a hook underneath that generates all handlers required for the pressable component, and connects those states to the props inside the function, so you have dynamic, changable stylesheets, that take all of the themed values. Even better, it is fully typed so you get props and names inside the returned object, eg. classes.root shows typehints and so on. I think that connects all those problems in a great way without coupling things to dripsy itself.

export const Tag: React.FC<TagProps> = ({ onPress, children, ...props }) => {
  const { classes, handlers } = useStyles({ onPress })

  return (
    <Pressable sx={classes.root} {...(onPress && handlers)} {...props} accessibilityRole="button">
      <Text sx={classes.text}>{children}</Text>
    </Pressable>
  )
}

const useStyles = makeInteractiveStyles(
  ({ isHovered, onPress }: PropsWithInteractions<TagProps>) => ({
    root: {
      backgroundColor: pickValue('primary-400', isHovered && 'primary-600'),
      cursor: pickValue('default', onPress && 'pointer'),
      borderRadius: 100,
      px: [3, null, 4],
      py: 2,
    },
    text: {
      color: 'white',
      fontWeight: 'semibold',
      fontSize: [1, null, 2],
      lineHeight: [0, null, 2],
    },
  })
)`

This is awesome. Thanks for sharing. I might use some of this logic in one of my projects.

I'm still curious if there might be a way to work in variants inside of the theme and be able to pass them to the hook to produce results. The part I'm currently struggling with is being able to theme pseudo styles including animations with dripsy and now moti or reanimated.

nandorojo commented 3 years ago

Interesting approach! As I mentioned at https://github.com/nandorojo/dripsy/pull/69#issuecomment-808949540, I've opted to just use Pressable + <MotiView>, since I don't think pseudoevents should live in Dripsy's primitives. So this kind of solution is interesting, since it's an abstraction where users implement their own pseudoelements.

miikebar commented 3 years ago

I may have found something interesting regarding not only pseudo selectors but also media queries. Recently I've been looking for alternatives to Fresnel as the responsive style solution, as I'm not really keen on the idea of generating all breakpoints on the server side and sending them to client. I've decided to try Emotion and react-responsive, with the idea of estimating the client screen dimensions on the server side based on client-hints and user-agent, and populating the media query state with it. After some time I've encountered a bug related to SSR with @emotion/native and react-native-web, which led me to the idea of using separate packages on web and native. It turned out to be working quite well, generating media queries and SSR styles on the web without the need for rendering each breakpoint as a DOM node, while on native responsive styles are handled by JS. It seems like it would be possible to use the same approach to use pseudo selectors on web, and handle them using JS on native (maybe wrapping with Pressable and passing interaction state as a prop - isHovered, isFocused?). Here's the issue in detail with an example repo: https://github.com/emotion-js/emotion/issues/2356#issuecomment-825640973. Maybe we could a similar approach with Dripsy?

nandorojo commented 3 years ago

Yeah I hear you, and I did try that.

The problem is, I still want to wrap React Native components, such as View, Text, and any custom ones. But React Native Web doesn't forward the className prop, which is what allows us to use those kinds of styles, such as media queries.

nandorojo commented 3 years ago

The best solution I've come up with for 95% of the time is a Div component I made. It renders a plain div on web with CSS media queries, and a Dripsy View on native. It only accepts children and the SX prop, which is mostly fine. I'll be doing the same with a Span for text. It uses media queries like theme UI on web, and renders a plain span on web. I should be able to release this next version, I've been using it successfully in my app.

miikebar commented 3 years ago

That's true, I had to use the patch from SnackUI which brings back the className to get @emotion/styled to work with React Native components through React Native Web. Not sure if using this patch for a library would be ideal, but it fixes a lot of problems

nandorojo commented 3 years ago

Yeah, I'm concerned about a patch for something so fundamental, though. While I really wish RNW had a className escape hatch (like, I am shocked it doesn't), it seems like this is the best we can do. I don't want a design system to force people to change such a fundamental thing about RNW.

I tried to discuss this on the RNW repo, along with many others, and got nowhere. Nicolas doesn't seem interested in allowing class names, and doesn't think CSS media queries should be used. Instead, he recommends conditional rendering altogether.

🤷🏼‍♂️

nandorojo commented 3 years ago

I'll probably release a major version (v2) with the Div and Span as the recommended API. It's really a bummer, since I don't want to entirely circumvent RNW, and it feels like doing exactly that.

The alternative, and recommended approach by RNW, is to just drop SSR support and use the Dimensions API. This is also worth considering.

nandorojo commented 3 years ago

I tried my best here: https://github.com/necolas/react-native-web/issues/1318#issuecomment-661382290

miikebar commented 3 years ago

Yeah, I've seen the discussion around the className is quite heated 😁. Guess I'll continue using the patch for now as it seems to be the simplest and most efficient solution in my case.

nandorojo commented 3 years ago

Yeah it's really tough.

jeffscottward commented 1 year ago

Is this the best solution at this point?

// * We have extracted colors to an external refrence for use in
// * react native element's `style={({ hovered, pressed }) => ({`
// * because Dripsy doesn't support hover states through `sx` and `&:hover`
// * https://github.com/nandorojo/dripsy/issues/7
Screenshot 2023-04-20 at 1 37 37 PM Screenshot 2023-04-20 at 1 37 55 PM Screenshot 2023-04-20 at 1 39 03 PM
nandorojo commented 1 year ago

I ultimately decided to leave this up to moti/interactions, this way I could control all hover / press styles with performant animations.

jeffscottward commented 1 year ago

I ultimately decided to leave this up to moti/interactions, this way I could control all hover / press styles with performant animations.

Gotcha, so this is fine if I need theme values, just that if I want to change or animate anything, I'm gonna have to explicitly pull those in.

I'm not so in need of animations, I may upgrade later though.

I need the responsive stuff most of all so Dripsy takes precedence. Although there is probably a hook way of doing that without SX.