@@ -16,6 +16,15 @@ const safeSelector = (selector: string) => {
1616 const placeholders : string [ ] = [ ] ;
1717 let index = 0 ;
1818
19+ // Replaces [part=~"..."] attribute selectors with placeholders.
20+ // As we do not want to add the scoped selector to these selectors
21+ selector = selector . replace ( / ( \[ \s * p a r t ~ = \s * ( " [ ^ " ] * " | ' [ ^ ' ] * ' ) \s * \] ) / g, ( _ , keep ) => {
22+ const replaceBy = `__part-${ index } __` ;
23+ placeholders . push ( keep ) ;
24+ index ++ ;
25+ return replaceBy ;
26+ } ) ;
27+
1928 // Replaces attribute selectors with placeholders.
2029 // The WS in [attr="va lue"] would otherwise be interpreted as a selector separator.
2130 selector = selector . replace ( / ( \[ [ ^ \] ] * \] ) / g, ( _ , keep ) => {
@@ -42,6 +51,7 @@ const safeSelector = (selector: string) => {
4251} ;
4352
4453const restoreSafeSelector = ( placeholders : string [ ] , content : string ) => {
54+ content = content . replace ( / _ _ p a r t - ( \d + ) _ _ / g, ( _ , index ) => placeholders [ + index ] ) ;
4555 return content . replace ( / _ _ p h - ( \d + ) _ _ / g, ( _ , index ) => placeholders [ + index ] ) ;
4656} ;
4757
@@ -71,6 +81,7 @@ const _cssColonSlottedRe = new RegExp('(' + _polyfillSlotted + _parenSuffix, 'gi
7181const _polyfillHostNoCombinator = _polyfillHost + '-no-combinator' ;
7282const _polyfillHostNoCombinatorRe = / - s h a d o w c s s h o s t - n o - c o m b i n a t o r ( [ ^ \s ] * ) / ;
7383const _shadowDOMSelectorsRe = [ / : : s h a d o w / g, / : : c o n t e n t / g] ;
84+ const _safePartRe = / _ _ p a r t - ( \d + ) _ _ / g;
7485
7586const _selectorReSuffix = '([>\\s~+[.,{:][\\s\\S]*)?$' ;
7687const _polyfillHostRe = / - s h a d o w c s s h o s t / gim;
@@ -408,7 +419,7 @@ const applyStrictSelectorScope = (selector: string, scopeSelector: string, hostS
408419 let scopedSelector = '' ;
409420 let startIndex = 0 ;
410421 let res : RegExpExecArray | null ;
411- const sep = / ( | > | \+ | ~ (? ! = ) ) \s * / g;
422+ const sep = / ( | > | \+ | ~ (? ! = ) ) (? = (?: [ ^ ( ) ] * \( [ ^ ( ) ] * \) ) * [ ^ ( ) ] * $ ) \s * / g;
412423
413424 // If a selector appears before :host it should not be shimmed as it
414425 // matches on ancestor elements and not on elements in the host's shadow
@@ -435,7 +446,7 @@ const applyStrictSelectorScope = (selector: string, scopeSelector: string, hostS
435446 }
436447
437448 const part = selector . substring ( startIndex ) ;
438- shouldScope = shouldScope || part . indexOf ( _polyfillHostNoCombinator ) > - 1 ;
449+ shouldScope = ! part . match ( _safePartRe ) && ( shouldScope || part . indexOf ( _polyfillHostNoCombinator ) > - 1 ) ;
439450 scopedSelector += shouldScope ? _scopeSelectorPart ( part ) : part ;
440451
441452 // replace the placeholders with their original values
@@ -533,6 +544,58 @@ const replaceShadowCssHost = (cssText: string, hostScopeId: string) => {
533544 return cssText . replace ( / - s h a d o w c s s h o s t - n o - c o m b i n a t o r / g, `.${ hostScopeId } ` ) ;
534545} ;
535546
547+ /**
548+ * Expands selectors with ::part(...) to also include [part~="..."] selectors.
549+ * For example:
550+ * ```css
551+ * selectors-like-this::part(demo) { ... }
552+ * .something .selectors::part(demo demo2):hover { ... }
553+ * ```
554+ * Becomes:
555+ * ```
556+ * selectors-like-this::part(demo), selectors-like-this [part~="demo"] { ... }
557+ * .something .selectors::part(demo demo2):hover, .something .selectors [part~="demo"][part~="demo2"]:hover { ... }
558+ * ```
559+ *
560+ * @param cssText The CSS text to process
561+ * @returns The CSS text with expanded ::part(...) selectors
562+ */
563+ export const expandPartSelectors = ( cssText : string ) => {
564+ // Regex matches: (selector before)::part(part names)(pseudo after)
565+ const partSelectorRe = / ( [ ^ \s , { ] [ ^ , { ] * ?) : : p a r t \( \s * ( [ ^ ) ] + ?) \s * \) ( (?: [: .] [ ^ , { ] * ) * ) / g;
566+ return processRules ( cssText , ( rule : CssRule ) => {
567+ if ( rule . selector [ 0 ] === '@' ) {
568+ return rule ;
569+ }
570+ // Split by comma, process each selector
571+ const selectors = rule . selector . split ( ',' ) . map ( ( sel ) => {
572+ const out = [ sel . trim ( ) ] ;
573+ let m ;
574+ // For each ::part(...) in the selector, add the expanded version
575+ while ( ( m = partSelectorRe . exec ( sel ) ) !== null ) {
576+ const before = m [ 1 ] . trimEnd ( ) ;
577+ const partNames = m [ 2 ] . trim ( ) . split ( / \s + / ) ;
578+ const after = m [ 3 ] || '' ;
579+ const partAttr = partNames
580+ . flatMap ( ( p : string ) : string [ ] => {
581+ if ( ! rule . selector . includes ( `[part~="${ p } "]` ) ) {
582+ return [ `[part~="${ p } "]` ] ;
583+ }
584+ return [ ] ;
585+ } )
586+ . join ( '' ) ;
587+ const expanded = `${ before } ${ partAttr } ${ after } ` ;
588+ if ( ! ! partAttr && expanded !== sel . trim ( ) ) {
589+ out . push ( expanded ) ;
590+ }
591+ }
592+ return out . join ( ', ' ) ;
593+ } ) ;
594+ rule . selector = selectors . join ( ', ' ) ;
595+ return rule ;
596+ } ) ;
597+ } ;
598+
536599export const scopeCss = ( cssText : string , scopeId : string , commentOriginalSelector : boolean ) => {
537600 const hostScopeId = scopeId + '-h' ;
538601 const slotScopeId = scopeId + '-s' ;
@@ -585,5 +648,8 @@ export const scopeCss = (cssText: string, scopeId: string, commentOriginalSelect
585648 cssText = cssText . replace ( regex , slottedSelector . updatedSelector ) ;
586649 } ) ;
587650
651+ // Expand ::part(...) selectors
652+ cssText = expandPartSelectors ( cssText ) ;
653+
588654 return cssText ;
589655} ;
0 commit comments