Skip to content

Commit 1b74aff

Browse files
authored
Respect Tailwind hoverOnlyWhenSupported, and fix placeholder-shown with Input (#5587)
1 parent a8fc9ee commit 1b74aff

File tree

2 files changed

+56
-16
lines changed

2 files changed

+56
-16
lines changed

packages/tailwindcss-react-aria-components/src/index.js

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,27 @@ const nativeVariantSelectors = new Map([
6666
['hovered', ':hover'],
6767
['focused', ':focus'],
6868
['readonly', ':read-only'],
69-
['open', '[open]'],
69+
['open', '[open]']
70+
]);
71+
72+
// Variants where both native and RAC attributes should apply. We don't override these.
73+
const nativeMergeSelectors = new Map([
7074
['placeholder', ':placeholder-shown']
7175
]);
7276

7377
// If no prefix is specified, we want to avoid overriding native variants on non-RAC components, so we only target elements with the data-rac attribute for those variants.
74-
let getSelector = (prefix, attributeName, attributeValue) => {
78+
let getSelector = (prefix, attributeName, attributeValue, hoverOnlyWhenSupported) => {
7579
let baseSelector = attributeValue ? `[data-${attributeName}="${attributeValue}"]` : `[data-${attributeName}]`;
76-
if (prefix === '' && nativeVariantSelectors.has(attributeName)) {
77-
return [`&:where([data-rac])${baseSelector}`, `&:where(:not([data-rac]))${nativeVariantSelectors.get(attributeName)}`];
80+
let nativeSelector = nativeVariantSelectors.get(attributeName);
81+
if (prefix === '' && nativeSelector) {
82+
let wrappedNativeSelector = `&:where(:not([data-rac]))${nativeSelector}`;
83+
let nativeSelectorGenerator = wrappedNativeSelector;
84+
if (nativeSelector === ':hover' && hoverOnlyWhenSupported) {
85+
nativeSelectorGenerator = wrap => `@media (hover: hover) and (pointer: fine) { ${wrap(wrappedNativeSelector)} }`;
86+
}
87+
return [`&:where([data-rac])${baseSelector}`, nativeSelectorGenerator];
88+
} else if (prefix === '' && nativeMergeSelectors.has(attributeName)) {
89+
return [`&${baseSelector}`, `&${nativeMergeSelectors.get(attributeName)}`];
7890
} else {
7991
return `&${baseSelector}`;
8092
}
@@ -88,36 +100,46 @@ let mapSelector = (selector, fn) => {
88100
}
89101
};
90102

103+
let wrapSelector = (selector, wrap) => {
104+
if (typeof selector === 'function') {
105+
return selector(wrap);
106+
} else {
107+
return wrap(selector);
108+
}
109+
};
110+
91111
let addVariants = (variantName, selectors, addVariant, matchVariant) => {
92-
addVariant(variantName, selectors);
112+
addVariant(variantName, mapSelector(selectors, selector => wrapSelector(selector, s => s)));
93113
matchVariant(
94114
'group',
95115
(_, {modifier}) =>
96116
modifier
97-
? mapSelector(selectors, selector => `:merge(.group\\/${modifier})${selector.slice(1)} &`)
98-
: mapSelector(selectors, selector => `:merge(.group)${selector.slice(1)} &`),
117+
? mapSelector(selectors, selector => wrapSelector(selector, s => `:merge(.group\\/${modifier})${s.slice(1)} &`))
118+
: mapSelector(selectors, selector => wrapSelector(selector, s => `:merge(.group)${s.slice(1)} &`)),
99119
{values: {[variantName]: variantName}}
100120
);
101121
matchVariant(
102122
'peer',
103123
(_, {modifier}) =>
104124
modifier
105-
? mapSelector(selectors, selector => `:merge(.peer\\/${modifier})${selector.slice(1)} ~ &`)
106-
: mapSelector(selectors, selector => `:merge(.peer)${selector.slice(1)} ~ &`),
125+
? mapSelector(selectors, selector => wrapSelector(selector, s => `:merge(.peer\\/${modifier})${s.slice(1)} ~ &`))
126+
: mapSelector(selectors, selector => wrapSelector(selector, s => `:merge(.peer)${s.slice(1)} ~ &`)),
107127
{values: {[variantName]: variantName}}
108128
);
109129
};
110130

111-
module.exports = plugin.withOptions((options) => (({addVariant, matchVariant}) => {
131+
module.exports = plugin.withOptions((options) => (({addVariant, matchVariant, config}) => {
112132
let prefix = options?.prefix ? `${options.prefix}-` : '';
133+
let future = config().future;
134+
let hoverOnlyWhenSupported = future === 'all' || future?.hoverOnlyWhenSupported;
113135

114136
// Enum attributes go first because currently they are all non-interactive states.
115137
Object.keys(attributes.enum).forEach((attributeName) => {
116138
attributes.enum[attributeName].forEach(
117139
(attributeValue) => {
118140
let name = shortNames[attributeName] || attributeName;
119141
let variantName = `${prefix}${name}-${attributeValue}`;
120-
let selectors = getSelector(prefix, attributeName, attributeValue);
142+
let selectors = getSelector(prefix, attributeName, attributeValue, hoverOnlyWhenSupported);
121143
addVariants(variantName, selectors, addVariant, matchVariant);
122144
}
123145
);
@@ -127,7 +149,7 @@ module.exports = plugin.withOptions((options) => (({addVariant, matchVariant}) =
127149
let variantName = Array.isArray(attribute) ? attribute[0] : attribute;
128150
variantName = `${prefix}${variantName}`;
129151
let attributeName = Array.isArray(attribute) ? attribute[1] : attribute;
130-
let selectors = getSelector(prefix, attributeName);
152+
let selectors = getSelector(prefix, attributeName, null, hoverOnlyWhenSupported);
131153
addVariants(variantName, selectors, addVariant, matchVariant);
132154
});
133155
}));

packages/tailwindcss-react-aria-components/src/index.test.js

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@ const postcss = require('postcss');
55
let html = String.raw;
66
let css = String.raw;
77

8-
function run({options, content}) {
8+
function run({options, content, future = {}}) {
99
let {currentTestName} = expect.getState();
1010
let config = {
1111
plugins: [require('./index.js')(options)],
1212
corePlugins: {preflight: false},
1313
theme: {colors: {red: 'red'}},
14-
content: [{raw: content}]
14+
content: [{raw: content}],
15+
future
1516
};
1617

1718
return postcss(tailwind(config)).process(
@@ -138,11 +139,11 @@ test('variants', async () => {
138139
--tw-bg-opacity: 1;
139140
background-color: rgb(255 0 0 / var(--tw-bg-opacity))
140141
}
141-
.placeholder-shown\:bg-red:where([data-rac])[data-placeholder] {
142+
.placeholder-shown\:bg-red[data-placeholder] {
142143
--tw-bg-opacity: 1;
143144
background-color: rgb(255 0 0 / var(--tw-bg-opacity))
144145
}
145-
.placeholder-shown\:bg-red:where(:not([data-rac])):placeholder-shown {
146+
.placeholder-shown\:bg-red:placeholder-shown {
146147
--tw-bg-opacity: 1;
147148
background-color: rgb(255 0 0 / var(--tw-bg-opacity))
148149
}
@@ -505,3 +506,20 @@ test('variants with prefix', async () => {
505506
);
506507
});
507508
});
509+
510+
test('hoverOnlyWhenSupported', () => {
511+
let content = html`<div data-rac className="hover:bg-red"></div>`;
512+
return run({content, future: {hoverOnlyWhenSupported: true}}).then((result) => {
513+
expect(result.css).toContain(css`
514+
.hover\:bg-red:where([data-rac])[data-hovered] {
515+
--tw-bg-opacity: 1;
516+
background-color: rgb(255 0 0 / var(--tw-bg-opacity))
517+
}
518+
@media (hover: hover) and (pointer: fine) {
519+
.hover\:bg-red:where(:not([data-rac])):hover {
520+
--tw-bg-opacity: 1;
521+
background-color: rgb(255 0 0 / var(--tw-bg-opacity))
522+
}
523+
}`);
524+
});
525+
});

0 commit comments

Comments
 (0)