Skip to content

Commit 01d3fa4

Browse files
authored
feat(preset-mini): nested tagged variants (#4789)
1 parent 4e7e7fd commit 01d3fa4

6 files changed

Lines changed: 370 additions & 41 deletions

File tree

packages-presets/preset-mini/src/_variants/aria.ts

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { Variant, VariantContext, VariantObject } from '@unocss/core'
2+
import type { PresetMiniOptions } from '..'
23
import type { Theme } from '../theme'
4+
import { escapeRegExp, escapeSelector } from '@unocss/core'
35
import { h, variantGetParameter } from '../utils'
46

57
export const variantAria: VariantObject = {
@@ -20,28 +22,91 @@ export const variantAria: VariantObject = {
2022
multiPass: true,
2123
}
2224

23-
function taggedAria(tagName: string): Variant {
25+
function taggedAria(tagName: string, combinator: string, options: PresetMiniOptions = {}): Variant {
2426
return {
2527
name: `${tagName}-aria`,
2628
match(matcher, ctx: VariantContext<Theme>) {
2729
const variant = variantGetParameter(`${tagName}-aria-`, matcher, ctx.generator.config.separators)
2830
if (variant) {
2931
const [match, rest, label] = variant
3032
const ariaAttribute = h.bracket(match) ?? ctx.theme.aria?.[match] ?? ''
33+
if (ariaAttribute) {
34+
const attributify = !!options?.attributifyPseudo
35+
let firstPrefix = options?.prefix ?? ''
36+
firstPrefix = (Array.isArray(firstPrefix) ? firstPrefix : [firstPrefix]).filter(Boolean)[0] ?? ''
37+
38+
const parent = `${attributify ? `[${firstPrefix}${tagName}=""]` : `.${firstPrefix}${tagName}`}`
39+
const escapedLabel = escapeSelector(label ? `/${label}` : '')
40+
41+
return {
42+
matcher: rest,
43+
handle: (input, next) => {
44+
const regexp = new RegExp(`${escapeRegExp(parent)}${escapeRegExp(escapedLabel)}(?:\\[.+?\\])+`)
45+
const match = input.prefix.match(regexp)
46+
47+
let nextPrefix
48+
if (match) {
49+
const insertIndex = (match.index ?? 0) + parent.length + escapedLabel.length
50+
nextPrefix = [
51+
input.prefix.slice(0, insertIndex),
52+
`[aria-${ariaAttribute}]`,
53+
input.prefix.slice(insertIndex),
54+
].join('')
55+
}
56+
else {
57+
const prefixGroupIndex = Math.max(input.prefix.indexOf(parent), 0)
58+
nextPrefix = [
59+
input.prefix.slice(0, prefixGroupIndex),
60+
parent,
61+
escapedLabel,
62+
`[aria-${ariaAttribute}]`,
63+
combinator,
64+
input.prefix.slice(prefixGroupIndex),
65+
].join('')
66+
}
67+
68+
return next({
69+
...input,
70+
prefix: nextPrefix,
71+
})
72+
},
73+
}
74+
}
75+
}
76+
},
77+
multiPass: true,
78+
}
79+
}
80+
81+
function taggedHasAria(): Variant {
82+
return {
83+
name: 'has-aria',
84+
match(matcher, ctx: VariantContext<Theme>) {
85+
const variant = variantGetParameter('has-aria-', matcher, ctx.generator.config.separators)
86+
if (variant) {
87+
const [match, rest] = variant
88+
const ariaAttribute = h.bracket(match) ?? ctx.theme.aria?.[match] ?? ''
3189
if (ariaAttribute) {
3290
return {
33-
matcher: `${tagName}-[[aria-${ariaAttribute}]]${label ? `/${label}` : ''}:${rest}`,
91+
matcher: rest,
92+
handle: (input, next) => next({
93+
...input,
94+
pseudo: `${input.pseudo}:has([aria-${ariaAttribute}])`,
95+
}),
3496
}
3597
}
3698
}
3799
},
100+
multiPass: true,
38101
}
39102
}
40103

41-
export const variantTaggedAriaAttributes: Variant[] = [
42-
taggedAria('group'),
43-
taggedAria('peer'),
44-
taggedAria('parent'),
45-
taggedAria('previous'),
46-
taggedAria('has'),
47-
]
104+
export function variantTaggedAriaAttributes(options: PresetMiniOptions = {}): Variant[] {
105+
return [
106+
taggedAria('group', ' ', options),
107+
taggedAria('peer', '~', options),
108+
taggedAria('parent', '>', options),
109+
taggedAria('previous', '+', options),
110+
taggedHasAria(),
111+
]
112+
}

packages-presets/preset-mini/src/_variants/data.ts

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { Variant, VariantContext, VariantObject } from '@unocss/core'
2+
import type { PresetMiniOptions } from '..'
23
import type { Theme } from '../theme'
4+
import { escapeRegExp, escapeSelector } from '@unocss/core'
35
import { h, variantGetParameter } from '../utils'
46

57
export const variantDataAttribute: VariantObject = {
@@ -20,28 +22,91 @@ export const variantDataAttribute: VariantObject = {
2022
multiPass: true,
2123
}
2224

23-
function taggedData(tagName: string): Variant {
25+
function taggedData(tagName: string, combinator: string, options: PresetMiniOptions = {}): Variant {
2426
return {
2527
name: `${tagName}-data`,
2628
match(matcher, ctx: VariantContext<Theme>) {
2729
const variant = variantGetParameter(`${tagName}-data-`, matcher, ctx.generator.config.separators)
2830
if (variant) {
2931
const [match, rest, label] = variant
3032
const dataAttribute = h.bracket(match) ?? ctx.theme.data?.[match] ?? ''
33+
if (dataAttribute) {
34+
const attributify = !!options?.attributifyPseudo
35+
let firstPrefix = options?.prefix ?? ''
36+
firstPrefix = (Array.isArray(firstPrefix) ? firstPrefix : [firstPrefix]).filter(Boolean)[0] ?? ''
37+
38+
const parent = `${attributify ? `[${firstPrefix}${tagName}=""]` : `.${firstPrefix}${tagName}`}`
39+
const escapedLabel = escapeSelector(label ? `/${label}` : '')
40+
41+
return {
42+
matcher: rest,
43+
handle: (input, next) => {
44+
const regexp = new RegExp(`${escapeRegExp(parent)}${escapeRegExp(escapedLabel)}(?:\\[.+?\\])+`)
45+
const match = input.prefix.match(regexp)
46+
47+
let nextPrefix
48+
if (match) {
49+
const insertIndex = (match.index ?? 0) + parent.length + escapedLabel.length
50+
nextPrefix = [
51+
input.prefix.slice(0, insertIndex),
52+
`[data-${dataAttribute}]`,
53+
input.prefix.slice(insertIndex),
54+
].join('')
55+
}
56+
else {
57+
const prefixGroupIndex = Math.max(input.prefix.indexOf(parent), 0)
58+
nextPrefix = [
59+
input.prefix.slice(0, prefixGroupIndex),
60+
parent,
61+
escapedLabel,
62+
`[data-${dataAttribute}]`,
63+
combinator,
64+
input.prefix.slice(prefixGroupIndex),
65+
].join('')
66+
}
67+
68+
return next({
69+
...input,
70+
prefix: nextPrefix,
71+
})
72+
},
73+
}
74+
}
75+
}
76+
},
77+
multiPass: true,
78+
}
79+
}
80+
81+
function taggedHasData(): Variant {
82+
return {
83+
name: 'has-data',
84+
match(matcher, ctx: VariantContext<Theme>) {
85+
const variant = variantGetParameter('has-data-', matcher, ctx.generator.config.separators)
86+
if (variant) {
87+
const [match, rest] = variant
88+
const dataAttribute = h.bracket(match) ?? ctx.theme.data?.[match] ?? ''
3189
if (dataAttribute) {
3290
return {
33-
matcher: `${tagName}-[[data-${dataAttribute}]]${label ? `/${label}` : ''}:${rest}`,
91+
matcher: rest,
92+
handle: (input, next) => next({
93+
...input,
94+
pseudo: `${input.pseudo}:has([data-${dataAttribute}])`,
95+
}),
3496
}
3597
}
3698
}
3799
},
100+
multiPass: true,
38101
}
39102
}
40103

41-
export const variantTaggedDataAttributes: Variant[] = [
42-
taggedData('group'),
43-
taggedData('peer'),
44-
taggedData('parent'),
45-
taggedData('previous'),
46-
taggedData('has'),
47-
]
104+
export function variantTaggedDataAttributes(options: PresetMiniOptions = {}): Variant[] {
105+
return [
106+
taggedData('group', ' ', options),
107+
taggedData('peer', '~', options),
108+
taggedData('parent', '>', options),
109+
taggedData('previous', '+', options),
110+
taggedHasData(),
111+
]
112+
}

packages-presets/preset-mini/src/_variants/default.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ export function variants(options: PresetMiniOptions): Variant<Theme>[] {
4646

4747
variantContainerQuery,
4848
variantVariables,
49-
...variantTaggedDataAttributes,
50-
...variantTaggedAriaAttributes,
49+
...variantTaggedDataAttributes(options),
50+
...variantTaggedAriaAttributes(options),
5151

5252
variantTheme,
5353
]

test/assets/output/preset-mini-targets.css

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -479,13 +479,13 @@ unocss .scope-\[unocss\]\:block{display:block;}
479479
.break-normal{overflow-wrap:normal;word-break:normal;}
480480
.break-words{overflow-wrap:break-word;}
481481
.break-keep{word-break:keep-all;}
482-
.peer[data-state=closed]~.peer-data-\[state\=closed\]\:border-3,
483-
.border-width-3{border-width:3px;}
484482
.b-2,
485483
.border-size-2{border-width:2px;}
486484
.border{border-width:1px;}
487485
.border-4,
488486
.has-data-\[state\=closed\]\:border-4:has([data-state=closed]){border-width:4px;}
487+
.border-width-3,
488+
.peer[data-state=closed]~.peer-data-\[state\=closed\]\:border-3{border-width:3px;}
489489
.border-x{border-left-width:1px;border-right-width:1px;}
490490
.border-b{border-bottom-width:1px;}
491491
.border-e-4{border-inline-end-width:4px;}
@@ -869,19 +869,13 @@ unocss .scope-\[unocss\]\:block{display:block;}
869869
.group:hover .group-\[\:hover\]\:font-11{font-weight:11;}
870870
.group.not-parent .group-\[\.not-parent\]\:font-14{font-weight:14;}
871871
.group[data-attr] .group-\[\[data-attr\]\]\:font-12{font-weight:12;}
872-
.group[data-state=open] .group-data-\[state\=open\]\:font-bold{font-weight:700;}
873872
.group\/label:hover .group-\[\:hover\]\/label\:font-16{font-weight:16;}
874873
.group\/label.not-parent .group-\[\.not-parent\]\/label\:font-19{font-weight:19;}
875874
.group\/label[data-attr] .group-\[\[data-attr\]\]\/label\:font-17,
876875
.group\/named[data-x=y] .group-data-\[x\=y\]\/named\:font-17,
877876
.parent\/named[data-x=y]>.parent-data-\[x\=y\]\/named\:font-17,
878877
.peer\/named[data-x=y]~.peer-data-\[x\=y\]\/named\:font-17,
879878
.previous\/named[data-x=y]+.previous-data-\[x\=y\]\/named\:font-17{font-weight:17;}
880-
.group\/named[aria-level="1"] .group-aria-\[level\=\"1\"\]\/named\:font-21,
881-
.parent\/named[aria-level="3"]>.parent-aria-\[level\=\"3\"\]\/named\:font-21,
882-
.peer\/named[aria-level="2"]~.peer-aria-\[level\=\"2\"\]\/named\:font-21{font-weight:21;}
883-
.group\/named[data-state=open] .group-data-\[state\=open\]\/named\:font-medium{font-weight:500;}
884-
.previous\/named[aria-level="4"]+.previous-aria-\[level\=\"4\"\]\/named\:hover\:font-21:hover{font-weight:21;}
885879
.font-050,
886880
.font-50,
887881
.fw-050,
@@ -891,9 +885,15 @@ unocss .scope-\[unocss\]\:block{display:block;}
891885
.fw-900{font-weight:900;}
892886
.font-thin{font-weight:100;}
893887
.fw-inherit{font-weight:inherit;}
888+
.group[data-state=open] .group-data-\[state\=open\]\:font-bold{font-weight:700;}
889+
.group\/named[aria-level="1"] .group-aria-\[level\=\"1\"\]\/named\:font-21,
890+
.parent\/named[aria-level="3"]>.parent-aria-\[level\=\"3\"\]\/named\:font-21,
891+
.peer\/named[aria-level="2"]~.peer-aria-\[level\=\"2\"\]\/named\:font-21{font-weight:21;}
892+
.group\/named[data-state=open] .group-data-\[state\=open\]\/named\:font-medium{font-weight:500;}
894893
.has-aria-\[hidden\=false\]\:font-20:has([aria-hidden=false]){font-weight:20;}
895894
.group:hover .group-hover\:font-10{font-weight:10;}
896895
.group\/label:hover .group-hover\/label\:font-15{font-weight:15;}
896+
.previous\/named[aria-level="4"]+.previous-aria-\[level\=\"4\"\]\/named\:hover\:font-21:hover{font-weight:21;}
897897
.font-leading-2,
898898
.leading-2{line-height:0.5rem;}
899899
.leading-\$variable,

0 commit comments

Comments
 (0)