praxis-kit 6.2.1 → 6.2.3

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.
@@ -993,11 +993,24 @@ var ContractDiagnostics = {
993
993
 
994
994
  // ../../lib/contract/src/diagnostics/html.ts
995
995
  import { DiagnosticCategory as DiagnosticCategory3, DiagnosticCode as DiagnosticCode3 } from "./_shared/diagnostics.js";
996
+ var ATTRIBUTE_IGNORED_CODES = {
997
+ checked: DiagnosticCode3.HtmlInputCheckedIgnoredForType,
998
+ multiple: DiagnosticCode3.HtmlInputMultipleIgnoredForType,
999
+ maxLength: DiagnosticCode3.HtmlInputMaxLengthIgnoredForType,
1000
+ minLength: DiagnosticCode3.HtmlInputMinLengthIgnoredForType,
1001
+ pattern: DiagnosticCode3.HtmlInputPatternIgnoredForType,
1002
+ min: DiagnosticCode3.HtmlInputMinIgnoredForType,
1003
+ max: DiagnosticCode3.HtmlInputMaxIgnoredForType,
1004
+ step: DiagnosticCode3.HtmlInputStepIgnoredForType,
1005
+ accept: DiagnosticCode3.HtmlInputAcceptIgnoredForType,
1006
+ capture: DiagnosticCode3.HtmlInputCaptureIgnoredForType
1007
+ };
996
1008
  var HtmlDiagnostics = {
997
1009
  emptyRole(tag) {
998
1010
  return {
999
1011
  code: DiagnosticCode3.HtmlEmptyRole,
1000
1012
  category: DiagnosticCategory3.HTML,
1013
+ severity: "warning",
1001
1014
  message: `<${tag}> has an explicit empty role="". Omit the attribute instead.`
1002
1015
  };
1003
1016
  },
@@ -1005,6 +1018,7 @@ var HtmlDiagnostics = {
1005
1018
  return {
1006
1019
  code: DiagnosticCode3.HtmlImplicitRoleRedundant,
1007
1020
  category: DiagnosticCategory3.HTML,
1021
+ severity: "warning",
1008
1022
  message: `<${tag}> already has implicit role="${implicitRole}". Avoid redundant role assignment.`
1009
1023
  };
1010
1024
  },
@@ -1012,6 +1026,7 @@ var HtmlDiagnostics = {
1012
1026
  return {
1013
1027
  code: DiagnosticCode3.HtmlImplicitRoleOverride,
1014
1028
  category: DiagnosticCategory3.HTML,
1029
+ severity: "error",
1015
1030
  message: `<${tag}> should not override its implicit role="${implicitRole}" with role="${role}".`
1016
1031
  };
1017
1032
  },
@@ -1019,6 +1034,7 @@ var HtmlDiagnostics = {
1019
1034
  return {
1020
1035
  code: DiagnosticCode3.HtmlStandaloneRegionOverride,
1021
1036
  category: DiagnosticCategory3.HTML,
1037
+ severity: "error",
1022
1038
  message: `<${tag}> is a self-contained element with implicit role="${implicitRole}". Assigning role="region" has been removed.`
1023
1039
  };
1024
1040
  },
@@ -1026,6 +1042,7 @@ var HtmlDiagnostics = {
1026
1042
  return {
1027
1043
  code: DiagnosticCode3.HtmlLandmarkRoleOverride,
1028
1044
  category: DiagnosticCategory3.HTML,
1045
+ severity: "error",
1029
1046
  message: `<${tag}> has a fixed landmark role="${implicitRole}". role="${role}" overrides it and confuses assistive technology. The override has been removed.`
1030
1047
  };
1031
1048
  },
@@ -1033,34 +1050,148 @@ var HtmlDiagnostics = {
1033
1050
  return {
1034
1051
  code: DiagnosticCode3.HtmlInvalidChild,
1035
1052
  category: DiagnosticCategory3.HTML,
1053
+ severity: "error",
1036
1054
  message: `<${child}> is not a valid direct child of <${parent}>. Allowed: ${allowed}.`
1037
1055
  };
1056
+ },
1057
+ roleNotPermitted(tag, role, allowedRoles) {
1058
+ const allowed = allowedRoles.length > 0 ? allowedRoles.map((r) => `"${r}"`).join(", ") : "none \u2014 no explicit role is permitted on this element";
1059
+ return {
1060
+ code: DiagnosticCode3.HtmlRoleNotPermitted,
1061
+ category: DiagnosticCategory3.HTML,
1062
+ severity: "error",
1063
+ message: `role="${role}" is not permitted on <${tag}>. Allowed alternate role(s): ${allowed}.`,
1064
+ rationale: 'The WAI-ARIA "ARIA in HTML" specification restricts which explicit roles a native element may take. A role outside that set is ignored or produces undefined behavior in assistive technology.'
1065
+ };
1066
+ },
1067
+ // Reserved for <input>-specific facts (HTML3101–3199, see codes.ts) — later element families
1068
+ // (button, img, table, ...) get their own reserved block and their own namespace here.
1069
+ input: {
1070
+ unsupportedType(type) {
1071
+ return {
1072
+ code: DiagnosticCode3.HtmlInputUnsupportedType,
1073
+ category: DiagnosticCategory3.HTML,
1074
+ severity: "warning",
1075
+ message: `type="${type}" is not a value defined by the HTML specification. Browsers silently fall back to type="text".`,
1076
+ rationale: 'An unrecognized input type is not invalid markup \u2014 the spec requires the "text" fallback \u2014 but it usually means a typo, since the input keeps working while silently losing the intended type-specific behavior (validation, virtual keyboard, picker UI).',
1077
+ suggestions: [
1078
+ {
1079
+ title: "Check for a typo in the type value",
1080
+ description: `"${type}" does not match any HTML5 input type.`
1081
+ }
1082
+ ]
1083
+ };
1084
+ },
1085
+ // The user-visible problem is that the attribute is ignored — not that it "requires" a type;
1086
+ // that's the rule's internal framing, not what the browser actually does.
1087
+ attributeIgnoredForType(attribute, type, allowedTypes) {
1088
+ const allowed = allowedTypes.map((t) => `"${t}"`).join(", ");
1089
+ const code = ATTRIBUTE_IGNORED_CODES[attribute];
1090
+ if (!code) throw new Error(`No DiagnosticCode registered for input attribute "${attribute}"`);
1091
+ return {
1092
+ code,
1093
+ category: DiagnosticCategory3.HTML,
1094
+ severity: "warning",
1095
+ message: `"${attribute}" is ignored on <input type="${type}">.`,
1096
+ rationale: `"${attribute}" only has an effect when type is one of: ${allowed}. Browsers silently ignore it on other input types.`,
1097
+ suggestions: [
1098
+ {
1099
+ title: `Remove "${attribute}"`,
1100
+ description: `"${attribute}" only affects <input> when type is one of: ${allowed}.`
1101
+ }
1102
+ ]
1103
+ };
1104
+ }
1038
1105
  }
1039
1106
  };
1040
1107
 
1041
- // ../../lib/contract/src/diagnostics/slot.ts
1108
+ // ../../lib/contract/src/diagnostics/input-accessibility.ts
1042
1109
  import { DiagnosticCategory as DiagnosticCategory4, DiagnosticCode as DiagnosticCode4 } from "./_shared/diagnostics.js";
1110
+ function accessibilityFact(input) {
1111
+ return { category: DiagnosticCategory4.Accessibility, ...input };
1112
+ }
1113
+ var InputAccessibilityDiagnostics = {
1114
+ missingAccessibleName() {
1115
+ return accessibilityFact({
1116
+ code: DiagnosticCode4.A11yInputMissingAccessibleName,
1117
+ severity: "warning",
1118
+ message: "This input has no accessible name. Add an associated <label>, aria-label, or aria-labelledby.",
1119
+ rationale: "Assistive technology announces a form field by its accessible name; without one, users of screen readers cannot tell what the field is for.",
1120
+ suggestions: [
1121
+ { title: "Add aria-label", description: 'Set aria-label="\u2026" directly on the input.' },
1122
+ {
1123
+ title: "Add an associated <label>",
1124
+ description: 'Wrap the input in a <label>, or point a <label for="\u2026"> at its id.'
1125
+ }
1126
+ ]
1127
+ });
1128
+ },
1129
+ placeholderIsNotLabel() {
1130
+ return accessibilityFact({
1131
+ code: DiagnosticCode4.A11yInputPlaceholderNotLabel,
1132
+ severity: "warning",
1133
+ message: "Placeholder text does not provide an accessible name. Add an associated <label>, aria-label, or aria-labelledby.",
1134
+ rationale: "Placeholder text disappears as users interact with the field and is not treated as the control's accessible name by many assistive technologies.",
1135
+ suggestions: [
1136
+ { title: "Add aria-label", description: 'Set aria-label="\u2026" directly on the input.' },
1137
+ {
1138
+ title: "Add an associated <label>",
1139
+ description: 'Wrap the input in a <label>, or point a <label for="\u2026"> at its id.'
1140
+ }
1141
+ ]
1142
+ });
1143
+ },
1144
+ passwordMissingAutocomplete() {
1145
+ return accessibilityFact({
1146
+ code: DiagnosticCode4.A11yInputPasswordAutocomplete,
1147
+ severity: "warning",
1148
+ message: "Password inputs should specify an autoComplete value.",
1149
+ rationale: "Without an explicit autocomplete hint, password managers and browsers cannot reliably tell a sign-in field apart from a password-creation field.",
1150
+ suggestions: [
1151
+ {
1152
+ title: 'Set autoComplete="current-password"',
1153
+ description: "Use this for sign-in forms."
1154
+ },
1155
+ {
1156
+ title: 'Set autoComplete="new-password"',
1157
+ description: "Use this for sign-up / change-password forms."
1158
+ }
1159
+ ]
1160
+ });
1161
+ },
1162
+ requiredReadOnlyConflict() {
1163
+ return accessibilityFact({
1164
+ code: DiagnosticCode4.A11yInputRequiredReadOnlyConflict,
1165
+ severity: "warning",
1166
+ message: "The required and readOnly attributes are both present. A read-only field cannot satisfy required validation through user interaction.",
1167
+ rationale: "This combination is valid HTML but usually indicates an unintended state. Consider using disabled instead of readOnly, or only applying required when the field is editable."
1168
+ });
1169
+ }
1170
+ };
1171
+
1172
+ // ../../lib/contract/src/diagnostics/slot.ts
1173
+ import { DiagnosticCategory as DiagnosticCategory5, DiagnosticCode as DiagnosticCode5 } from "./_shared/diagnostics.js";
1043
1174
  var SlotDiagnostics = {
1044
1175
  exclusive(name) {
1045
1176
  return {
1046
- code: DiagnosticCode4.SlotExclusive,
1047
- category: DiagnosticCategory4.Contract,
1177
+ code: DiagnosticCode5.SlotExclusive,
1178
+ category: DiagnosticCategory5.Contract,
1048
1179
  component: name,
1049
1180
  message: `${name}: "as" and "asChild" are mutually exclusive`
1050
1181
  };
1051
1182
  },
1052
1183
  singleChildRequired(name, elementTerm) {
1053
1184
  return {
1054
- code: DiagnosticCode4.SlotSingleChild,
1055
- category: DiagnosticCategory4.Contract,
1185
+ code: DiagnosticCode5.SlotSingleChild,
1186
+ category: DiagnosticCategory5.Contract,
1056
1187
  component: name,
1057
1188
  message: `${name}: asChild requires a ${elementTerm} child`
1058
1189
  };
1059
1190
  },
1060
1191
  singleChildExceeded(name, elementTerm, count) {
1061
1192
  return {
1062
- code: DiagnosticCode4.SlotSingleChild,
1063
- category: DiagnosticCategory4.Contract,
1193
+ code: DiagnosticCode5.SlotSingleChild,
1194
+ category: DiagnosticCategory5.Contract,
1064
1195
  component: name,
1065
1196
  message: `${name}: asChild requires exactly one ${elementTerm} child, got ${count}`
1066
1197
  };
@@ -1068,16 +1199,16 @@ var SlotDiagnostics = {
1068
1199
  discardedChildren(name, elementTerm, count) {
1069
1200
  const suffix = count === 1 ? "" : "ren";
1070
1201
  return {
1071
- code: DiagnosticCode4.SlotDiscardedChildren,
1072
- category: DiagnosticCategory4.Contract,
1202
+ code: DiagnosticCode5.SlotDiscardedChildren,
1203
+ category: DiagnosticCategory5.Contract,
1073
1204
  component: name,
1074
1205
  message: `${name}: asChild discarded ${count} non-element child${suffix} \u2014 only ${elementTerm}s are valid asChild children.`
1075
1206
  };
1076
1207
  },
1077
1208
  renderFnRequired(name, received) {
1078
1209
  return {
1079
- code: DiagnosticCode4.SlotRenderFn,
1080
- category: DiagnosticCategory4.Contract,
1210
+ code: DiagnosticCode5.SlotRenderFn,
1211
+ category: DiagnosticCategory5.Contract,
1081
1212
  component: name,
1082
1213
  message: `${name}: asChild requires a render function as children, got ${received}`
1083
1214
  };
@@ -1139,7 +1270,7 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1139
1270
  tag,
1140
1271
  role: "",
1141
1272
  attribute: void 0,
1142
- severity: "warning",
1273
+ severity: d.severity,
1143
1274
  phase: "evaluate"
1144
1275
  }
1145
1276
  ]
@@ -1398,13 +1529,14 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1398
1529
  const role = props.role;
1399
1530
  if (!implicitRole || !role || role === implicitRole) return NO_VIOLATIONS;
1400
1531
  if (isStrongImplicitRole(tag) && role === "region") {
1532
+ const diagnostic = HtmlDiagnostics.implicitRoleOverride(tag, implicitRole, role);
1401
1533
  return [
1402
1534
  {
1403
1535
  valid: false,
1404
1536
  fixable: true,
1405
- severity: "error",
1537
+ severity: diagnostic.severity,
1406
1538
  fix: _AriaPolicyEngine.#removeRole,
1407
- diagnostic: HtmlDiagnostics.implicitRoleOverride(tag, implicitRole, role)
1539
+ diagnostic
1408
1540
  }
1409
1541
  ];
1410
1542
  }
@@ -1413,13 +1545,14 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1413
1545
  static #checkRedundantRole({ tag, props, implicitRole }) {
1414
1546
  const role = props.role;
1415
1547
  if (!implicitRole || !role || role !== implicitRole) return NO_VIOLATIONS;
1548
+ const diagnostic = HtmlDiagnostics.implicitRoleRedundant(tag, implicitRole);
1416
1549
  return [
1417
1550
  {
1418
1551
  valid: false,
1419
1552
  fixable: true,
1420
- severity: "warning",
1553
+ severity: diagnostic.severity,
1421
1554
  fix: _AriaPolicyEngine.#removeRole,
1422
- diagnostic: HtmlDiagnostics.implicitRoleRedundant(tag, implicitRole)
1555
+ diagnostic
1423
1556
  }
1424
1557
  ];
1425
1558
  }
@@ -1427,13 +1560,14 @@ var AriaPolicyEngine = class _AriaPolicyEngine extends InvariantBase {
1427
1560
  const role = props.role;
1428
1561
  if (role !== "region") return NO_VIOLATIONS;
1429
1562
  if (!isStandaloneTag(tag)) return NO_VIOLATIONS;
1563
+ const diagnostic = HtmlDiagnostics.standaloneRegionOverride(tag, implicitRole ?? tag);
1430
1564
  return [
1431
1565
  {
1432
1566
  valid: false,
1433
1567
  fixable: true,
1434
- severity: "error",
1568
+ severity: diagnostic.severity,
1435
1569
  fix: _AriaPolicyEngine.#removeRole,
1436
- diagnostic: HtmlDiagnostics.standaloneRegionOverride(tag, implicitRole ?? tag)
1570
+ diagnostic
1437
1571
  }
1438
1572
  ];
1439
1573
  }
@@ -2122,6 +2256,331 @@ var readonlyProps = ({
2122
2256
  // ../core/src/html/evaluators.ts
2123
2257
  import { warnDiagnostics as warnDiagnostics2 } from "./_shared/diagnostics.js";
2124
2258
 
2259
+ // ../core/src/html/input-rules.ts
2260
+ var DEFAULT_INPUT_TYPE = "text";
2261
+ function omit(props, key) {
2262
+ const next = { ...props };
2263
+ delete next[key];
2264
+ return next;
2265
+ }
2266
+ function removeAttributeFix(attribute) {
2267
+ return {
2268
+ kind: `removeAttribute:${attribute}`,
2269
+ apply: ({ props }) => {
2270
+ if (!(attribute in props)) return { applied: false, next: props };
2271
+ return { applied: true, next: omit(props, attribute), previous: props };
2272
+ }
2273
+ };
2274
+ }
2275
+ function inputAttributeRequiresType(attribute, allowedTypes) {
2276
+ const rule = ({ tag, props }) => {
2277
+ if (tag !== "input" || !(attribute in props)) return [];
2278
+ const type = typeof props.type === "string" ? props.type : DEFAULT_INPUT_TYPE;
2279
+ if (allowedTypes.includes(type)) return [];
2280
+ const diagnostic = HtmlDiagnostics.input.attributeIgnoredForType(attribute, type, allowedTypes);
2281
+ return [
2282
+ {
2283
+ valid: false,
2284
+ fixable: true,
2285
+ severity: diagnostic.severity,
2286
+ fix: removeAttributeFix(attribute),
2287
+ diagnostic
2288
+ }
2289
+ ];
2290
+ };
2291
+ return Object.assign(rule, { readsProps: ["type", attribute] });
2292
+ }
2293
+ var TEXT_INPUT_TYPES = ["text", "search", "url", "tel", "email", "password"];
2294
+ var NUMERIC_INPUT_TYPES = [
2295
+ "number",
2296
+ "range",
2297
+ "date",
2298
+ "month",
2299
+ "week",
2300
+ "time",
2301
+ "datetime-local"
2302
+ ];
2303
+ var HTML_INPUT_TYPES = /* @__PURE__ */ new Set([
2304
+ ...TEXT_INPUT_TYPES,
2305
+ ...NUMERIC_INPUT_TYPES,
2306
+ "checkbox",
2307
+ "radio",
2308
+ "file",
2309
+ "color",
2310
+ "hidden",
2311
+ "button",
2312
+ "submit",
2313
+ "reset",
2314
+ "image"
2315
+ ]);
2316
+ var supportedInputTypeRule = Object.assign(
2317
+ ({ tag, props }) => {
2318
+ if (tag !== "input" || typeof props.type !== "string") return [];
2319
+ const type = props.type;
2320
+ if (HTML_INPUT_TYPES.has(type)) return [];
2321
+ const diagnostic = HtmlDiagnostics.input.unsupportedType(type);
2322
+ return [
2323
+ {
2324
+ valid: false,
2325
+ fixable: false,
2326
+ severity: diagnostic.severity,
2327
+ diagnostic
2328
+ }
2329
+ ];
2330
+ },
2331
+ { readsProps: ["type"] }
2332
+ );
2333
+ var checkedRequiresCheckableTypeRule = inputAttributeRequiresType("checked", [
2334
+ "checkbox",
2335
+ "radio"
2336
+ ]);
2337
+ var multipleRequiresSupportedTypeRule = inputAttributeRequiresType("multiple", [
2338
+ "email",
2339
+ "file"
2340
+ ]);
2341
+ var maxLengthRequiresTextTypeRule = inputAttributeRequiresType(
2342
+ "maxLength",
2343
+ TEXT_INPUT_TYPES
2344
+ );
2345
+ var minLengthRequiresTextTypeRule = inputAttributeRequiresType(
2346
+ "minLength",
2347
+ TEXT_INPUT_TYPES
2348
+ );
2349
+ var patternRequiresTextTypeRule = inputAttributeRequiresType("pattern", TEXT_INPUT_TYPES);
2350
+ var minRequiresNumericTypeRule = inputAttributeRequiresType("min", NUMERIC_INPUT_TYPES);
2351
+ var maxRequiresNumericTypeRule = inputAttributeRequiresType("max", NUMERIC_INPUT_TYPES);
2352
+ var stepRequiresNumericTypeRule = inputAttributeRequiresType("step", NUMERIC_INPUT_TYPES);
2353
+ var acceptRequiresFileTypeRule = inputAttributeRequiresType("accept", ["file"]);
2354
+ var captureRequiresFileTypeRule = inputAttributeRequiresType("capture", ["file"]);
2355
+ var inputAccessibleNameRule = Object.assign(
2356
+ ({ tag, props }) => {
2357
+ if (tag !== "input" || props.type === "hidden") return [];
2358
+ if ("aria-label" in props || "aria-labelledby" in props) return [];
2359
+ const hasPlaceholder = typeof props.placeholder === "string" && props.placeholder.length > 0;
2360
+ const diagnostic = hasPlaceholder ? InputAccessibilityDiagnostics.placeholderIsNotLabel() : InputAccessibilityDiagnostics.missingAccessibleName();
2361
+ return [{ valid: false, fixable: false, severity: diagnostic.severity, diagnostic }];
2362
+ },
2363
+ { readsProps: ["type", "aria-label", "aria-labelledby", "placeholder"] }
2364
+ );
2365
+ var PASSWORD_AUTOCOMPLETE_VALUES = ["current-password", "new-password"];
2366
+ var passwordAutocompleteRule = Object.assign(
2367
+ ({ tag, props }) => {
2368
+ if (tag !== "input" || props.type !== "password") return [];
2369
+ const autoComplete = props.autoComplete;
2370
+ const tokens = typeof autoComplete === "string" ? autoComplete.split(" ") : [];
2371
+ if (PASSWORD_AUTOCOMPLETE_VALUES.some((value) => tokens.includes(value))) return [];
2372
+ const diagnostic = InputAccessibilityDiagnostics.passwordMissingAutocomplete();
2373
+ return [{ valid: false, fixable: false, severity: diagnostic.severity, diagnostic }];
2374
+ },
2375
+ { readsProps: ["type", "autoComplete"] }
2376
+ );
2377
+ var requiredReadOnlyConflictRule = Object.assign(
2378
+ ({ tag, props }) => {
2379
+ if (tag !== "input" || !props.required || !props.readOnly) return [];
2380
+ const diagnostic = InputAccessibilityDiagnostics.requiredReadOnlyConflict();
2381
+ return [{ valid: false, fixable: false, severity: diagnostic.severity, diagnostic }];
2382
+ },
2383
+ { readsProps: ["required", "readOnly"] }
2384
+ );
2385
+ var INPUT_RULES = [
2386
+ supportedInputTypeRule,
2387
+ checkedRequiresCheckableTypeRule,
2388
+ multipleRequiresSupportedTypeRule,
2389
+ maxLengthRequiresTextTypeRule,
2390
+ minLengthRequiresTextTypeRule,
2391
+ patternRequiresTextTypeRule,
2392
+ minRequiresNumericTypeRule,
2393
+ maxRequiresNumericTypeRule,
2394
+ stepRequiresNumericTypeRule,
2395
+ acceptRequiresFileTypeRule,
2396
+ captureRequiresFileTypeRule,
2397
+ inputAccessibleNameRule,
2398
+ passwordAutocompleteRule,
2399
+ requiredReadOnlyConflictRule
2400
+ ];
2401
+
2402
+ // ../core/src/html/role-restrictions.ts
2403
+ var ALLOWED_ROLES = {
2404
+ article: ["application", "document", "feed", "main", "none", "presentation", "region"],
2405
+ aside: ["feed", "none", "presentation", "region", "search"],
2406
+ footer: ["group", "none", "presentation"],
2407
+ header: ["group", "none", "presentation"],
2408
+ main: [],
2409
+ nav: [],
2410
+ a: [
2411
+ "button",
2412
+ "checkbox",
2413
+ "menuitem",
2414
+ "menuitemcheckbox",
2415
+ "menuitemradio",
2416
+ "option",
2417
+ "radio",
2418
+ "switch",
2419
+ "tab",
2420
+ "treeitem"
2421
+ ],
2422
+ button: [
2423
+ "checkbox",
2424
+ "link",
2425
+ "menuitem",
2426
+ "menuitemcheckbox",
2427
+ "menuitemradio",
2428
+ "option",
2429
+ "radio",
2430
+ "switch",
2431
+ "tab"
2432
+ ],
2433
+ select: ["menu"],
2434
+ h1: ["tab", "presentation", "none"],
2435
+ h2: ["tab", "presentation", "none"],
2436
+ h3: ["tab", "presentation", "none"],
2437
+ h4: ["tab", "presentation", "none"],
2438
+ h5: ["tab", "presentation", "none"],
2439
+ h6: ["tab", "presentation", "none"],
2440
+ ul: [
2441
+ "directory",
2442
+ "group",
2443
+ "listbox",
2444
+ "menu",
2445
+ "menubar",
2446
+ "radiogroup",
2447
+ "tablist",
2448
+ "toolbar",
2449
+ "tree"
2450
+ ],
2451
+ ol: [
2452
+ "directory",
2453
+ "group",
2454
+ "listbox",
2455
+ "menu",
2456
+ "menubar",
2457
+ "radiogroup",
2458
+ "tablist",
2459
+ "toolbar",
2460
+ "tree"
2461
+ ],
2462
+ li: [
2463
+ "menuitem",
2464
+ "menuitemcheckbox",
2465
+ "menuitemradio",
2466
+ "option",
2467
+ "none",
2468
+ "presentation",
2469
+ "radio",
2470
+ "separator",
2471
+ "tab",
2472
+ "treeitem"
2473
+ ],
2474
+ table: ["grid", "treegrid"],
2475
+ dialog: ["alertdialog"],
2476
+ fieldset: ["none", "presentation", "radiogroup"]
2477
+ };
2478
+ var IMG_NAMED_ROLES = [
2479
+ "button",
2480
+ "checkbox",
2481
+ "link",
2482
+ "menuitem",
2483
+ "menuitemcheckbox",
2484
+ "menuitemradio",
2485
+ "option",
2486
+ "progressbar",
2487
+ "scrollbar",
2488
+ "separator",
2489
+ "slider",
2490
+ "switch",
2491
+ "tab",
2492
+ "treeitem"
2493
+ ];
2494
+ var ALLOWED_INPUT_ROLES = {
2495
+ checkbox: ["menuitemcheckbox", "option", "switch", "button"],
2496
+ radio: ["menuitemradio"],
2497
+ range: [],
2498
+ number: [],
2499
+ search: ["combobox"],
2500
+ text: ["combobox", "searchbox", "spinbutton"],
2501
+ email: ["combobox"],
2502
+ tel: ["combobox"],
2503
+ url: ["combobox"],
2504
+ button: [
2505
+ "link",
2506
+ "menuitem",
2507
+ "menuitemcheckbox",
2508
+ "menuitemradio",
2509
+ "option",
2510
+ "radio",
2511
+ "switch",
2512
+ "tab"
2513
+ ],
2514
+ submit: [
2515
+ "link",
2516
+ "menuitem",
2517
+ "menuitemcheckbox",
2518
+ "menuitemradio",
2519
+ "option",
2520
+ "radio",
2521
+ "switch",
2522
+ "tab"
2523
+ ],
2524
+ reset: [
2525
+ "link",
2526
+ "menuitem",
2527
+ "menuitemcheckbox",
2528
+ "menuitemradio",
2529
+ "option",
2530
+ "radio",
2531
+ "switch",
2532
+ "tab"
2533
+ ],
2534
+ image: [
2535
+ "link",
2536
+ "menuitem",
2537
+ "menuitemcheckbox",
2538
+ "menuitemradio",
2539
+ "option",
2540
+ "radio",
2541
+ "switch",
2542
+ "tab"
2543
+ ],
2544
+ hidden: []
2545
+ };
2546
+ function getAllowedRoles(tag, props) {
2547
+ if (tag === "input") {
2548
+ const type = typeof props.type === "string" ? props.type : "text";
2549
+ return ALLOWED_INPUT_ROLES[type];
2550
+ }
2551
+ if (tag === "img") {
2552
+ return props.alt === "" ? [] : IMG_NAMED_ROLES;
2553
+ }
2554
+ return ALLOWED_ROLES[tag];
2555
+ }
2556
+ var removeRoleFix = {
2557
+ kind: "removeRole",
2558
+ apply: ({ props }) => {
2559
+ if (!("role" in props)) return { applied: false, next: props };
2560
+ const { role: _role, ...rest } = props;
2561
+ return { applied: true, next: rest, previous: props };
2562
+ }
2563
+ };
2564
+ var roleNotPermittedRule = Object.assign(
2565
+ ({ tag, props, implicitRole }) => {
2566
+ const role = props.role;
2567
+ if (typeof role !== "string" || role.length === 0 || role === implicitRole) return [];
2568
+ const allowed = getAllowedRoles(tag, props);
2569
+ if (allowed === void 0 || allowed.includes(role)) return [];
2570
+ const diagnostic = HtmlDiagnostics.roleNotPermitted(tag, role, allowed);
2571
+ return [
2572
+ {
2573
+ valid: false,
2574
+ fixable: true,
2575
+ severity: diagnostic.severity,
2576
+ fix: removeRoleFix,
2577
+ diagnostic
2578
+ }
2579
+ ];
2580
+ },
2581
+ { readsProps: ["role", "type", "alt"] }
2582
+ );
2583
+
2125
2584
  // ../core/src/html/aria-rules.ts
2126
2585
  var LANDMARK_TAG_SET = /* @__PURE__ */ new Set(["article", "aside", "footer", "header", "main", "nav"]);
2127
2586
  var removeLandmarkRoleOverride = {
@@ -2136,13 +2595,14 @@ function landmarkRoleRule({ tag, props, implicitRole }) {
2136
2595
  if (!LANDMARK_TAG_SET.has(tag) || !implicitRole) return [];
2137
2596
  const role = props.role;
2138
2597
  if (!role || role === implicitRole) return [];
2598
+ const diagnostic = HtmlDiagnostics.landmarkRoleOverride(tag, implicitRole, role);
2139
2599
  return [
2140
2600
  {
2141
2601
  valid: false,
2142
2602
  fixable: true,
2143
- severity: "error",
2603
+ severity: diagnostic.severity,
2144
2604
  fix: removeLandmarkRoleOverride,
2145
- diagnostic: HtmlDiagnostics.landmarkRoleOverride(tag, implicitRole, role)
2605
+ diagnostic
2146
2606
  }
2147
2607
  ];
2148
2608
  }
@@ -2162,7 +2622,12 @@ function landmarkNameAdvisory(ctx) {
2162
2622
  if (!ctx.implicitRole || !NAMED_LANDMARK_TAGS.has(ctx.tag)) return [];
2163
2623
  return requireAccessibleName(ctx);
2164
2624
  }
2165
- var HTML_ARIA_RULES = [landmarkRoleRule, landmarkNameAdvisory];
2625
+ var HTML_ARIA_RULES = [
2626
+ landmarkRoleRule,
2627
+ landmarkNameAdvisory,
2628
+ roleNotPermittedRule,
2629
+ ...INPUT_RULES
2630
+ ];
2166
2631
 
2167
2632
  // ../core/src/html/contracts.ts
2168
2633
  import { warnDiagnostics } from "./_shared/diagnostics.js";
@@ -2623,21 +3088,21 @@ function validateRenderProps(diagnostics, options, props, recipeKey) {
2623
3088
  import { throwDiagnostics } from "./_shared/diagnostics.js";
2624
3089
 
2625
3090
  // ../core/src/factory/plugin-diagnostics.ts
2626
- import { DiagnosticCategory as DiagnosticCategory5, DiagnosticCode as DiagnosticCode5 } from "./_shared/diagnostics.js";
3091
+ import { DiagnosticCategory as DiagnosticCategory6, DiagnosticCode as DiagnosticCode6 } from "./_shared/diagnostics.js";
2627
3092
  var PluginDiagnostics = {
2628
3093
  invalidShape(received) {
2629
3094
  const got = received === null ? "null" : typeof received;
2630
3095
  return {
2631
- code: DiagnosticCode5.PluginInvalidShape,
2632
- category: DiagnosticCategory5.Internal,
3096
+ code: DiagnosticCode6.PluginInvalidShape,
3097
+ category: DiagnosticCategory6.Internal,
2633
3098
  message: `[praxis-kit] Plugin factory must return an object with a 'pipeline' function. Got: ${got}.`
2634
3099
  };
2635
3100
  },
2636
3101
  pipelineReturnType(received) {
2637
3102
  const got = received === null ? "null" : Array.isArray(received) ? "array" : typeof received;
2638
3103
  return {
2639
- code: DiagnosticCode5.PluginPipelineReturnType,
2640
- category: DiagnosticCategory5.Internal,
3104
+ code: DiagnosticCode6.PluginPipelineReturnType,
3105
+ category: DiagnosticCategory6.Internal,
2641
3106
  message: `[praxis-kit] Plugin pipeline must return a string. Got: ${got}.`
2642
3107
  };
2643
3108
  }