Khan / aphrodite

Framework-agnostic CSS-in-JS with support for server-side rendering, browser prefixing, and minimum CSS generation

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

How to handle descendant selection of pseudo-states?

zgotsch opened this issue · comments

If I want to apply styles to a child based on its parent's :hover state, I think I have to track hover state in js. Is that accurate? Do you think handling this is in the scope of aphrodite?

Specifically I am thinking of something that emulates the CSS:

.parent:hover .child {
    background-color: blue;
}

In general, having parents affect the styles of their children is a pattern we want to avoid, but I can see why this particular example might occur within a single component's markup.

I'm reluctant to see patterns like this become first class citizens, but we do need some escape hatch way of writing arbitrary CSS (whether it's globally scoped or scoped to the generated selector), akin to dangerouslySetInnerHTML, which you'd be able to use to do that.

One hack which we're cautiously using right now works only because of the way pseudo-selectors are handled.

const styles = StyleSheet.create({
  parent: {
    ':hover .child': {
      backgroundColor: blue
    }
  }
});

Which is totally not part of the API, and I fully expect it to break at some point.

Come to think of it, this also might break on merge

Interesting! That definitely was not an intention of the API design, but perhaps is something we should support for the sake of API simplicity. I don't see any immediate reason for that to break on merging.

I also just ran into this case. I'll proceed with @zgotsch's workaround. Is there anything specific that we could do to make this a first-class citizen so I could rely on it and not worry about this breaking in future releases? I'm happy to contribute to the project with a little direction :-)

Actually, thinking about this more, I think that this is a scenario that would be better to avoid supporting and we should find another way to work around this issue. Here's the kind of API I'd like to see (and maybe help build):

const styles = StyleSheet.create({
  parent: {
    ':hover': {
      backgroundColor: 'white',
      nonCssPropertyShouldCreateAnotherClass: {
        color: 'black',
      },
    },
  },
})

const { parentClassName, nonCssPropertyShouldCreateAnotherClassClassName } = cssWithDecendents(styles.parent)
// parentClassName === 'parent-blahblah'
// nonCssPropertyShouldCreateAnotherClassClassName === 'nonCssPropertyShouldCreateAnotherClass-blahblah'

Which would produce classes with the following CSS:

.parent-blahblah:hover {
  background-color: 'white';
}

.parent-blahblah:hover .nonCssPropertyShouldCreateAnotherClass {
  color: 'black';
}

Obviously, the names are up for debate. But I think an entirely new method should be used and we shouldn't overload the css method. Also, the css method should probably warn when given a property that is not a css property it understands (maybe? but that's a different issue).

Thoughts 💭?

@kentcdodds I really like that interface. Seems like a great balance between being flexible and still sorta following the "inline-styling way". Some random thoughts:

  • We don't have a good way to distinguish whether something is a CSS property or not. Maybe we could make the key value something like ">descendantClass", where the > indicates a descendant?
  • What would rules for recursion be? I think I would want to limit the depth that you could go to 1 level (so you would never be generating .a .b .c class names).

Maybe we could make the key value something like ">descendantClass", where the > indicates a descendant

Seems reasonable to me 👍

As for depth, I don't see why aphrodite should concern itself with enforcing a depth ¯_(ツ)_/¯ while I agree developers should probably not go too deep on this, I don't think we'd have to put in extra effort to combat it. It'd turn into a serious code smell before it caused aphrodite trouble I think.

If there was an API for defining a ruleset you could easily determine which are properties vs a descendantClass:

