@godxjp/ui 27.11.0 → 27.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/data-display/card.d.ts +46 -4
- package/dist/components/data-display/card.js +29 -6
- package/dist/contracts/measurement.json +1 -1
- package/dist/props/registry.d.ts +1 -1
- package/dist/props/registry.js +1 -1
- package/dist/styles/card-layout.css +4 -0
- package/dist/styles/focus-ring.css +5 -0
- package/docs/data-display/card/index.tsx +41 -0
- package/package.json +2 -2
- package/scripts/ui-audit.mjs +134 -8
|
@@ -40,10 +40,31 @@ export type CardProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
|
40
40
|
*
|
|
41
41
|
* This is a PRESENTATION flag, not an interaction: it announces nothing and binds no handler.
|
|
42
42
|
* A card that looks clickable has to BE clickable for everyone, so pair it with a real control
|
|
43
|
-
* — a `Link`/`Button` in the header or footer, or the whole card rendered as one
|
|
44
|
-
* bare `onClick` on this div, which a keyboard or
|
|
43
|
+
* — a `Link`/`Button` in the header or footer, or the whole card rendered as one via
|
|
44
|
+
* {@link CardProps.asChild} — never with a bare `onClick` on this div, which a keyboard or
|
|
45
|
+
* screen-reader user cannot reach.
|
|
45
46
|
*/
|
|
46
47
|
hoverable?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Borrow the child's element for the card box instead of rendering a `div` — the package's
|
|
50
|
+
* standard Slot passthrough (`Button`, `AspectRatio`, `ListRow`). The card's chrome and every
|
|
51
|
+
* `data-*` flag move onto that element, so a card rendered as an `<a>` or a `<button>` measures
|
|
52
|
+
* byte for byte like the `div` it replaces and stays a SINGLE tab stop.
|
|
53
|
+
*
|
|
54
|
+
* This is the other half of `hoverable`: `<Card asChild hoverable><a href="…">` is "the whole
|
|
55
|
+
* card rendered as one control", reachable by keyboard and announced as one link.
|
|
56
|
+
*
|
|
57
|
+
* NESTING CAVEAT — the card is now ONE control, so it may not contain another. An
|
|
58
|
+
* `<a>`/`<button>` (a `Link`, a `Button`, a menu trigger) inside a card-as-link or
|
|
59
|
+
* card-as-button is invalid HTML: the inner control is unreachable or mis-announced, and
|
|
60
|
+
* `tabList` — a strip of `button` triggers — is for the same reason not drawn under `asChild`.
|
|
61
|
+
* A card that needs interactive children is not one control: drop `asChild` and put the
|
|
62
|
+
* `Link`/`Button` inside it instead, as `hoverable` also allows.
|
|
63
|
+
*
|
|
64
|
+
* Exactly one child, as everywhere else in this package: two children have no element to
|
|
65
|
+
* borrow, so `Slot`'s `React.Children.only` throws — the same error `Button asChild` throws.
|
|
66
|
+
*/
|
|
67
|
+
asChild?: boolean;
|
|
47
68
|
/**
|
|
48
69
|
* Ant Design `tabList` — the tab strip that lives IN THE CARD'S HEAD, under the title, inside
|
|
49
70
|
* the same border and on the same surface, so the card and its tabs read as one object. The
|
|
@@ -102,10 +123,31 @@ export declare const Card: React.ForwardRefExoticComponent<React.HTMLAttributes<
|
|
|
102
123
|
*
|
|
103
124
|
* This is a PRESENTATION flag, not an interaction: it announces nothing and binds no handler.
|
|
104
125
|
* A card that looks clickable has to BE clickable for everyone, so pair it with a real control
|
|
105
|
-
* — a `Link`/`Button` in the header or footer, or the whole card rendered as one
|
|
106
|
-
* bare `onClick` on this div, which a keyboard or
|
|
126
|
+
* — a `Link`/`Button` in the header or footer, or the whole card rendered as one via
|
|
127
|
+
* {@link CardProps.asChild} — never with a bare `onClick` on this div, which a keyboard or
|
|
128
|
+
* screen-reader user cannot reach.
|
|
107
129
|
*/
|
|
108
130
|
hoverable?: boolean;
|
|
131
|
+
/**
|
|
132
|
+
* Borrow the child's element for the card box instead of rendering a `div` — the package's
|
|
133
|
+
* standard Slot passthrough (`Button`, `AspectRatio`, `ListRow`). The card's chrome and every
|
|
134
|
+
* `data-*` flag move onto that element, so a card rendered as an `<a>` or a `<button>` measures
|
|
135
|
+
* byte for byte like the `div` it replaces and stays a SINGLE tab stop.
|
|
136
|
+
*
|
|
137
|
+
* This is the other half of `hoverable`: `<Card asChild hoverable><a href="…">` is "the whole
|
|
138
|
+
* card rendered as one control", reachable by keyboard and announced as one link.
|
|
139
|
+
*
|
|
140
|
+
* NESTING CAVEAT — the card is now ONE control, so it may not contain another. An
|
|
141
|
+
* `<a>`/`<button>` (a `Link`, a `Button`, a menu trigger) inside a card-as-link or
|
|
142
|
+
* card-as-button is invalid HTML: the inner control is unreachable or mis-announced, and
|
|
143
|
+
* `tabList` — a strip of `button` triggers — is for the same reason not drawn under `asChild`.
|
|
144
|
+
* A card that needs interactive children is not one control: drop `asChild` and put the
|
|
145
|
+
* `Link`/`Button` inside it instead, as `hoverable` also allows.
|
|
146
|
+
*
|
|
147
|
+
* Exactly one child, as everywhere else in this package: two children have no element to
|
|
148
|
+
* borrow, so `Slot`'s `React.Children.only` throws — the same error `Button asChild` throws.
|
|
149
|
+
*/
|
|
150
|
+
asChild?: boolean;
|
|
109
151
|
/**
|
|
110
152
|
* Ant Design `tabList` — the tab strip that lives IN THE CARD'S HEAD, under the title, inside
|
|
111
153
|
* the same border and on the same surface, so the card and its tabs read as one object. The
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { padStyle, padStepToken } from "../../lib/variants.js";
|
|
4
4
|
import * as React from "react";
|
|
5
|
+
import { isDevelopment } from "../../lib/dev.js";
|
|
6
|
+
import { Slot } from "../../lib/slot.js";
|
|
5
7
|
import { cn } from "../../lib/utils.js";
|
|
6
8
|
import { Tabs } from "../navigation/tabs.js";
|
|
7
9
|
function resolveActiveCardTabKey(tabList, requested) {
|
|
@@ -37,11 +39,32 @@ const Card = React.forwardRef(
|
|
|
37
39
|
onTabChange,
|
|
38
40
|
extra,
|
|
39
41
|
tabProps,
|
|
42
|
+
asChild = false,
|
|
40
43
|
children,
|
|
41
44
|
...props
|
|
42
45
|
}, ref) => {
|
|
46
|
+
const slotted = asChild && Boolean(tabList?.length);
|
|
47
|
+
if (isDevelopment() && slotted) {
|
|
48
|
+
console.warn(
|
|
49
|
+
"Card: `asChild` renders the whole card as ONE control, so `tabList` is ignored and the tab strip is NOT drawn \u2014 a strip of `button` triggers inside a link/button is invalid HTML. Drop `asChild` and put a `Link`/`Button` inside the card instead."
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
if (isDevelopment()) {
|
|
53
|
+
const slots = React.Children.toArray(children).filter(React.isValidElement);
|
|
54
|
+
const hasHeader = slots.some((child) => child.type === CardHeader);
|
|
55
|
+
const soloBody = slots.some(
|
|
56
|
+
(child) => child.type === CardContent && child.props?.solo === true
|
|
57
|
+
);
|
|
58
|
+
if (hasHeader && soloBody) {
|
|
59
|
+
console.warn(
|
|
60
|
+
"Card: `CardContent solo` says there is NO header above it, so it takes the card's own block padding \u2014 but this card also renders a `CardHeader`. The two cannot both hold, and the padding wins, which is the empty strip above your content. Drop `solo` to keep the header, or drop the header if the body really is alone."
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const tabs = slotted ? void 0 : tabList;
|
|
65
|
+
const Comp = asChild ? Slot : "div";
|
|
43
66
|
const [mirroredKey, setMirroredKey] = React.useState(void 0);
|
|
44
|
-
const activeKey =
|
|
67
|
+
const activeKey = tabs?.length ? resolveActiveCardTabKey(tabs, activeTabKey ?? mirroredKey ?? defaultActiveTabKey) : void 0;
|
|
45
68
|
const handleTabChange = React.useCallback(
|
|
46
69
|
(key) => {
|
|
47
70
|
setMirroredKey(key);
|
|
@@ -49,9 +72,9 @@ const Card = React.forwardRef(
|
|
|
49
72
|
},
|
|
50
73
|
[onTabChange]
|
|
51
74
|
);
|
|
52
|
-
const { head, body, foot } =
|
|
75
|
+
const { head, body, foot } = tabs?.length ? splitCardBands(children) : { head: void 0, body: void 0, foot: void 0 };
|
|
53
76
|
return /* @__PURE__ */ jsx(
|
|
54
|
-
|
|
77
|
+
Comp,
|
|
55
78
|
{
|
|
56
79
|
ref,
|
|
57
80
|
className: cn("group/card", className),
|
|
@@ -61,16 +84,16 @@ const Card = React.forwardRef(
|
|
|
61
84
|
"data-accent-placement": accentPlacement === "perimeter" ? "perimeter" : void 0,
|
|
62
85
|
"data-variant": variant && variant !== "default" ? variant : void 0,
|
|
63
86
|
"data-density": density,
|
|
64
|
-
"data-tab-list":
|
|
87
|
+
"data-tab-list": tabs?.length ? "" : void 0,
|
|
65
88
|
...props,
|
|
66
|
-
children:
|
|
89
|
+
children: tabs?.length ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
67
90
|
head,
|
|
68
91
|
/* @__PURE__ */ jsx(
|
|
69
92
|
Tabs,
|
|
70
93
|
{
|
|
71
94
|
variant: "line",
|
|
72
95
|
...tabProps,
|
|
73
|
-
items:
|
|
96
|
+
items: tabs.map((item) => ({
|
|
74
97
|
value: item.key,
|
|
75
98
|
label: item.tab,
|
|
76
99
|
disabled: item.disabled,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "AUTO-GENERATED by scripts/gen-measurement-contract.mjs — do not edit. Read this instead of guessing: docs/MEASUREMENT-CONTRACT.md.",
|
|
3
|
-
"version": "27.
|
|
3
|
+
"version": "27.12.1",
|
|
4
4
|
"targetSize": {
|
|
5
5
|
"standard": "WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px",
|
|
6
6
|
"min": 24,
|
package/dist/props/registry.d.ts
CHANGED
|
@@ -2851,7 +2851,7 @@ export declare const COMPONENT_PROP_REGISTRY: {
|
|
|
2851
2851
|
readonly CardProp: {
|
|
2852
2852
|
readonly group: "data-display";
|
|
2853
2853
|
readonly file: "components/data-display/card.tsx";
|
|
2854
|
-
readonly vocabulary: readonly ["ToneProp", "ClassNameProp", "ChildrenProp"];
|
|
2854
|
+
readonly vocabulary: readonly ["ToneProp", "ClassNameProp", "ChildrenProp", "AsChildProp"];
|
|
2855
2855
|
};
|
|
2856
2856
|
readonly CardTabItemProp: {
|
|
2857
2857
|
readonly group: "data-display";
|
package/dist/props/registry.js
CHANGED
|
@@ -3366,7 +3366,7 @@ const COMPONENT_PROP_REGISTRY = {
|
|
|
3366
3366
|
CardProp: {
|
|
3367
3367
|
group: "data-display",
|
|
3368
3368
|
file: "components/data-display/card.tsx",
|
|
3369
|
-
vocabulary: ["ToneProp", "ClassNameProp", "ChildrenProp"]
|
|
3369
|
+
vocabulary: ["ToneProp", "ClassNameProp", "ChildrenProp", "AsChildProp"]
|
|
3370
3370
|
},
|
|
3371
3371
|
CardTabItemProp: {
|
|
3372
3372
|
group: "data-display",
|
|
@@ -35,6 +35,9 @@
|
|
|
35
35
|
.ui-carousel-dot,
|
|
36
36
|
.ui-rating-star,
|
|
37
37
|
a.ui-list-row,
|
|
38
|
+
|
|
39
|
+
a[data-slot="card"],
|
|
40
|
+
button[data-slot="card"],
|
|
38
41
|
.ui-text[data-link],
|
|
39
42
|
.sb-nav-item,
|
|
40
43
|
.sb-user,
|
|
@@ -96,6 +99,8 @@
|
|
|
96
99
|
.ui-carousel-dot,
|
|
97
100
|
.ui-rating-star,
|
|
98
101
|
a.ui-list-row,
|
|
102
|
+
a[data-slot="card"],
|
|
103
|
+
button[data-slot="card"],
|
|
99
104
|
.ui-text[data-link],
|
|
100
105
|
.sb-nav-item,
|
|
101
106
|
.sb-user,
|
|
@@ -160,6 +160,47 @@ export default function Demo() {
|
|
|
160
160
|
</ResponsiveGrid>
|
|
161
161
|
</Flex>
|
|
162
162
|
|
|
163
|
+
<Flex direction="col" gap="sm">
|
|
164
|
+
<Text as="div" weight="medium">
|
|
165
|
+
asChild · カード全体をひとつのコントロールにする
|
|
166
|
+
</Text>
|
|
167
|
+
<ResponsiveGrid columns={{ sm: 1, md: 2 }}>
|
|
168
|
+
{/*
|
|
169
|
+
* `hoverable` が約束する「カード全体をひとつのコントロールとして描く」側。
|
|
170
|
+
* `asChild` はカードの箱を子の要素に譲るので、リンクそのものがカードになり、
|
|
171
|
+
* Tab は一度しか止まらず、スクリーンリーダーはリンクひとつとして読み上げます。
|
|
172
|
+
* 入れ子の禁則:これはもうコントロールなので、中に <a>/<Button> は置けません
|
|
173
|
+
* (HTML として不正)。中にボタンが要るカードは asChild を外し、hoverable +
|
|
174
|
+
* CardFooter のボタン(上の例)にします。
|
|
175
|
+
*/}
|
|
176
|
+
<Card asChild hoverable>
|
|
177
|
+
<a href="#card-as-child">
|
|
178
|
+
<CardHeader>
|
|
179
|
+
<CardTitle level={2}>カード全体がリンク</CardTitle>
|
|
180
|
+
<CardDescription>
|
|
181
|
+
タブ 1 回で届き、フォーカスリングはカードの箱に出ます
|
|
182
|
+
</CardDescription>
|
|
183
|
+
</CardHeader>
|
|
184
|
+
<CardContent>
|
|
185
|
+
詳細画面へ遷移します。onClick しか無い場合もこの形(router の Link
|
|
186
|
+
コンポーネントを子に渡す)にします。
|
|
187
|
+
</CardContent>
|
|
188
|
+
</a>
|
|
189
|
+
</Card>
|
|
190
|
+
<Card hoverable>
|
|
191
|
+
<CardHeader>
|
|
192
|
+
<CardTitle level={2}>中にコントロールがある場合</CardTitle>
|
|
193
|
+
<CardDescription>
|
|
194
|
+
asChild は使わず、本物のコントロールを内側に置きます
|
|
195
|
+
</CardDescription>
|
|
196
|
+
</CardHeader>
|
|
197
|
+
<CardFooter separated>
|
|
198
|
+
<Button variant="outline">詳細</Button>
|
|
199
|
+
</CardFooter>
|
|
200
|
+
</Card>
|
|
201
|
+
</ResponsiveGrid>
|
|
202
|
+
</Flex>
|
|
203
|
+
|
|
163
204
|
<Flex direction="col" gap="sm">
|
|
164
205
|
<Text as="div" weight="medium">
|
|
165
206
|
Accent · accentPlacement="edge" (既定 · 先頭エッジのレール)
|
package/package.json
CHANGED
package/scripts/ui-audit.mjs
CHANGED
|
@@ -498,15 +498,12 @@ const RULES = [
|
|
|
498
498
|
id: "icon-button-needs-name",
|
|
499
499
|
severity: "warn",
|
|
500
500
|
spansElement: true,
|
|
501
|
-
//
|
|
502
|
-
//
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
"g",
|
|
506
|
-
),
|
|
507
|
-
standard: "WCAG 2.2 SC 4.1.2 · 1.1.1 · WAI-ARIA 1.2",
|
|
501
|
+
// A `<Button size="icon">` with no accessible name AT ALL — neither from the author
|
|
502
|
+
// (aria-label / aria-labelledby / title) nor from its CONTENT. See `iconButtonMatches`.
|
|
503
|
+
matches: iconButtonMatches,
|
|
504
|
+
standard: "WCAG 2.2 SC 4.1.2 · 1.1.1 · WAI-ARIA 1.2 · Accessible Name Computation 1.2",
|
|
508
505
|
message:
|
|
509
|
-
"Icon-only <Button size=\"icon\">
|
|
506
|
+
"Icon-only <Button size=\"icon\"> has no accessible name — neither aria-label={t('…')} nor any content that names it. A visually-hidden child names it too: <VisuallyHidden>{t('…')}</VisuallyHidden> (or className=\"sr-only\") beside the aria-hidden glyph. A glyph alone, or text inside an aria-hidden subtree, names nothing.",
|
|
510
507
|
},
|
|
511
508
|
{
|
|
512
509
|
id: "img-needs-alt",
|
|
@@ -1074,6 +1071,135 @@ function* lucideGlyphMatches(source) {
|
|
|
1074
1071
|
}
|
|
1075
1072
|
}
|
|
1076
1073
|
|
|
1074
|
+
/**
|
|
1075
|
+
* Is this subtree hidden from the accessibility tree? `aria-hidden` with anything but a literal
|
|
1076
|
+
* `false` — an author who writes `aria-hidden={busy}` is saying the subtree MAY be hidden, and a
|
|
1077
|
+
* name that may not be there is not a name.
|
|
1078
|
+
*/
|
|
1079
|
+
const ariaHiddenTag = (openTag) =>
|
|
1080
|
+
!/\baria-hidden\s*=\s*(?:\{\s*false\s*\}|["']false["'])/.test(openTag) &&
|
|
1081
|
+
/\baria-hidden(?=[\s=/>])/.test(openTag);
|
|
1082
|
+
|
|
1083
|
+
/** The three attributes accname takes from the AUTHOR, on one opening tag. */
|
|
1084
|
+
const authoredNameTag = (openTag) => /\b(?:aria-label|aria-labelledby|title)\s*=/.test(openTag);
|
|
1085
|
+
|
|
1086
|
+
/** Where the element whose opening tag ended at `from` closes, counting nesting of the same tag. */
|
|
1087
|
+
function closingTagRange(source, tagName, from) {
|
|
1088
|
+
const tag = new RegExp(`<(/?)${tagName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?=[\\s/>])`, "g");
|
|
1089
|
+
tag.lastIndex = from;
|
|
1090
|
+
let depth = 0;
|
|
1091
|
+
for (let m; (m = tag.exec(source));) {
|
|
1092
|
+
if (m[1]) {
|
|
1093
|
+
if (depth === 0) {
|
|
1094
|
+
const gt = source.indexOf(">", m.index);
|
|
1095
|
+
return { start: m.index, end: gt < 0 ? source.length : gt + 1 };
|
|
1096
|
+
}
|
|
1097
|
+
depth -= 1;
|
|
1098
|
+
continue;
|
|
1099
|
+
}
|
|
1100
|
+
const end = jsxOpeningEnd(source, m.index);
|
|
1101
|
+
if (!/\/\s*$/.test(source.slice(m.index, end))) depth += 1;
|
|
1102
|
+
tag.lastIndex = end + 1;
|
|
1103
|
+
}
|
|
1104
|
+
return null;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
* "Name from content" (Accessible Name and Description Computation §2F) over JSX SOURCE: does
|
|
1109
|
+
* anything in these children reach the accessibility tree as text?
|
|
1110
|
+
*
|
|
1111
|
+
* - an `aria-hidden` subtree contributes NOTHING, however much text it holds;
|
|
1112
|
+
* - a visually-hidden child does — `sr-only` (and `VisuallyHidden`, which renders `span.sr-only`)
|
|
1113
|
+
* clips the box, it does not `display:none` the node, so the text is still named;
|
|
1114
|
+
* - a non-hidden child carrying its own `aria-label`/`title` contributes that name (`asChild`);
|
|
1115
|
+
* - an `{expression}` renders text, EXCEPT when it holds JSX — then what renders is those
|
|
1116
|
+
* elements, and `open ? <ChevronUp /> : <ChevronDown />` is still two unnamed glyphs.
|
|
1117
|
+
*
|
|
1118
|
+
* Limits, both deliberate: a bare `{icon}` identifier is read as text (source alone cannot say
|
|
1119
|
+
* what it holds, and guessing "glyph" would re-create the false positive this replaces), and
|
|
1120
|
+
* visual hiding is only recognised as `aria-hidden` — a `hidden`/`display:none` class is not read.
|
|
1121
|
+
*/
|
|
1122
|
+
function contributesName(source, textCounts = true) {
|
|
1123
|
+
let i = 0;
|
|
1124
|
+
while (i < source.length) {
|
|
1125
|
+
const char = source[i];
|
|
1126
|
+
if (char === "<") {
|
|
1127
|
+
if (source[i + 1] === "/") {
|
|
1128
|
+
const gt = source.indexOf(">", i);
|
|
1129
|
+
i = gt < 0 ? source.length : gt + 1;
|
|
1130
|
+
continue;
|
|
1131
|
+
}
|
|
1132
|
+
const tagName = /^<([A-Za-z][\w.:-]*)/.exec(source.slice(i))?.[1];
|
|
1133
|
+
if (!tagName) {
|
|
1134
|
+
i += 1;
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
const end = jsxOpeningEnd(source, i);
|
|
1138
|
+
const openTag = source.slice(i, end + 1);
|
|
1139
|
+
const hidden = ariaHiddenTag(openTag);
|
|
1140
|
+
if (!hidden && authoredNameTag(openTag)) return true;
|
|
1141
|
+
if (/\/\s*>$/.test(openTag)) {
|
|
1142
|
+
i = end + 1;
|
|
1143
|
+
continue;
|
|
1144
|
+
}
|
|
1145
|
+
const close = closingTagRange(source, tagName, end + 1);
|
|
1146
|
+
const childrenEnd = close ? close.start : source.length;
|
|
1147
|
+
if (!hidden && contributesName(source.slice(end + 1, childrenEnd))) return true;
|
|
1148
|
+
i = close ? close.end : source.length;
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
if (char === "{") {
|
|
1152
|
+
const close = matchBracket(source, i);
|
|
1153
|
+
const expr = source.slice(i + 1, close < 0 ? source.length : close);
|
|
1154
|
+
if (/<[A-Za-z]/.test(expr)) {
|
|
1155
|
+
if (contributesName(expr, false)) return true;
|
|
1156
|
+
} else if (
|
|
1157
|
+
/\S/.test(expr.replace(/(["'`])(?:\\.|(?!\1)[\s\S])*?\1/g, (s) => s.slice(1, -1)))
|
|
1158
|
+
) {
|
|
1159
|
+
// `{t("…")}` / `{label}` name it; prettier's `{" "}` does not.
|
|
1160
|
+
return true;
|
|
1161
|
+
}
|
|
1162
|
+
i = close < 0 ? source.length : close + 1;
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
// Inside an expression the scaffolding (`cond ? … : …`, `.map(…)`) is not rendered text, but a
|
|
1166
|
+
// string literal in it is.
|
|
1167
|
+
if (!textCounts && (char === '"' || char === "'" || char === "`")) {
|
|
1168
|
+
const end = endOfString(source, i);
|
|
1169
|
+
if (/\S/.test(source.slice(i + 1, end))) return true;
|
|
1170
|
+
i = end + 1;
|
|
1171
|
+
continue;
|
|
1172
|
+
}
|
|
1173
|
+
if (textCounts && char.trim()) return true;
|
|
1174
|
+
i += 1;
|
|
1175
|
+
}
|
|
1176
|
+
return false;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* An icon-only `<Button size="icon">` that NOTHING names (gh#739).
|
|
1181
|
+
*
|
|
1182
|
+
* The rule was a single regex over the OPENING TAG, so the most standard naming of an icon button
|
|
1183
|
+
* — an `aria-hidden` glyph beside visually-hidden text — was reported as unnamed: 5/5 findings in
|
|
1184
|
+
* one consumer repo were that shape. Following the message literally adds an `aria-label` on TOP
|
|
1185
|
+
* of a name, and two name sources drift apart the first time one i18n key is edited. So read the
|
|
1186
|
+
* children the way accname does, via `contributesName`.
|
|
1187
|
+
*/
|
|
1188
|
+
function* iconButtonMatches(source) {
|
|
1189
|
+
for (const m of source.matchAll(/<Button(?=[\s/>])/g)) {
|
|
1190
|
+
const end = jsxOpeningEnd(source, m.index);
|
|
1191
|
+
if (end >= source.length) continue;
|
|
1192
|
+
const openTag = source.slice(m.index, end + 1);
|
|
1193
|
+
if (!/\bsize\s*=\s*["']icon["']/.test(openTag)) continue;
|
|
1194
|
+
if (authoredNameTag(openTag)) continue;
|
|
1195
|
+
if (!/\/\s*>$/.test(openTag)) {
|
|
1196
|
+
const close = closingTagRange(source, "Button", end + 1);
|
|
1197
|
+
if (contributesName(source.slice(end + 1, close ? close.start : source.length))) continue;
|
|
1198
|
+
}
|
|
1199
|
+
yield { 0: openTag, index: m.index };
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1077
1203
|
/** Match real JSX text after a balanced opening tag, including props with comparisons. */
|
|
1078
1204
|
function* currencyMatches(source) {
|
|
1079
1205
|
for (const opening of source.matchAll(/<(?:[A-Za-z][\w.:]*\b|(?=>))/g)) {
|