// https://github.com/adobe/react-spectrum/blob/41ef71d18049af1fcec7d4a9953c2600ed1fa116/packages/tailwindcss-react-aria-components/src/index.js import plugin from "tailwindcss/plugin.js"; import type { PluginAPI } from "tailwindcss/types/config"; // Order of these is important because it determines which states win in a conflict. // We mostly follow Tailwind's defaults, adding our additional states following the categories they define. // https://github.com/tailwindlabs/tailwindcss/blob/304c2bad6cb5fcb62754a4580b1c8f4c16b946ea/src/corePlugins.js#L83 const attributes = { boolean: [ // Conditions "allows-removing", "allows-sorting", "allows-dragging", "has-submenu", // States "open", "expanded", "entering", "exiting", "indeterminate", ["placeholder-shown", "placeholder"], "current", "required", "unavailable", "invalid", ["read-only", "readonly"], "outside-month", "outside-visible-range", "pending", // Content "empty", // Interactive states "focus-within", ["hover", "hovered"], ["focus", "focused"], "focus-visible", "pressed", "selected", "selection-start", "selection-end", "dragging", "drop-target", "resizing", "disabled", ] as const, enum: { placement: ["left", "right", "top", "bottom"], type: ["literal", "year", "month", "day"], layout: ["grid", "stack"], orientation: ["horizontal", "vertical"], "selection-mode": ["single", "multiple"], "resizable-direction": ["right", "left", "both"], "sort-direction": ["ascending", "descending"], }, }; const shortNames: Record = { "selection-mode": "selection", "resizable-direction": "resizable", "sort-direction": "sort", }; // Variants we use that are already defined by Tailwind: // https://github.com/tailwindlabs/tailwindcss/blob/a2fa6932767ab328515f743d6188c2164ad2a5de/src/corePlugins.js#L84 const nativeVariants = [ "indeterminate", "required", "invalid", "empty", "focus-visible", "focus-within", "disabled", ]; const nativeVariantSelectors = new Map([ ...nativeVariants.map(variant => [variant, `:${variant}`] as const), ["hovered", ":hover"], ["focused", ":focus"], ["readonly", ":read-only"], ["open", "[open]"], ["expanded", "[expanded]"], ]); // Variants where both native and RAC attributes should apply. We don't override these. const nativeMergeSelectors = new Map([["placeholder", ":placeholder-shown"]]); type SelectorFn = (wrap: (s: string) => string) => string; type SelectorValue = string | SelectorFn; type Selector = string | [string, SelectorValue]; // 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. function getSelector( prefix: string, attributeName: string, attributeValue: string | null ): Selector { const baseSelector = attributeValue ? `[data-${attributeName}="${attributeValue}"]` : `[data-${attributeName}]`; const nativeSelector = nativeVariantSelectors.get(attributeName); if (prefix === "" && nativeSelector) { const wrappedNativeSelector = `&:not([data-rac])${nativeSelector}`; let nativeSelectorGenerator: SelectorValue = wrappedNativeSelector; if (nativeSelector === ":hover") { nativeSelectorGenerator = wrap => `@media (hover: hover) { ${wrap(wrappedNativeSelector)} }`; } return [`&[data-rac]${baseSelector}`, nativeSelectorGenerator]; } else if (prefix === "" && nativeMergeSelectors.has(attributeName)) { return [`&${baseSelector}`, `&${nativeMergeSelectors.get(attributeName)}`]; } else { return `&${baseSelector}`; } } const mapSelector = (selector: Selector, fn: (v: SelectorValue) => string) => Array.isArray(selector) ? selector.map(fn) : fn(selector); const wrapSelector = (selector: SelectorValue, wrap: (text: string) => string) => typeof selector === "function" ? selector(wrap) : wrap(selector); const addVariants = ( variantName: string, selectors: Selector, addVariant: PluginAPI["addVariant"] ) => { addVariant( variantName, mapSelector(selectors, selector => wrapSelector(selector, s => s)) ); }; export default plugin.withOptions<{ prefix: string }>(options => ({ addVariant }) => { const prefix = options?.prefix ? `${options.prefix}-` : ""; // Enum attributes go first because currently they are all non-interactive states. for (const [attributeName, value] of Object.entries(attributes.enum) as [ keyof typeof attributes.enum, string[], ][]) { for (const [i, attributeValue] of value.entries()) { const name = shortNames[attributeName] || attributeName; const variantName = `${prefix}${name}-${attributeValue}`; const selectors = getSelector(prefix, attributeName, attributeValue); addVariants(variantName, selectors, addVariant, i); } } for (const [i, attribute] of attributes.boolean.entries()) { let variantName = Array.isArray(attribute) ? attribute[0] : attribute; variantName = `${prefix}${variantName}`; const attributeName = Array.isArray(attribute) ? attribute[1] : attribute; const selectors = getSelector(prefix, attributeName, null); addVariants(variantName, selectors, addVariant, i); } });