const child = StyleSheet.createRuleset({
  color: 'black'
}
const styles = StyleSheet.create({
  parent: {
    ':hover': {
      backgroundColor: 'white',
      child: child
    }
  }
})

const { parent, child } = css(styles)
// parent === 'parent-blahblah'
// child === 'child-xyz'

Ok, I think that we've got two solid ideas, which should we go with?

Use >decendantClass

const styles = StyleSheet.create({
  parent: {
    ':hover': {
      backgroundColor: 'white',
      '>child': {color: 'black'}
    }
  }
})

const { parent, child } = css(styles.parent)
// parent === 'parent-blahblah'
// child === 'child-xyz'

Have a special API for creating rule sets

const styles = StyleSheet.create({
  parent: {
    ':hover': {
      backgroundColor: 'white',
      child: StyleSheet.createRuleset({color: 'black'}
    }
  }
})

const { parent, child } = css(styles.parent)
// parent === 'parent-blahblah'
// child === 'child-xyz'

I'm not sure which I like better. Would love to get feedback from others.

commented

I like the simplicity (for the developer using aphrodite) of >descendentClass. However, I'm wondering if I'm misinterpreting something:

The > symbol is reminiscent of .foo > .bar in true CSS, which indicates a direct descendent, whereas via the examples given above, it seems like you are proposing that this produce css which is selecting any (direct or indirect) descendent.

I think that this would be a misleading choice of api.

Am I misinterpreting?

Also, is the goal to be able to support both direct and arbitrary descendents? It seems like arbitrary descendents should cover all desired use cases since the class names are uniquely generated, whereas direct descendents would not.

Agreed. That would be misleading... I also think that I prefer the simplicity of the > but not at the expense of it being misleading. Other thoughts of how we could make the API more intuitive and simple?

commented

Also, I think I agree with this:

... I think an entirely new method should be used and we shouldn't overload the css method.

cssWithDescendents or css.descendents seems reasonable to me, though I'll have to think on it a bit more.

@montemishkin You are correct, the > symbol would be a bit misleading about what it does if we allowed any descendents. Can anyone think of a better symbol to use there, or another way to indicate children?

Would we still want to have this syntax if we supported unsafeRawCss as discussed in #30?

Is there a reason we couldn't just do:

const styles = StyleSheet.create({
  parent: {
    ':hover': {
      backgroundColor: 'white',
      child: {color: 'black'}
    }
  }
})

const { parent, child } = css.descendents(styles.parent)
// parent === 'parent-blahblah'
// child === 'child-xyz'

I'm pretty sure that we can just say that if the value of a css property is an object, consider it a descendant.

As for the unsafeRawCss, the difference with this feature is that I can still get a className back for child which I can apply to a specific element. The use case with unsafeRawCss that I see is that you can use any kind of selector you want and you don't get a className back for it, so it covers cases where you're not in control of the markup (like with dangerouslySetInnerHTML)

I'm pretty sure that we can just say that if the value of a css property is an object, consider it a descendant.

This wouldn't work, because we sometimes allow real objects as values for css properties, for instance with fontFamily you can use arrays/objects, or with animationName you can provide objects.

Just a drive-by kibbitz, but one possible syntax for "direct or indirect descendant" could be ">>descendentClass".

Okay, I played around with a couple different alternatives, and it looks like we haven't really talked about what gets done with the results of css.descendents. Would it make sense to plug those things into a call to css()? So after something like:

const styles = StyleSheet.create({
  parent: {
    ':hover': {
      backgroundColor: 'white',
      '>>child': {color: 'black'},
    },
  },

  red: {
    color: 'red',
  },
})

const { parent, child } = css.descendents(styles.parent);

Would we be doing className={child + " " + css(styles.red)} or className={css(styles.red, child)}? It sounds like we were talking about the first one, but the second one looks nicer.

I also played around with something that looks like

const styles = StyleSheet.create({
  parent: {
    ':hover': {
      backgroundColor: 'white',
      '>>child': {color: 'black'},
    },
  },

  red: {
    color: 'red',
  },
})

<div className={css(styles.parent)}>
  <div className={css(styles.red, styles.parent.child)}> // <- 

but I don't think that we'll actually be able to generate appropriate class names using this interface (even though it looks even nicer than the css.descendents stuff).

Just a drive-by, but I really like the second API, Emily (the one with styles.parent.child). I would also be happy with something like css(styles.red, styles.parent.descendent('child')) or similar.

@zgotsch I like that too. I'm a little scared of a situation like this though:

const styles = StyleSheet.create({
    child: {
        color: 'red',
    },

    base: {
        color: 'black',
        ':hover': {
            '>>child': {
                color: 'blue',
            }
        },
    },

    greenchild: {
        ':hover': {
            '>>child': {
                color: 'green',
            },
        },
    },
});

<div className={css(styles.base, styles.greenchild)}>
    <div className={css(styles.child, styles.base.child)}>

Presumably we would want to generate CSS like...

.child_XXX {
    color: red;
}
.base_XXX-o_O-greenchild_XXX {
    color: black;
}
.base_XXX-o_O-greenchild_XXX:hover .child_XXX {
    color: green;
}

but then you might want

<div className={css(styles.base, styles.greenchild)}>
    <div className={css(styles.child, styles.greenchild.child)}>

to do the same thing? But we can't make styles.base.child and styles.greenchild.child add the same class names, so maybe we have to duplicate the related CSS:

.base_XXX-o_O-greenchild_XXX:hover .child_XXX { // child from base
    color: green;
}
.base_XXX-o_O-greenchild_XXX:hover .child_XXX { // child from greenchild
    color: green;
}

Sorry for brain dumping. I'm gonna play around with this...

Is there any update regarding this issue?
I'm using .mySelector:nth-child(3) .childElement which currently I can only set up as a global CSS.

@xymostech Continuing discussion about alternatives to the complexity introduced in #61 here.

While the goal of making class names an implementation detail and not controllable by the user, I think the complexity that introduces in #61 is overkill for trying to solve this problem.

Here's my proposed alternative: just use class names. I know this does, to some extent, defeat the niceness of reasoning about whether certain styles/classnames are in use, but I think this use case is sufficiently rare that I think it's okay.

const styles = StyleSheet.create({
  parent: {
    ':hover': {
      backgroundColor: 'white',
      '.someChild': {
        color: 'black',
      },
    },
  },
});

const Component = () => <div className={css(styles.parent)}>
    White background on hover
    <div className='someChild'>
        Black text on hover
    </div>
</div>

Based on the discussion on this issue, this is what people are already doing as a hack, except that if we support this directly then the merging semantics will be correct.

If we so wished, we could generalize this to style arbitrary selectors, and potentially knock out #30 too, something like so:

const styles = StyleSheet.create({
  parent: {
    ':hover': {
      backgroundColor: 'white',
      'selector(.someChild)': {
        color: 'black',
      },
    },
    'selector(a)': {
         // styles for anchors in the element go here.
    }
  },
});

If we went the selector() route, we could preserve correct merging semantics by requiring that the selector specified not contain any spaces or colons. We could include in the docs the reasoning for this syntax existing, with a discouraging note about why you shouldn't use them unless you absolutely have to :)

Thoughts?

Another motivation for supporting child selectors (outside of pseudo-classes even) is to style React components that we don't control. For example, React Bootstrap has a NavItem component that renders <li><a /></li>, and supports setting only the <li> element's className.

In this case I want to write:

StyleSheet.create({
  navItem: {
    'selector(a)': { color: linkColor }
  }
});

but sometimes would want more nested selectors like selector(a img) too.

@ide The syntax I'm proposing would have you do 'selector(a)': { 'selector(img)': { color: linkColor } } for that instead. The generated CSS would do what you want, but having them separated would make the merging semantics more obvious.

@ide I am facing the same problem. I came up with a pretty hacky function to select children:

function children(selector) {
  return `:active, ${selector}`;
}

/// use it like this

StyleSheet.create({
  header: {
    color: 'red',
    [children('li a')]: {
      color: 'blue',
    }
  },
})

Clearly, this works as expected only if the parent component cannot be active, otherwise you'd have to use another selector. I am using it until an official API is designed

Thinking more about the selector(a) syntax, I'm worried about the non-determinism demonstrated in the following situation:

const Component = () => <div className={css(styles.foo)}>
    <div className={css(styles.bar)}>
        <span>Hello</span>
    </div>
</div>

const styles = StyleSheet.create({
    foo: { 'selector(span)': { color: 'red' }},
    bar: { 'selector(span)': { color: 'green' }}
});

The color of the resulting span will depend on the injection order of foo and bar, which is a kind of non-determinism we specifically try to avoid in aphrodite.

To see why this happens, consider the following:

<div><span><em>Hello</em></span></div>

with CSS

span em { color: green; }
div em { color: red; }

In this case, "Hello" will be red. The selectors have equivalent precedence, so the latter is used. If the CSS is switched to:

div em { color: green; }
span em { color: red; }

Then "Hello" becomes green. This reversal of the declaration order mimics what would happen in aphrodite depending on whether css is first called with styles.foo as an argument, or first called with styles.bar as an argument.

I don't think there's a clear implementation of the proposed selector(a) syntax that doesn't have this problem.

@jlfwong :( Oof. I guess that probably happens with my pull request implementation, as well?

Is this kind of non-determinism already an issue with multiple styles on the same component? For example:

<div className={css(styles.s1, styles.s2)} />
// or
<div className={classNames(css(styles.s1), css(styles.s2))} />

const styles = StyleSheet.create({
  s1: { color: 'red' },
  s2: { color: 'green' },
});

My understanding is that Aphrodite will emit two class selectors of the same specificity, so the last one emitted wins. I guess technically it's deterministic but hard to reason about since the order of the css() calls matter. But if this is OK, then there could be an argument that it's OK to have similar behavior for the selector(a) syntax too.

@ide That is true, but you should always do css(styles.s1, styles.s2). Actually, I think we have that written up somewhere internal but I don't know if we ever say that in the repo. We might want to put that in the README as a caveat.

The point is, adding two classnames is non-determinism that you can avoid (by combining the calls to css()). The non-determinism that @jlfwong is describing cannot be solved by anything that aphrodite does.

const styles = StyleSheet.create({ parent: { ':hover .child': { backgroundColor: blue } } });

This is actually rather intuitive. I'm using it in a project.

@misterfresh That's fine, but do recognize that it is a hack that's not specifically supported by aphrodite, and might break in future versions.

@xymostech if hey way that @misterfresh is handling child elements is a hack, did the main contributors to this project decide on what will be the recommended way to implement styles on nested children in the future? I can use this now as we only have a beta application, but my boss just gave me the nod on using Aphrodite (AWESOME library btw) and I'll need to know the recommended way to handle this in the future. Thanks.

has there been any progress for this? I am trying to select td's in a table table tr td:first-of-type

Yeah, I want to selected all the anchors in a div (need to style component I don't control), whats the status of this? I see its closed but as far as I can tell there was no resolution?

@sontek This was closed due to the solution in #95, which is documented here: https://github.com/Khan/aphrodite#advanced-extensions

This hacky solution might help if you end up in this thread...

.parent {
  --child-background-color: red
}
.parent:hover {
  --child-background-color: blue
}
.child {
  background-color: var(--child-background-color)
}