facebook / stylex

StyleX is the styling system for ambitious user interfaces.
https://stylexjs.com
MIT License
8.34k stars 307 forks source link

Target '.class[pseudo class] [pseudo element]', ie. '.button:hover ::before', or ':before:hover:prop' in tw #137

Closed olivierpascal closed 9 months ago

olivierpascal commented 9 months ago

Is your feature request related to a problem? Please describe.

I would like to target .class[pseudo class] [pseudo element], ie. .button:hover ::before.

Describe a solution you'd like

const buttonStyles = stylex.create({
  shadow: {
    boxShadow: {
      default: null,
      ':hover': {
        '::before': `0 4px 4px 0 ${colorScheme.shadow}`,
        '::after': `0 8px 12px 6px ${colorScheme.shadow}`,
      },
    },
  },
});

Describe alternatives you've considered

I have no alternative yet.

necolas commented 9 months ago

Did you mean .button:hover::before?

You wouldn't typically need a pseudo-element here, as you can use an empty element and then set the value of a custom variable (used by the empty element) on :hover

nmn commented 9 months ago

What you are asking for is essentially a descendent selector. We have documented that this is not supported for now.

https://stylexjs.com/docs/learn/thinking-in-stylex/#encapsulation

The current workaround for such situations is to set the value of a variable in the parent and read it in the child.

import {myVars} from './variables.stylex';

const buttonStyles = stylex.create({
  shadow: {
    [myVars.shadowStart]: {
      default: null,
      ':hover': `0 4px 4px 0 ${colorScheme.shadow}`,
    },
  },
  child: {
    boxShadow: myVars.shadowStart,
  }
});
olivierpascal commented 9 months ago

That's genius. Thanks.

olivierpascal commented 9 months ago

@nmn Hum after all I think I don't fully get how your workaround works.

How does myVars.shadowStart is set in your code? Does [myVars.shadowStart]: { ... } can really set a var content?

I get eslint: Computed key cannot be resolved when I am using your syntax.

Also, can stylex.create be used to set an arbitrary var like --my-var: 'my-content';? I get eslint: This is not a key that is allowed by stylex when trying to do so.

Thanks.

nmn commented 9 months ago

Does [myVars.shadowStart]: { ... } can really set a var content?

Yes! StyleX can use variables in both the key and var position. This approach is meant for one-offs, as opposed to createTheme which is meant to override a bunch of variables together.

I get eslint: Computed key cannot be resolved when I am using your syntax.

This is bug in the ESLint plugin. I tried to fix it just before release, but it needs some more work.

...can stylex.create be used to set an arbitrary var like --my-var: 'my-content';?

This is not an officially supported pattern because you're using a non-StyleX variable, but it does work.

olivierpascal commented 9 months ago

Ok thanks. After all it seems very hacky to me, and too complicated for my usecase. I end up using JS events onPointerEnter / onPointerLeave and a React state variable. Still hacky, not perfect but cleaner imho, waiting for a formal API for descendants and siblings.

nmn commented 9 months ago

We know the approach we can take, but still discussing the API. Will share an RFC soon.

olivierpascal commented 9 months ago

What you are asking for is essentially a descendent selector. We have documented that this is not supported for now.

Will you also support 'ascendent' selectors?

The current workaround for such situations is to set the value of a variable in the parent and read it in the child.

Do you by any chance have a workaround to address this?

<!-- the div container has to be red when the button is hover -->
<div class="container">
  <button>Button</Button>
</div>
.container:has(button:hover) {
  background-color: red;
}
olivierpascal commented 9 months ago

Here is my workaround:

const styles = stylex.create({
 container: {
    backgroundColor: {
      default: 'inherit',
      ':has(button:hover)': 'red',
    },
  }
});

NOTE: as @nmn mentioned, "this is a workaround and may stop working sometime in the future.".

nmn commented 9 months ago

@olivierpascal That workaround is fine and doesn't break encapsulation. (There is no styling at a distance, only observing). The official API will generate the same thing eventually. So yes, we're looking at selectors in all 4 directions.

necolas commented 9 months ago

It is unlikely, however, that any of these selector types will be supported in React Native - especially when it requires knowledge of the underlying element type that might be encapsulated within a composite component

nmn commented 9 months ago

Yes, to be clear, no complex selectors will be supported in React Native at all. In any of the four directions.

necolas commented 9 months ago

Arguably anything like this - :has(button:hover) - shouldn't be allowed on web either, as part of the selector is an element type - and there could be many or no button elements in the subtree. Inevitably people will want to scope this to a specific button when it goes bad, and will probably reach for a more specific selector type like an attribute selector. Not to mention that :hover-related styling has general drawbacks for touch device UX. Ignoring my skepticism about this pattern being a good UX, the most reliable mechanism is probably to use JS callbacks.

asaf commented 8 months ago

Is there a workaround for sibling selector such: .mycheckbox:checked + .label span

In a case such:

<input
  type="checkbox"
/>
<label>
  <span>content</span>
</label>

I'd like to change the span property based on the 'checked' state of the input.

thanks.

nmn commented 8 months ago

Using the "checkbox hack` is considered bad for accessibility anyway.

I'd suggest using JS to do this and also set the correct aria-attributes accordingly.

nikeee commented 5 days ago

What you are asking for is essentially a descendent selector. We have documented that this is not supported for now.

stylexjs.com/docs/learn/thinking-in-stylex#encapsulation

The current workaround for such situations is to set the value of a variable in the parent and read it in the child.

I'm currently trying to style an input[type=range] and I think I'm facing a similar problem. Consider this code that styles the slider's thumb based on the hover of the input:

.slider {
    &::-webkit-slider-thumb {
        background-color: red;
    }
    &:hover::-webkit-slider-thumb {
        background-color: green;
    }
}

If I understand the variable use correctly, this would be the workaround:

const someVars stylex.defineVars({
    thumbHover: "unset",
});

const slider = stylex.create({
    slider: {
        [someVars.thumbHover]: {
            default: "red",
            ":hover": "green",
        },

        "::-webkit-slider-thumb": {
            backgroundColor: someVars.thumbHover
        },
    },
});

Is this approach correct? If so, does it resemble the current best practice?

ESlint currently yields: Keys must be strings via eslint(@stylexjs/valid-styles)

To align my mental model of what this does internally, does the resulting code end up in something like this (the var name being generated in some way by the compiler)?

.slider {
    --someVars-thumbHover: red;
    &:hover {
        --someVars-thumbHover: green;
    }

    &::-webkit-slider-thumb {
        background-color: var(--someVars-thumbHover);
    }
}
nmn commented 5 days ago

@nikeee Yes, this is currently the recommended best practice.

The one important thing to note is that you must follow the rules when defining variables when using stylex.defineVars. i.e., stylex.defineVars must be in a separate file and the file name must have a .stylex.ts extension.

Making this change will also fix the lint error that you're seein.


// tokens.stylex.ts
export const someVars stylex.defineVars({
    thumbHover: "unset",
});
// somefile.tsx
import {someVars} from './tokens.stylex.ts';

const slider = stylex.create({
    slider: {
        [someVars.thumbHover]: {
            default: "red",
            ":hover": "green",
        },

        "::-webkit-slider-thumb": {
            backgroundColor: someVars.thumbHover
        },
    },
});