diff --git a/src/compile.ts b/src/compile.ts index a804aa63..93270347 100644 --- a/src/compile.ts +++ b/src/compile.ts @@ -65,11 +65,13 @@ function absolutize( * @param token Selector token(s) to compile. * @param options Options that control this operation. * @param compilationContext Compilation context for relative selector handling. + * @param shouldAbsolutize Override relative handling for this call only. */ export function compileToken( token: InternalSelector[][], options: InternalOptions, compilationContext?: Node[] | Node, + shouldAbsolutize: boolean | undefined = options.relativeSelector, ): CompiledQuery { for (const rules of token) { sortRules(rules); @@ -86,7 +88,7 @@ export function compileToken( context && (Array.isArray(context) ? context : [context]); // Check if the selector is relative - if (options.relativeSelector !== false) { + if (shouldAbsolutize !== false) { absolutize(token, options, finalContext); } else if (token.some((t) => t.length > 0 && isTraversal(t[0]))) { throw new Error( diff --git a/src/pseudo-selectors/subselects.ts b/src/pseudo-selectors/subselects.ts index e93f4804..9ffca39e 100644 --- a/src/pseudo-selectors/subselects.ts +++ b/src/pseudo-selectors/subselects.ts @@ -79,7 +79,6 @@ export const subselects: Record = { const { adapter } = options; const copiedOptions = copyOptions(options); - copiedOptions.relativeSelector = true; const context = subselect.some((s) => s.some(isTraversal)) ? // Used as a placeholder. Will be replaced with the actual element. @@ -87,7 +86,11 @@ export const subselects: Record = { : undefined; const shouldSkipCache = isContextDependent(subselect); - const compiled = compileToken(subselect, copiedOptions, context); + /* + * Only the :has argument is relative. Nested selectors retain the + * caller's relativeSelector option. + */ + const compiled = compileToken(subselect, copiedOptions, context, true); if (compiled === boolbase.falseFunc) { return boolbase.falseFunc; diff --git a/src/types.ts b/src/types.ts index 0a364f3b..98f5ca95 100644 --- a/src/types.ts +++ b/src/types.ts @@ -204,4 +204,5 @@ export type CompileToken = ( token: InternalSelector[][], options: InternalOptions, context?: Node[] | Node, + shouldAbsolutize?: boolean, ) => CompiledQuery; diff --git a/test/pseudo-classes.ts b/test/pseudo-classes.ts index 1feeb8ce..9a781aec 100644 --- a/test/pseudo-classes.ts +++ b/test/pseudo-classes.ts @@ -181,6 +181,116 @@ describe(":empty", () => { }); describe(":has", () => { + const preceding = '
'; + const nestedSelectors = [ + { + selector: ".p:has(+ .a:not(.b))", + html: `${preceding}
`, + expected: false, + relativeExpected: true, + }, + { + selector: ".p:has(+ :is(.a))", + html: `${preceding}
`, + expected: true, + relativeExpected: false, + }, + { + selector: ".p:has(~ :where(.a, .b))", + html: `${preceding}
`, + expected: true, + relativeExpected: false, + }, + { + selector: ".p:has(+ input:checked)", + html: `${preceding}`, + expected: true, + relativeExpected: false, + }, + { + selector: ".p:has(+ :nth-child(2 of .a))", + html: `${preceding}`, + expected: true, + relativeExpected: false, + }, + { + selector: ".p:has(> .a:is(.x .a))", + html: '
', + expected: true, + relativeExpected: false, + }, + { + selector: ".p:has(+ :matches(.a))", + html: `${preceding}
`, + expected: true, + relativeExpected: false, + }, + { + selector: ".p:has(+ :is(:where(.a)))", + html: `${preceding}
`, + expected: true, + relativeExpected: false, + }, + { + selector: ".p:has(> .a:not(.b))", + html: '
', + expected: true, + relativeExpected: true, + }, + { + selector: ".p:has(> .a:is(.b))", + html: '
', + expected: false, + relativeExpected: false, + }, + ]; + + it.each(nestedSelectors)( + "should honor relativeSelector: false in $selector", + ({ selector, html, expected }) => { + const document = parseDocument(html); + const [target] = CSSselect.selectAll( + "#target", + document, + ); + + for (const isCacheResults of [true, false]) { + const options = { + relativeSelector: false, + cacheResults: isCacheResults, + }; + expect(CSSselect.compile(selector, options)(target)).toBe( + expected, + ); + expect(CSSselect.is(target, selector, options)).toBe(expected); + expect( + CSSselect.selectAll( + selector, + document, + options, + ).includes(target), + ).toBe(expected); + } + }, + ); + + it.each(nestedSelectors)( + "should retain enabled and default relative behavior in $selector", + ({ selector, html, relativeExpected }) => { + const document = parseDocument(html); + const [target] = CSSselect.selectAll( + "#target", + document, + ); + + for (const options of [undefined, { relativeSelector: true }]) { + expect(CSSselect.compile(selector, options)(target)).toBe( + relativeExpected, + ); + } + }, + ); + it("should cache :has if applicable", () => { const dom = parseDocument(`