Skip to content

Commit 6cb2774

Browse files
authored
Merge pull request #55 from component-driven/with-selector-nested
2 parents c42b4f9 + 3b955cf commit 6cb2774

File tree

4 files changed

+540
-293
lines changed

4 files changed

+540
-293
lines changed

.changeset/fifty-socks-crash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@component-driven/with-selector": minor
3+
---
4+
5+
Add support for nested selectors

packages/with-selector/Readme.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,58 @@ const Button = styled('button')`
5858
</WithSelector>
5959
</>
6060
```
61+
62+
## Support for nested selectors
63+
64+
```jsx
65+
import styled from 'styled-components'
66+
67+
const InputWrapper = styled('div')({
68+
padding: 4,
69+
border: '1px solid #ccc',
70+
borderRadius: 3,
71+
background: '#efefef',
72+
'& > input': {
73+
border: '1px solid green'
74+
},
75+
'> button': {
76+
position: 'relative',
77+
appearance: 'none',
78+
'::after': {
79+
position: 'absolute',
80+
right: 0,
81+
top: 0,
82+
background: 'pink',
83+
content: "''"
84+
}
85+
},
86+
':focus-within': {
87+
background: '#ccc',
88+
input: {
89+
outline: 'none',
90+
borderColor: 'red'
91+
},
92+
button: {
93+
marginLeft: 10,
94+
borderColor: 'yellow',
95+
'::after': {
96+
content: "'focus'"
97+
}
98+
}
99+
}
100+
})
101+
102+
const Input = React.forwardRef((props, ref) => (
103+
<InputWrapper {...props} ref={ref}>
104+
<input type="text" />
105+
<button>this is a nested button</button>
106+
</InputWrapper>
107+
))
108+
109+
;<>
110+
<Input placeholder="Normal input" />
111+
<WithSelector selector=":focus-within">
112+
<Input placeholder="Normal input" />
113+
</WithSelector>
114+
</>
115+
```

packages/with-selector/src/WithSelector.js

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,38 +10,41 @@ function addStylesheetRule(rule) {
1010

1111
const generateCssClassName = customAlphabet(
1212
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
13-
32
13+
16
1414
)
1515

1616
// Inspired by https://codesandbox.io/s/pseudo-class-sticker-sheet-jiu2x
1717
const useAddSelector = (ref, selector) => {
1818
const [modifiedClassName, setModifiedClassName] = useState('')
1919
useEffect(() => {
20-
const className = ref.current.classList[ref.current.classList.length - 1]
20+
const className = ref.current?.classList[ref.current.classList.length - 1]
2121
const fullSelector = `${className && `.${className}`}${selector}`
22-
// NOTE: This could be improved, because checking the provided selector starts with a '.'
23-
// is probably not the best way to determine the selector is a class name or not.
24-
const isClassNameSelector = selector.startsWith('.')
25-
let newRule = ''
26-
for (const ss of document.styleSheets) {
27-
for (const rule of ss.cssRules) {
28-
if (fullSelector === rule.selectorText) {
29-
const cssClassName = isClassNameSelector ? selector : `.${generateCssClassName()}`
30-
newRule = `${cssClassName} { ${rule.style.cssText}}`
31-
setModifiedClassName(cssClassName.substring(1))
32-
break
22+
const newClassName = generateCssClassName()
23+
let newRules = []
24+
for (const styleSheet of document.styleSheets) {
25+
for (const rule of styleSheet.cssRules) {
26+
if (rule.selectorText?.startsWith(fullSelector)) {
27+
/**
28+
* Replace current CSS selector with the generated one so that
29+
* after adding the newClassName all children can be matched
30+
* i.e. we map:
31+
* .component:focus > input -> .generatedClass > input
32+
*/
33+
const CSSSelector = rule.selectorText.replace(fullSelector, `.${newClassName}`)
34+
newRules.push(`${CSSSelector} { ${rule.style.cssText} }`)
3335
}
3436
}
35-
if (newRule) {
36-
addStylesheetRule(newRule)
37-
break
37+
if (newRules.length > 0) {
38+
newRules.forEach(addStylesheetRule)
39+
setModifiedClassName(newClassName)
40+
break // Avoid triggering infinite loop since we're modifying stylesheets
3841
}
3942
}
4043
}, [ref, selector])
4144
return [modifiedClassName]
4245
}
4346

44-
const WithSelector = props => {
47+
const WithSelector = (props) => {
4548
const ref = useRef(null)
4649
const [modifiedClassName] = useAddSelector(ref, props.selector)
4750

0 commit comments

Comments
 (0)