Skip to content

Commit 5f69479

Browse files
authored
RAC: add Tailwind plugin for RAC states (#4933)
* add tailwind plugin for RAC states * add docs * add option for prefix * update docs to mention prefix option * update to use data-rac attribute * change focused->focus and hovered->hover * update example app to use plugin * fix selector * fix test * add native element to test app * temp run verdaccio build * temp run verdaccio build * revert verdaccio * typo
1 parent e35ab0d commit 5f69479

File tree

9 files changed

+485
-3
lines changed

9 files changed

+485
-3
lines changed

examples/rac-spectrum-tailwind/src/App.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ export function App() {
1616
<div className="grid justify-center grid-cols-1 gap-160 auto-rows-fr">
1717
<SelectBoxExample />
1818
<SentimentRatingGroup />
19+
<div className="flex justify-center">
20+
<div className="flex flex-col max-w-sm">
21+
<label for="test-input">Native input</label>
22+
<input id="test-input" className="border focus:bg-gray-200 focus:outline-none focus:border-blue-600 hover:border-blue-300" />
23+
<p>For the purpose of ensuring Tailwind's default selectors still work for non-RAC elements when using the plugin.</p>
24+
</div>
25+
</div>
1926
</div>
2027
</Provider>
2128
);
@@ -24,6 +31,7 @@ export function App() {
2431
function SelectBoxExample() {
2532
return (
2633
<RadioGroup
34+
data-rac
2735
className="flex flex-col space-y-2 text-center"
2836
defaultValue="Team"
2937
>
@@ -52,6 +60,7 @@ function SelectBoxExample() {
5260
function SelectBox({ name, icon, description }) {
5361
return (
5462
<Radio
63+
data-rac
5564
value={name}
5665
className={({ isFocusVisible, isSelected, isPressed }) => `
5766
flex justify-center p-160 m-160 h-2000 w-2000 focus:outline-none border rounded
@@ -100,8 +109,9 @@ function SentimentRatingGroup() {
100109
function SentimentRating({ rating }) {
101110
return (
102111
<Radio
112+
data-rac
103113
value={rating}
104-
className="flex items-center justify-center bg-white border rounded-full p-160 m-75 h-200 w-200 focus:outline-none focus-visible:ring dark:bg-black selected:bg-accent-800 dark:selected:bg-accent-800 selected:border-accent-800 selected:text-white pressed:bg-gray-200 dark:pressed:bg-gray-200 hovered:border-gray-300"
114+
className="flex items-center justify-center bg-white border rounded-full p-160 m-75 h-200 w-200 focus:outline-none focus-visible:ring dark:bg-black selected:bg-accent-800 dark:selected:bg-accent-800 selected:border-accent-800 selected:text-white pressed:bg-gray-200 dark:pressed:bg-gray-200 hover:border-gray-300"
105115
>
106116
{rating}
107117
</Radio>

examples/rac-spectrum-tailwind/tailwind.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ module.exports = {
77
require('./src/spectrum-preset.js')
88
],
99
plugins: [
10-
require('./src/rac-plugin.js')
10+
require('../../packages/tailwindcss-react-aria-components/src/index.js')
1111
],
1212
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@
170170
"sharp": "^0.31.2",
171171
"sinon": "^7.3.1",
172172
"storybook-dark-mode": "^1.1.1-canary.120.3843.0",
173+
"tailwindcss": "^3.2.2",
173174
"tempy": "^0.5.0",
174175
"typescript": "5.0.4",
175176
"typescript-strict-plugin": "^2.0.0",

packages/react-aria-components/docs/react-aria-components.mdx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,51 @@ If you are using Tailwind CSS, we recommend using the [tailwindcss-animate](http
425425

426426
The documentation for each component includes many examples styled using vanilla CSS and the default class names. We also have an example of many of the components using Tailwind CSS. You can find the [code](https://github.com/adobe/react-spectrum/blob/main/examples/rac-tailwind/src/App.js) in the repo, as well as a [live demo](https://reactspectrum.blob.core.windows.net/reactspectrum/f239d0b1a96c3e6119135fe6bbf1994dc9984257/verdaccio/rac-tailwind/index.html).
427427

428+
## Tailwind CSS Plugin
429+
430+
There is a Tailwind CSS plugin available which makes styling different [states](#states) easier.
431+
432+
This allows an element with the `data-selected` attribute to be styled in Tailwind with `selected:` instead of `data-[selected]:`. Non-boolean data attributes follow the `{name}-{value}` pattern, so you can style an element with `data-orientation="horizontal"` using `orientation-horizontal:`.
433+
434+
To install:
435+
436+
```
437+
yarn add tailwindcss-react-aria-components
438+
```
439+
440+
Then add the plugin to your `tailwind.config.js` file:
441+
442+
```jsx
443+
/** @type {import('tailwindcss').Config} */
444+
module.exports = {
445+
plugins: [
446+
require('tailwindcss-react-aria-components')
447+
],
448+
}
449+
```
450+
451+
You can optionally specify a prefix using `require('tailwindcss-react-aria-components')({prefix: 'rac'})`. This will allow you to use `rac-selected:` instead of `selected:`.
452+
453+
### Boolean states
454+
455+
The `data-selected` state can be styled with `selected:` like this:
456+
457+
```jsx
458+
<Button className="selected:bg-blue">
459+
{/* ... */}
460+
</Button>
461+
```
462+
463+
### Non-boolean states
464+
465+
The `data-orientation="vertical"` state can be styled with `orientation-vertical:` like this:
466+
467+
```jsx
468+
<Tabs className="orientation-vertical:flex-row">
469+
{/* ... */}
470+
</Tabs>
471+
```
472+
428473
## Components
429474

430475
### Buttons

packages/react-aria-components/src/utils.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,8 @@ export function useRenderProps<T>(props: RenderPropsHookOptions<T>) {
126126
return {
127127
className: computedClassName ?? defaultClassName,
128128
style: computedStyle,
129-
children: computedChildren
129+
children: computedChildren,
130+
'data-rac': ''
130131
};
131132
}, [className, style, children, defaultClassName, defaultChildren, values]);
132133
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "tailwindcss-react-aria-components",
3+
"version": "1.0.0-alpha.1",
4+
"description": "A Tailwind plugin that adds variants for data attributes in React Aria Components",
5+
"license": "Apache-2.0",
6+
"main": "src/index.js",
7+
"types": "src/types.d.ts",
8+
"source": "src/index.js",
9+
"files": [
10+
"dist",
11+
"src"
12+
],
13+
"sideEffects": false,
14+
"repository": {
15+
"type": "git",
16+
"url": "https://github.com/adobe/react-spectrum"
17+
},
18+
"peerDependencies": {
19+
"tailwindcss": ">=3.0.0 || insiders"
20+
},
21+
"devDependencies": {
22+
"postcss": "^7.0.0",
23+
"tailwindcss": "^3.2.2"
24+
},
25+
"publishConfig": {
26+
"access": "public"
27+
}
28+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
declare function plugin(options?: Partial<{ className: string, target: 'modern' | 'legacy' }>): {
2+
handler: () => void
3+
}
4+
5+
declare namespace plugin {
6+
const __isOptionsFunction: true;
7+
}
8+
9+
export = plugin
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import plugin from 'tailwindcss/plugin';
2+
3+
const attributes = {
4+
boolean: [
5+
['hover', 'hovered'],
6+
['focus', 'focused'],
7+
'focus-visible',
8+
'pressed',
9+
'disabled',
10+
'drop-target',
11+
'dragging',
12+
'empty',
13+
'allows-removing',
14+
'allows-sorting',
15+
'placeholder',
16+
'selected',
17+
'indeterminate',
18+
'readonly',
19+
'required',
20+
'entering',
21+
'exiting',
22+
'open',
23+
'unavailable',
24+
'current',
25+
'invalid'
26+
],
27+
enum: {
28+
'validation-state': ['invalid', 'valid'],
29+
placement: ['left', 'right', 'top', 'bottom'],
30+
type: ['literal', 'year', 'month', 'day'],
31+
layout: ['grid', 'stack'],
32+
orientation: ['horizontal', 'vertical']
33+
}
34+
};
35+
36+
// Variants we use that are already defined by Tailwind:
37+
// https://github.com/tailwindlabs/tailwindcss/blob/a2fa6932767ab328515f743d6188c2164ad2a5de/src/corePlugins.js#L84
38+
const nativeVariants = ['indeterminate', 'required', 'invalid', 'empty', 'focus-visible', 'disabled'];
39+
const nativeVariantSelectors = new Map([...nativeVariants.map((variant) => [variant, `:${variant}`]), ['hovered', ':hover'], ['focused', ':focus'], ['open', '[open]']]);
40+
41+
// 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.
42+
let getSelector = (prefix, attributeName, attributeValue) => {
43+
let baseSelector = attributeValue ? `[data-${attributeName}="${attributeValue}"]` : `[data-${attributeName}]`;
44+
if (prefix === '' && nativeVariantSelectors.has(attributeName)) {
45+
return `&:is([data-rac]${baseSelector}, :not([data-rac])${nativeVariantSelectors.get(attributeName)})`;
46+
} else {
47+
return `&${baseSelector}`;
48+
}
49+
};
50+
51+
module.exports = plugin.withOptions((options) => (({addVariant}) => {
52+
let prefix = options?.prefix ? `${options.prefix}-` : '';
53+
attributes.boolean.forEach((attribute) => {
54+
let variantName = Array.isArray(attribute) ? attribute[0] : attribute;
55+
variantName = `${prefix}${variantName}`;
56+
let attributeName = Array.isArray(attribute) ? attribute[1] : attribute;
57+
let selector = getSelector(prefix, attributeName);
58+
addVariant(variantName, selector);
59+
});
60+
Object.keys(attributes.enum).forEach((attributeName) => {
61+
attributes.enum[attributeName].forEach(
62+
(attributeValue) => {
63+
let variantName = `${prefix}${attributeName}-${attributeValue}`;
64+
let selector = getSelector(prefix, attributeName, attributeValue);
65+
addVariant(variantName, selector);
66+
}
67+
);
68+
});
69+
}));

0 commit comments

Comments
 (0)