diff --git a/COVERAGE.md b/COVERAGE.md
index 834a913..b024a15 100644
--- a/COVERAGE.md
+++ b/COVERAGE.md
@@ -29,7 +29,7 @@ We currently cover the following components:
- [x] Checkbox
- [] ColorPicker
- [x] Combobox
- - [] DataGrid
+ - [x] DataGrid
- [x] Dialog
- [N/A] Divider
- [] Drawer
@@ -70,7 +70,7 @@ We currently cover the following components:
- [N/A] SwatchPickerRow
- [x] Switch
- [] SearchBox
- - [] Table
+ - [x] Table
- [x] TabList
- [] Tag
- [] InteractionTag
@@ -83,7 +83,7 @@ We currently cover the following components:
- [] Toast
- [x] Toolbar
- [x] Tooltip
- - [] Tree
+ - [x] Tree
- [x] Datepicker
- [N/A] Calendar
- [x] Timepicker
diff --git a/README.md b/README.md
index 999ad2b..75cdd6b 100644
--- a/README.md
+++ b/README.md
@@ -184,6 +184,7 @@ Any use of third-party trademarks or logos are subject to those third-party's po
| [combobox-needs-labelling](docs/rules/combobox-needs-labelling.md) | All interactive elements must have an accessible name | ✅ | | |
| [compound-button-needs-labelling](docs/rules/compound-button-needs-labelling.md) | Accessibility: Compound buttons must have accessible labelling: title, aria-label, aria-labelledby, aria-describedby | ✅ | | |
| [counter-badge-needs-count](docs/rules/counter-badge-needs-count.md) | | ✅ | | 🔧 |
+| [datagrid-needs-labelling](docs/rules/datagrid-needs-labelling.md) | Accessibility: DataGrid must have proper labelling and follow ARIA grid patterns for complex data tables | ✅ | | |
| [dialogbody-needs-title-content-and-actions](docs/rules/dialogbody-needs-title-content-and-actions.md) | A DialogBody should have a header(DialogTitle), content(DialogContent), and footer(DialogActions) | ✅ | | |
| [dialogsurface-needs-aria](docs/rules/dialogsurface-needs-aria.md) | DialogueSurface need accessible labelling: aria-describedby on DialogueSurface and aria-label or aria-labelledby(if DialogueTitle is missing) | ✅ | | |
| [dropdown-needs-labelling](docs/rules/dropdown-needs-labelling.md) | Accessibility: Dropdown menu must have an id and it needs to be linked via htmlFor of a Label | ✅ | | |
@@ -209,11 +210,13 @@ Any use of third-party trademarks or logos are subject to those third-party's po
| [spin-button-unrecommended-labelling](docs/rules/spin-button-unrecommended-labelling.md) | Accessibility: Unrecommended accessibility labelling - SpinButton | ✅ | | |
| [spinner-needs-labelling](docs/rules/spinner-needs-labelling.md) | Accessibility: Spinner must have either aria-label or label, aria-live and aria-busy attributes | ✅ | | |
| [switch-needs-labelling](docs/rules/switch-needs-labelling.md) | Accessibility: Switch must have an accessible label | ✅ | | |
+| [table-needs-labelling](docs/rules/table-needs-labelling.md) | Accessibility: Table must have proper labelling and semantic structure for screen readers | ✅ | | |
| [tablist-and-tabs-need-labelling](docs/rules/tablist-and-tabs-need-labelling.md) | This rule aims to ensure that Tabs with icons but no text labels have an accessible name and that Tablist is properly labeled. | ✅ | | |
| [tag-dismissible-needs-labelling](docs/rules/tag-dismissible-needs-labelling.md) | This rule aims to ensure that dismissible Tag components have proper accessibility labelling: either aria-label on dismissIcon or aria-label on Tag with role on dismissIcon | ✅ | | |
| [tag-needs-name](docs/rules/tag-needs-name.md) | Accessibility: Tag must have an accessible name | ✅ | | |
| [toolbar-missing-aria](docs/rules/toolbar-missing-aria.md) | Accessibility: Toolbars need accessible labelling: aria-label or aria-labelledby | ✅ | | |
| [tooltip-not-recommended](docs/rules/tooltip-not-recommended.md) | Accessibility: Prefer text content or aria over a tooltip for these components MenuItem, SpinButton | ✅ | | |
+| [tree-needs-labelling](docs/rules/tree-needs-labelling.md) | Accessibility: Tree must have proper labelling and follow ARIA tree pattern for hierarchical navigation | ✅ | | |
| [visual-label-better-than-aria-suggestion](docs/rules/visual-label-better-than-aria-suggestion.md) | Visual label is better than an aria-label because sighted users can't read the aria-label text. | | ✅ | |
\ No newline at end of file
diff --git a/docs/rules/datagrid-needs-labelling.md b/docs/rules/datagrid-needs-labelling.md
new file mode 100644
index 0000000..3181ee6
--- /dev/null
+++ b/docs/rules/datagrid-needs-labelling.md
@@ -0,0 +1,85 @@
+# Accessibility: DataGrid must have proper labelling and follow ARIA grid patterns for complex data tables (`@microsoft/fluentui-jsx-a11y/datagrid-needs-labelling`)
+
+💼 This rule is enabled in the ✅ `recommended` config.
+
+
+
+DataGrid components must have accessible labelling for screen readers to understand the data structure and purpose.
+
+## Rule Details
+
+This rule enforces that DataGrid components have proper accessible names and follow ARIA grid patterns for complex data tables. DataGrids present tabular data that requires clear identification for assistive technology users.
+
+DataGrids must have an accessible name provided through one of these methods:
+- `aria-label` attribute with descriptive text
+- `aria-labelledby` attribute referencing existing label elements
+- Wrapping in a `Field` component that provides labeling context
+
+### Noncompliant
+
+```jsx
+// Missing any form of accessible labeling
+
+
+// Empty or whitespace-only labels
+
+
+
+// Invalid aria-labelledby reference
+
+```
+
+### Compliant
+
+```jsx
+// Using aria-label
+
+
+// Using aria-labelledby with existing elements
+
+
+
+// Wrapped in Field component
+
+
+
+
+// Best practice: Include row/column counts for large datasets
+
+```
+
+## Best Practices
+
+1. **Descriptive Labels**: Use clear, descriptive labels that explain the data's purpose
+2. **Data Context**: Include information about the data type (e.g., "Employee directory", "Sales report")
+3. **Size Hints**: For large datasets, consider mentioning approximate size in the label
+4. **Multiple Labels**: Use `aria-labelledby` to reference multiple elements for richer context
+
+## When Not To Use
+
+This rule should always be used for DataGrid components as they present complex tabular data that requires clear identification for screen reader users.
+
+## Related Rules
+
+- `table-needs-labelling` - Similar requirements for Table components
+- `field-needs-labelling` - Field wrapper component labeling
+
+## Accessibility Guidelines
+
+- [WCAG 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value.html)
+- [ARIA Grid Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/grid/)
+- [ARIA Table Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/table/)
diff --git a/docs/rules/table-needs-labelling.md b/docs/rules/table-needs-labelling.md
new file mode 100644
index 0000000..6cc783c
--- /dev/null
+++ b/docs/rules/table-needs-labelling.md
@@ -0,0 +1,103 @@
+# Accessibility: Table must have proper labelling and semantic structure for screen readers (`@microsoft/fluentui-jsx-a11y/table-needs-labelling`)
+
+💼 This rule is enabled in the ✅ `recommended` config.
+
+
+
+Table components must have accessible labelling for screen readers to understand the table's purpose and content structure.
+
+## Rule Details
+
+This rule enforces that Table components have proper accessible names following semantic HTML and ARIA best practices. Tables present structured data that requires clear identification for assistive technology users.
+
+Tables must have an accessible name provided through one of these methods:
+- `aria-label` attribute with descriptive text
+- `aria-labelledby` attribute referencing existing label elements
+- `
` element providing semantic table description
+- Wrapping in a `Field` component that provides labeling context
+
+### Noncompliant
+
+```jsx
+// Missing any form of accessible labeling
+
+
+
+
+
+// Empty labels don't provide accessibility
+
+
+
+
+
+// Invalid reference
+
+
+
+
+```
+
+### Compliant
+
+```jsx
+// Using aria-label
+
+
+// Using semantic caption element (preferred)
+
+
Sales Performance Q3 2024
+
+
+
+
+// Using aria-labelledby
+
Employee Directory
+
+
+
+
+
+// Wrapped in Field component
+
+
+
+
+
+
+```
+
+## Best Practices
+
+1. **Prefer `
`**: Use caption elements for semantic table labeling when possible
+2. **Descriptive Labels**: Clearly describe the table's content and purpose
+3. **Context Information**: Include relevant time periods, data scope, or categories
+4. **Consistent Structure**: Ensure tables have proper TableHeader and TableBody elements
+
+## When Not To Use
+
+This rule should always be used for Table components as they present structured data that requires clear identification for screen reader users.
+
+## Related Rules
+
+- `datagrid-needs-labelling` - Similar requirements for DataGrid components
+- `field-needs-labelling` - Field wrapper component labeling
+
+## Accessibility Guidelines
+
+- [WCAG 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value.html)
+- [ARIA Table Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/table/)
+- [HTML Table Accessibility](https://webaim.org/techniques/tables/)
diff --git a/docs/rules/tree-needs-labelling.md b/docs/rules/tree-needs-labelling.md
new file mode 100644
index 0000000..7124467
--- /dev/null
+++ b/docs/rules/tree-needs-labelling.md
@@ -0,0 +1,98 @@
+# Accessibility: Tree must have proper labelling and follow ARIA tree pattern for hierarchical navigation (`@microsoft/fluentui-jsx-a11y/tree-needs-labelling`)
+
+💼 This rule is enabled in the ✅ `recommended` config.
+
+
+
+Tree components must have accessible labelling for screen readers to understand the hierarchical structure and navigation purpose.
+
+## Rule Details
+
+This rule enforces that Tree components have proper accessible names following ARIA tree pattern guidelines. Trees present hierarchical data that requires clear identification for assistive technology users to understand the navigation context.
+
+Trees must have an accessible name provided through one of these methods:
+- `aria-label` attribute with descriptive text
+- `aria-labelledby` attribute referencing existing label elements
+- Wrapping in a `Field` component that provides labeling context
+
+### Noncompliant
+
+```jsx
+// Missing any form of accessible labeling
+
+ Folder 1
+ Folder 2
+
+
+// Empty or whitespace-only labels
+
+ Item 1
+
+
+// Invalid aria-labelledby reference
+
+ Item 1
+
+```
+
+### Compliant
+
+```jsx
+// Using aria-label
+
+
+ Documents
+
+ Resume.pdf
+ Cover Letter.docx
+
+
+ Images
+
+
+// Using aria-labelledby
+
Site Navigation
+
+ Home
+ Products
+ Contact
+
+
+// Wrapped in Field component
+
+
+ CEO
+ VP Engineering
+
+
+
+// Complex labeling with instructions
+
Project Files
+
Use arrow keys to navigate, Enter to open
+
+ src/
+ docs/
+ tests/
+
+```
+
+## Best Practices
+
+1. **Context Description**: Clearly describe what the tree represents (file system, navigation, org chart)
+2. **Navigation Hints**: Consider including keyboard navigation instructions in labels
+3. **Scope Information**: Indicate the breadth or type of items in the tree
+4. **Hierarchical Context**: Help users understand the tree's organizational purpose
+
+## When Not To Use
+
+This rule should always be used for Tree components as they present complex hierarchical navigation that requires clear identification for screen reader users.
+
+## Related Rules
+
+- `field-needs-labelling` - Field wrapper component labeling
+
+## Accessibility Guidelines
+
+- [WCAG 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value.html)
+- [ARIA Tree Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/)
+- [ARIA Navigation Landmarks](https://www.w3.org/WAI/ARIA/apg/practices/landmark-regions/)
diff --git a/lib/index.ts b/lib/index.ts
index f3a13e3..92d43fa 100644
--- a/lib/index.ts
+++ b/lib/index.ts
@@ -28,6 +28,7 @@ module.exports = {
"@microsoft/fluentui-jsx-a11y/combobox-needs-labelling": "error",
"@microsoft/fluentui-jsx-a11y/compound-button-needs-labelling": "error",
"@microsoft/fluentui-jsx-a11y/counter-badge-needs-count": "error",
+ "@microsoft/fluentui-jsx-a11y/datagrid-needs-labelling": "error",
"@microsoft/fluentui-jsx-a11y/dialogbody-needs-title-content-and-actions": "error",
"@microsoft/fluentui-jsx-a11y/dialogsurface-needs-aria": "error",
"@microsoft/fluentui-jsx-a11y/dropdown-needs-labelling": "error",
@@ -53,11 +54,13 @@ module.exports = {
"@microsoft/fluentui-jsx-a11y/spin-button-unrecommended-labelling": "error",
"@microsoft/fluentui-jsx-a11y/spinner-needs-labelling": "error",
"@microsoft/fluentui-jsx-a11y/switch-needs-labelling": "error",
+ "@microsoft/fluentui-jsx-a11y/table-needs-labelling": "error",
"@microsoft/fluentui-jsx-a11y/tablist-and-tabs-need-labelling": "error",
"@microsoft/fluentui-jsx-a11y/tag-dismissible-needs-labelling": "error",
"@microsoft/fluentui-jsx-a11y/tag-needs-name": "error",
"@microsoft/fluentui-jsx-a11y/toolbar-missing-aria": "error",
"@microsoft/fluentui-jsx-a11y/tooltip-not-recommended": "error",
+ "@microsoft/fluentui-jsx-a11y/tree-needs-labelling": "error",
"@microsoft/fluentui-jsx-a11y/visual-label-better-than-aria-suggestion": "warn"
}
}
@@ -75,6 +78,7 @@ module.exports = {
"combobox-needs-labelling": rules.comboboxNeedsLabelling,
"compound-button-needs-labelling": rules.compoundButtonNeedsLabelling,
"counter-badge-needs-count": rules.counterBadgeNeedsCount,
+ "datagrid-needs-labelling": rules.dataGridNeedsLabelling,
"dialogbody-needs-title-content-and-actions": rules.dialogbodyNeedsTitleContentAndActions,
"dialogsurface-needs-aria": rules.dialogsurfaceNeedsAria,
"dropdown-needs-labelling": rules.dropdownNeedsLabelling,
@@ -100,11 +104,13 @@ module.exports = {
"spin-button-unrecommended-labelling": rules.spinButtonUnrecommendedLabelling,
"spinner-needs-labelling": rules.spinnerNeedsLabelling,
"switch-needs-labelling": rules.switchNeedsLabelling,
+ "table-needs-labelling": rules.tableNeedsLabelling,
"tablist-and-tabs-need-labelling": rules.tablistAndTabsNeedLabelling,
"tag-dismissible-needs-labelling": rules.tagDismissibleNeedsLabelling,
"tag-needs-name": rules.tagNeedsName,
"toolbar-missing-aria": rules.toolbarMissingAria,
"tooltip-not-recommended": rules.tooltipNotRecommended,
+ "tree-needs-labelling": rules.treeNeedsLabelling,
"visual-label-better-than-aria-suggestion": rules.visualLabelBetterThanAriaSuggestion
}
};
diff --git a/lib/rules/datagrid-needs-labelling.ts b/lib/rules/datagrid-needs-labelling.ts
new file mode 100644
index 0000000..2911559
--- /dev/null
+++ b/lib/rules/datagrid-needs-labelling.ts
@@ -0,0 +1,72 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
+import { elementType } from "jsx-ast-utils";
+import { hasNonEmptyProp } from "../util/hasNonEmptyProp";
+import { hasAssociatedLabelViaAriaLabelledBy } from "../util/labelUtils";
+import { hasFieldParent } from "../util/hasFieldParent";
+import { JSXOpeningElement } from "estree-jsx";
+
+//------------------------------------------------------------------------------
+// Rule Definition
+//------------------------------------------------------------------------------
+
+const rule = ESLintUtils.RuleCreator.withoutDocs({
+ defaultOptions: [],
+ meta: {
+ messages: {
+ missingDataGridLabelling:
+ "Accessibility: DataGrid must have an accessible name via aria-label, aria-labelledby, or be wrapped in a Field component",
+ suggestAriaRowCount: "Accessibility: Consider adding aria-rowcount for better screen reader experience with large datasets"
+ },
+ type: "problem",
+ docs: {
+ description: "Accessibility: DataGrid must have proper labelling and follow ARIA grid patterns for complex data tables",
+ recommended: "strict",
+ url: "https://www.w3.org/WAI/ARIA/apg/patterns/grid/"
+ },
+ schema: []
+ },
+ create(context) {
+ return {
+ JSXOpeningElement(node: TSESTree.JSXOpeningElement) {
+ // if it is not a DataGrid, return
+ if (elementType(node as JSXOpeningElement) !== "DataGrid") {
+ return;
+ }
+
+ // Check if DataGrid is wrapped in a Field component (provides labeling context)
+ const hasFieldParentCheck = hasFieldParent(context);
+
+ // Check for direct labeling methods
+ const hasAriaLabel = hasNonEmptyProp(node.attributes, "aria-label");
+ const hasAriaLabelledBy = hasAssociatedLabelViaAriaLabelledBy(node, context);
+
+ // DataGrid accessibility requirements:
+ // 1. Must have accessible name (Field parent, aria-label, or aria-labelledby)
+ const hasAccessibleName = hasFieldParentCheck || hasAriaLabel || hasAriaLabelledBy;
+
+ if (!hasAccessibleName) {
+ context.report({
+ node,
+ messageId: "missingDataGridLabelling"
+ });
+ }
+
+ // Optional: Check for ARIA best practices
+ const hasRowCount = hasNonEmptyProp(node.attributes, "aria-rowcount");
+ const hasItems = hasNonEmptyProp(node.attributes, "items");
+
+ // If DataGrid has items but no aria-rowcount, suggest it for better UX
+ // (This would be a warning-level rule in practice)
+ if (hasItems && !hasRowCount && hasAccessibleName) {
+ // Could add a suggestion here for aria-rowcount
+ // context.report({ node, messageId: "suggestAriaRowCount" });
+ }
+ }
+ };
+ }
+});
+
+export default rule;
diff --git a/lib/rules/index.ts b/lib/rules/index.ts
index 83bc9e1..a1edb4c 100644
--- a/lib/rules/index.ts
+++ b/lib/rules/index.ts
@@ -45,3 +45,6 @@ export { default as toolbarMissingAria } from "./toolbar-missing-aria";
export { default as tooltipNotRecommended } from "./tooltip-not-recommended";
export { default as visualLabelBetterThanAriaSuggestion } from "./visual-label-better-than-aria-suggestion";
export { default as preferDisabledFocusableOverDisabled } from "./prefer-disabledfocusable-over-disabled";
+export { default as dataGridNeedsLabelling } from "./datagrid-needs-labelling";
+export { default as tableNeedsLabelling } from "./table-needs-labelling";
+export { default as treeNeedsLabelling } from "./tree-needs-labelling";
diff --git a/lib/rules/table-needs-labelling.ts b/lib/rules/table-needs-labelling.ts
new file mode 100644
index 0000000..af07316
--- /dev/null
+++ b/lib/rules/table-needs-labelling.ts
@@ -0,0 +1,72 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
+import { elementType } from "jsx-ast-utils";
+import { hasNonEmptyProp } from "../util/hasNonEmptyProp";
+import { hasAssociatedLabelViaAriaLabelledBy } from "../util/labelUtils";
+import { hasFieldParent } from "../util/hasFieldParent";
+import { hasTextContentChild } from "../util/hasTextContentChild";
+import { JSXOpeningElement } from "estree-jsx";
+
+//------------------------------------------------------------------------------
+// Rule Definition
+//------------------------------------------------------------------------------
+
+const rule = ESLintUtils.RuleCreator.withoutDocs({
+ defaultOptions: [],
+ meta: {
+ messages: {
+ missingTableLabelling:
+ "Accessibility: Table must have an accessible name via aria-label, aria-labelledby, caption element, or Field wrapper",
+ missingTableStructure: "Accessibility: Table should contain TableHeader and TableBody for proper structure"
+ },
+ type: "problem",
+ docs: {
+ description: "Accessibility: Table must have proper labelling and semantic structure for screen readers",
+ recommended: "strict",
+ url: "https://www.w3.org/WAI/ARIA/apg/patterns/table/"
+ },
+ schema: []
+ },
+ create(context) {
+ return {
+ JSXElement(node: TSESTree.JSXElement) {
+ const openingElement = node.openingElement;
+
+ // if it is not a Table, return
+ if (elementType(openingElement as JSXOpeningElement) !== "Table") {
+ return;
+ }
+
+ // Check if Table is wrapped in a Field component
+ const hasFieldParentCheck = hasFieldParent(context);
+
+ // Check for caption child element with meaningful content
+ const hasValidCaption = node.children.some(child => {
+ if (child.type === "JSXElement" && elementType(child.openingElement as JSXOpeningElement) === "caption") {
+ // Check if caption has text content
+ return hasTextContentChild(child);
+ }
+ return false;
+ });
+
+ // Check for ARIA labeling
+ const hasAriaLabel = hasNonEmptyProp(openingElement.attributes, "aria-label");
+ const hasAriaLabelledBy = hasAssociatedLabelViaAriaLabelledBy(openingElement, context);
+
+ // Table can be labeled via multiple methods
+ const hasAccessibleName = hasFieldParentCheck || hasValidCaption || hasAriaLabel || hasAriaLabelledBy;
+
+ if (!hasAccessibleName) {
+ context.report({
+ node: openingElement,
+ messageId: "missingTableLabelling"
+ });
+ }
+ }
+ };
+ }
+});
+
+export default rule;
diff --git a/lib/rules/tree-needs-labelling.ts b/lib/rules/tree-needs-labelling.ts
new file mode 100644
index 0000000..e91f2df
--- /dev/null
+++ b/lib/rules/tree-needs-labelling.ts
@@ -0,0 +1,82 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
+import { elementType } from "jsx-ast-utils";
+import { hasNonEmptyProp } from "../util/hasNonEmptyProp";
+import { hasAssociatedLabelViaAriaLabelledBy } from "../util/labelUtils";
+import { hasFieldParent } from "../util/hasFieldParent";
+import { JSXOpeningElement } from "estree-jsx";
+
+//------------------------------------------------------------------------------
+// Rule Definition
+//------------------------------------------------------------------------------
+
+const rule = ESLintUtils.RuleCreator.withoutDocs({
+ defaultOptions: [],
+ meta: {
+ messages: {
+ missingTreeLabelling:
+ "Accessibility: Tree must have an accessible name via aria-label, aria-labelledby, or Field wrapper to describe the tree's purpose",
+ missingTreeItems: "Accessibility: Tree should contain TreeItem elements for proper hierarchical structure"
+ },
+ type: "problem",
+ docs: {
+ description: "Accessibility: Tree must have proper labelling and follow ARIA tree pattern for hierarchical navigation",
+ recommended: "strict",
+ url: "https://www.w3.org/WAI/ARIA/apg/patterns/treeview/"
+ },
+ schema: []
+ },
+ create(context) {
+ return {
+ JSXElement(node: TSESTree.JSXElement) {
+ const openingElement = node.openingElement;
+
+ // if it is not a Tree, return
+ if (elementType(openingElement as JSXOpeningElement) !== "Tree") {
+ return;
+ }
+
+ // Check if this Tree is nested inside another Tree structure
+ const isNestedTree = context.getAncestors().some(ancestor => {
+ if (ancestor.type === "JSXElement") {
+ const ancestorElementType = elementType(ancestor.openingElement as JSXOpeningElement);
+ return ancestorElementType === "Tree" || ancestorElementType === "TreeItem";
+ }
+ return false;
+ });
+
+ // Check if Tree is wrapped in a Field component
+ const hasFieldParentCheck = hasFieldParent(context);
+
+ // Check for ARIA labeling
+ const hasAriaLabel = hasNonEmptyProp(openingElement.attributes, "aria-label");
+ const hasAriaLabelledBy = hasAssociatedLabelViaAriaLabelledBy(openingElement, context);
+
+ // Tree accessibility requirements
+ const hasAccessibleName = hasFieldParentCheck || hasAriaLabel || hasAriaLabelledBy;
+
+ // Only require labeling for top-level trees (not nested ones)
+ if (!hasAccessibleName && !isNestedTree) {
+ context.report({
+ node: openingElement,
+ messageId: "missingTreeLabelling"
+ });
+ }
+
+ // Optional: Check for TreeItem children (proper tree structure)
+ const hasTreeItems = node.children.some(
+ child => child.type === "JSXElement" && elementType(child.openingElement as JSXOpeningElement) === "TreeItem"
+ );
+
+ // Trees should contain TreeItems for proper navigation
+ if (hasAccessibleName && !hasTreeItems && node.children.length > 0) {
+ // context.report({ node: openingElement, messageId: "missingTreeItems" });
+ }
+ }
+ };
+ }
+});
+
+export default rule;
diff --git a/tests/lib/rules/datagrid-needs-labelling.test.ts b/tests/lib/rules/datagrid-needs-labelling.test.ts
new file mode 100644
index 0000000..ba9964f
--- /dev/null
+++ b/tests/lib/rules/datagrid-needs-labelling.test.ts
@@ -0,0 +1,88 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+import { Rule } from "eslint";
+import ruleTester from "./helper/ruleTester";
+import rule from "../../../lib/rules/datagrid-needs-labelling";
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+ruleTester.run("datagrid-needs-labelling", rule as unknown as Rule.RuleModule, {
+ valid: [
+ // DataGrid with aria-label
+ ``,
+
+ // DataGrid with aria-labelledby that references existing element
+ `<>>`,
+
+ // DataGrid wrapped in Field (provides labeling context)
+ `
+
+ `,
+
+ // DataGrid with comprehensive labeling and ARIA attributes
+ ``,
+
+ // DataGrid with complex aria-labelledby
+ `<>