diff --git a/.distignore b/.distignore index bed1efaf..a177c8f7 100644 --- a/.distignore +++ b/.distignore @@ -32,6 +32,7 @@ phpunit.xml.dist # Build / tooling config tailwind.config.js +tailwind.scoped-forms.js postcss.config.js webpack.config.js package.json diff --git a/src/assets/css/index.css b/src/assets/css/index.css index dae9e653..704d02da 100644 --- a/src/assets/css/index.css +++ b/src/assets/css/index.css @@ -46,7 +46,11 @@ logs: false; } -@plugin "@tailwindcss/forms"; +/* The forms reset, scoped to the weDocs container (see tailwind.scoped-forms.js), + so it stops restyling the checkboxes and fields of the rest of wp-admin. */ +@plugin "../../../tailwind.scoped-forms.js" { + selector: .wedocs-document; +} /* * Force a light color-scheme for all weDocs UI (modals, blocks, settings all diff --git a/src/blocks/Breadcrumb/style.css b/src/blocks/Breadcrumb/style.css index 0fda36fd..53ebbf4c 100644 --- a/src/blocks/Breadcrumb/style.css +++ b/src/blocks/Breadcrumb/style.css @@ -37,7 +37,12 @@ daisyUI component or theme colour, and registering it emitted the whole component set into every one of these four stylesheets. */ -@plugin "@tailwindcss/forms"; +/* The forms reset, scoped to the weDocs container (see tailwind.scoped-forms.js). + The bare `@tailwindcss/forms` plugin restyled every checkbox and radio on + the page, not just the ones inside weDocs markup. */ +@plugin "../../../tailwind.scoped-forms.js" { + selector: .wedocs-document; +} /* Breadcrumb Block Specific Styles */ .wp-block-wedocs-breadcrumbs { diff --git a/src/blocks/DocNavigation/style.css b/src/blocks/DocNavigation/style.css index 3eb6ba03..b77a3fe2 100644 --- a/src/blocks/DocNavigation/style.css +++ b/src/blocks/DocNavigation/style.css @@ -37,7 +37,12 @@ daisyUI component or theme colour, and registering it emitted the whole component set into every one of these four stylesheets. */ -@plugin "@tailwindcss/forms"; +/* The forms reset, scoped to the weDocs container (see tailwind.scoped-forms.js). + The bare `@tailwindcss/forms` plugin restyled every checkbox and radio on + the page, not just the ones inside weDocs markup. */ +@plugin "../../../tailwind.scoped-forms.js" { + selector: .wedocs-document; +} .components-grid.components-tools-panel.block-editor-panel-color-gradient-settings { padding: 0; diff --git a/src/blocks/QuickSearch/style.css b/src/blocks/QuickSearch/style.css index 3f99316b..83b38c77 100644 --- a/src/blocks/QuickSearch/style.css +++ b/src/blocks/QuickSearch/style.css @@ -37,7 +37,12 @@ daisyUI component or theme colour, and registering it emitted the whole component set into every one of these four stylesheets. */ -@plugin "@tailwindcss/forms"; +/* The forms reset, scoped to the weDocs container (see tailwind.scoped-forms.js). + The bare `@tailwindcss/forms` plugin restyled every checkbox and radio on + the page, not just the ones inside weDocs markup. */ +@plugin "../../../tailwind.scoped-forms.js" { + selector: .wedocs-document; +} .components-grid.components-tools-panel.block-editor-panel-color-gradient-settings { padding: 0; diff --git a/src/blocks/Sidebar/style.css b/src/blocks/Sidebar/style.css index 1141a732..4585ca24 100644 --- a/src/blocks/Sidebar/style.css +++ b/src/blocks/Sidebar/style.css @@ -37,7 +37,12 @@ daisyUI component or theme colour, and registering it emitted the whole component set into every one of these four stylesheets. */ -@plugin "@tailwindcss/forms"; +/* The forms reset, scoped to the weDocs container (see tailwind.scoped-forms.js). + The bare `@tailwindcss/forms` plugin restyled every checkbox and radio on + the page, not just the ones inside weDocs markup. */ +@plugin "../../../tailwind.scoped-forms.js" { + selector: .wedocs-document; +} /* Section styling for frontend */ .wedocs-section { diff --git a/tailwind.scoped-forms.js b/tailwind.scoped-forms.js new file mode 100644 index 00000000..a70bbf1f --- /dev/null +++ b/tailwind.scoped-forms.js @@ -0,0 +1,101 @@ +/** + * `@tailwindcss/forms`, scoped to a container. + * + * The forms plugin writes its reset as bare element rules + * (`input:where([type='checkbox']) { appearance: none; ... }`). Neither the + * `important` scope nor `tailwindcss-scoped-preflight` touches plugin base + * rules, so wherever a weDocs stylesheet loaded, every checkbox, radio, select + * and text field on the page was restyled, the theme's and other plugins' + * included. + * + * This runs the forms plugin unchanged and rewrites each selector it hands to + * `addBase` so it only matches inside the container, the same + * `:where(, *)` form the scoped preflight uses. `:where()` + * adds no specificity, so the rules keep the weight they had before. + * + * Usage, in a Tailwind 4 stylesheet: + * + * @plugin "../../tailwind.scoped-forms.js" { + * selector: .wedocs-document; + * } + * + * `selector` takes a comma-separated list when the markup lives in more than + * one container. + */ +const plugin = require( 'tailwindcss/plugin' ); +const forms = require( '@tailwindcss/forms' ); + +// Split on commas that are not inside parentheses or brackets. +const splitSelectorList = ( list ) => { + const parts = []; + let depth = 0; + let current = ''; + + for ( const char of list ) { + if ( '(' === char || '[' === char ) { + depth++; + } else if ( ')' === char || ']' === char ) { + depth--; + } + + if ( ',' === char && 0 === depth ) { + parts.push( current.trim() ); + current = ''; + } else { + current += char; + } + } + + parts.push( current.trim() ); + + return parts.filter( Boolean ); +}; + +// Append the scope to one selector, ahead of any pseudo-element, which has to +// stay last. +const scopeSelector = ( selector, scope ) => { + const pseudo = selector.indexOf( '::' ); + + if ( -1 === pseudo ) { + return `${ selector }${ scope }`; + } + + return `${ selector.slice( 0, pseudo ) }${ scope }${ selector.slice( pseudo ) }`; +}; + +const scopeRules = ( rules, scope ) => { + const scoped = {}; + + for ( const [ key, value ] of Object.entries( rules ) ) { + if ( key.startsWith( '@' ) ) { + // @media / @supports: scope the rules inside, keep the at-rule. + scoped[ key ] = scopeRules( value, scope ); + } else if ( value && 'object' === typeof value && ! Array.isArray( value ) ) { + const selector = splitSelectorList( key ) + .map( ( part ) => scopeSelector( part, scope ) ) + .join( ',' ); + + scoped[ selector ] = value; + } else { + // A declaration at this level; nothing to scope. + scoped[ key ] = value; + } + } + + return scoped; +}; + +module.exports = plugin.withOptions( ( options = {} ) => ( api ) => { + // One container or a comma-separated list of them. + const containers = splitSelectorList( String( options.selector || '.wedocs-document' ) ); + const scope = `:where(${ containers.map( ( c ) => `${ c }, ${ c } *` ).join( ', ' ) })`; + + forms( { strategy: options.strategy } ).handler( { + ...api, + addBase: ( rules ) => { + const list = Array.isArray( rules ) ? rules : [ rules ]; + + list.forEach( ( rule ) => api.addBase( scopeRules( rule, scope ) ) ); + }, + } ); +} );