diff --git a/COVERAGE.md b/COVERAGE.md
index 3bc9d4a..13a6882 100644
--- a/COVERAGE.md
+++ b/COVERAGE.md
@@ -34,7 +34,7 @@ We currently cover the following components:
- [] RadioGroup
- [] Select
- [] Slider
- - [] SpinButton
+ - [x] SpinButton
- [] Spinner
- [x] Switch
- [] Table
diff --git a/README.md b/README.md
index 1f7034e..a09acca 100644
--- a/README.md
+++ b/README.md
@@ -172,6 +172,8 @@ Any use of third-party trademarks or logos are subject to those third-party's po
| [no-empty-buttons](docs/rules/no-empty-buttons.md) | Accessibility: buttons must either text content or accessible labelling | |
| [no-empty-components-v9](docs/rules/no-empty-components-v9.md) | FluentUI components should not be empty | |
| [object-literal-button-no-missing-aria](docs/rules/object-literal-button-no-missing-aria.md) | Accessibility: Object literal image buttons must have accessible labelling: aria-label, aria-labelledby, aria-describedby | |
+| [spin-button-needs-labelling-v9](docs/rules/spin-button-needs-labelling-v9.md) | Accessibility: SpinButtons must have an accessible label | |
+| [spin-button-unrecommended-labelling-v9](docs/rules/spin-button-unrecommended-labelling-v9.md) | Accessibility: Unrecommended accessibility labelling - SpinButton | |
| [switch-needs-labelling-v9](docs/rules/switch-needs-labelling-v9.md) | Accessibility: Switch must have an accessible label | |
| [text-area-missing-label-v9](docs/rules/text-area-missing-label-v9.md) | Accessibility: Textarea must have an accessible name | |
| [text-content-button-does-not-need-aria](docs/rules/text-content-button-does-not-need-aria.md) | Accessibility: a button with text content does not need aria labelling. The button already has an accessible name and the aria-label will override the text content for screen reader users. | |
diff --git a/docs/rules/spin-button-needs-labelling-v9.md b/docs/rules/spin-button-needs-labelling-v9.md
new file mode 100644
index 0000000..f1936d6
--- /dev/null
+++ b/docs/rules/spin-button-needs-labelling-v9.md
@@ -0,0 +1,67 @@
+# Accessibility: SpinButtons must have an accessible label (`@microsoft/fluentui-jsx-a11y/spin-button-needs-labelling-v9`)
+
+
+
+All interactive elements must have an accessible name.
+
+Spin Button components need a visual label.
+
+Please add label, aria-labelledby or htmlFor.
+
+
+
+
+## Rule Details
+
+This rule aims to...
+
+Examples of **incorrect** code for this rule:
+
+```jsx
+
+```
+
+```jsx
+
+
+
+```
+
+Examples of **correct** code for this rule:
+
+```jsx
+
+```
+
+```jsx
+
+
+```
+
+```jsx
+
+
+```
diff --git a/docs/rules/spin-button-unrecommended-labelling-v9.md b/docs/rules/spin-button-unrecommended-labelling-v9.md
new file mode 100644
index 0000000..66a3b54
--- /dev/null
+++ b/docs/rules/spin-button-unrecommended-labelling-v9.md
@@ -0,0 +1,68 @@
+# Accessibility: Unrecommended accessibility labelling - SpinButton (`@microsoft/fluentui-jsx-a11y/spin-button-unrecommended-labelling-v9`)
+
+
+
+All interactive elements must have an accessible name.
+
+Spin Button components need a visual label.
+
+Using aria-label or wrapping the SpinButton in a Tooltip component is not recommended.
+
+
+
+
+## Rule Details
+
+This rule aims to...
+
+Examples of **unrecommended** code for this rule:
+
+```jsx
+
+```
+
+```jsx
+
+
+
+
+```
+
+Examples of **correct** code for this rule:
+
+```jsx
+
+```
+
+```jsx
+
+
+```
+
+```jsx
+
+
+```
diff --git a/lib/rules/index.js b/lib/rules/index.js
index 073c78c..14d0d93 100644
--- a/lib/rules/index.js
+++ b/lib/rules/index.js
@@ -18,6 +18,8 @@ module.exports = {
"toolbar-missing-aria-v9": require("./toolbar-missing-aria-v9"),
"combobox-needs-labelling-v9": require("./combobox-needs-labelling-v9"),
"no-empty-components-v9": require("./no-empty-components-v9"),
+ "spin-button-needs-labelling-v9": require("./spin-button-needs-labelling-v9"),
+ "spin-button-unrecommended-labelling-v9": require("./spin-button-unrecommended-labelling-v9"),
"breadcrumb-needs-labelling-v9": require("./breadcrumb-needs-labelling-v9"),
"dropdown-needs-labelling-v9": require("./dropdown-needs-labelling-v9"),
"tooltip-not-recommended-v9": require("./tooltip-not-recommended-v9"),
diff --git a/lib/rules/spin-button-needs-labelling-v9.js b/lib/rules/spin-button-needs-labelling-v9.js
new file mode 100644
index 0000000..b44cdc2
--- /dev/null
+++ b/lib/rules/spin-button-needs-labelling-v9.js
@@ -0,0 +1,57 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+"use strict";
+
+var elementType = require("jsx-ast-utils").elementType;
+const { hasAssociatedLabelViaAriaLabelledBy, isInsideLabelTag, hasAssociatedLabelViaHtmlFor } = require("../util/labelUtils");
+
+//------------------------------------------------------------------------------
+// Rule Definition
+//------------------------------------------------------------------------------
+
+module.exports = {
+ meta: {
+ // possible error messages for the rule
+ messages: {
+ noUnlabelledSpinButton: "Accessibility: SpinButtons must have an accessible label"
+ },
+ // "problem" means the rule is identifying code that either will cause an error or may cause a confusing behavior: https://eslint.org/docs/latest/developer-guide/working-with-rules
+ type: "problem",
+ // docs for the rule
+ docs: {
+ description: "Accessibility: SpinButtons must have an accessible label",
+ recommended: true,
+ url: "https://www.w3.org/TR/html-aria/" // URL to the documentation page for this rule
+ },
+ schema: []
+ },
+ // create (function) returns an object with methods that ESLint calls to “visit” nodes while traversing the abstract syntax tree
+ create(context) {
+ return {
+ // visitor functions for different types of nodes
+ JSXOpeningElement(node) {
+ // if it is not a SpinButton, return
+ if (elementType(node) !== "SpinButton") {
+ return;
+ }
+
+ // if the SpinButton has an associated label, return
+ if (
+ isInsideLabelTag(context) ||
+ hasAssociatedLabelViaHtmlFor(node, context) ||
+ hasAssociatedLabelViaAriaLabelledBy(node, context)
+ ) {
+ return;
+ }
+
+ // if it has no visual labelling, report error
+ context.report({
+ node,
+ messageId: `noUnlabelledSpinButton`
+ });
+ }
+ };
+ }
+};
+
diff --git a/lib/rules/spin-button-unrecommended-labelling-v9.js b/lib/rules/spin-button-unrecommended-labelling-v9.js
new file mode 100644
index 0000000..a713d83
--- /dev/null
+++ b/lib/rules/spin-button-unrecommended-labelling-v9.js
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+"use strict";
+
+const { hasNonEmptyProp } = require("../util/hasNonEmptyProp");
+const { hasToolTipParent } = require("../util/hasTooltipParent");
+var elementType = require("jsx-ast-utils").elementType;
+
+//------------------------------------------------------------------------------
+// Rule Definition
+//------------------------------------------------------------------------------
+
+module.exports = {
+ meta: {
+ // possible suggestion messages for the rule
+ messages: {
+ unRecommendedlabellingSpinButton: "Accessibility: Unrecommended accessibility labelling - SpinButton"
+ },
+ // "problem" means the rule is identifying something that could be done in a better way but no errors will occur if the code isn’t changed: https://eslint.org/docs/latest/developer-guide/working-with-rules
+ type: "suggestion",
+ // docs for the rule
+ docs: {
+ description: "Accessibility: Unrecommended accessibility labelling - SpinButton",
+ recommended: true,
+ url: "https://www.w3.org/TR/html-aria/" // URL to the documentation page for this rule
+ },
+ schema: []
+ },
+ // create (function) returns an object with methods that ESLint calls to “visit” nodes while traversing the abstract syntax tree
+ create(context) {
+ return {
+ // visitor functions for different types of nodes
+ JSXOpeningElement(node) {
+ // if it is not a SpinButton, return
+ if (elementType(node) !== "SpinButton") {
+ return;
+ }
+
+ // if the SpinButton has an aria-label or is wrapped in a Tooltip, show warning
+ if (hasNonEmptyProp(node.attributes, "aria-label") || hasToolTipParent(context)) {
+ context.report({
+ node,
+ messageId: `unRecommendedlabellingSpinButton`
+ });
+ }
+ }
+ };
+ }
+};
+
diff --git a/lib/util/labelUtils.js b/lib/util/labelUtils.js
index 4bc8112..266225f 100644
--- a/lib/util/labelUtils.js
+++ b/lib/util/labelUtils.js
@@ -80,7 +80,7 @@ function hasAssociatedLabelViaAriaLabelledBy(openingElement, context) {
}
/**
- * Determines if the element has a label assiciated with it via htmlFor
+ * Determines if the element has a label associated with it via htmlFor
* e.g.
*
*
@@ -101,3 +101,4 @@ module.exports = {
hasAssociatedLabelViaAriaLabelledBy,
hasAssociatedLabelViaHtmlFor
};
+
diff --git a/tests/lib/rules/spin-button-needs-labelling-v9.js b/tests/lib/rules/spin-button-needs-labelling-v9.js
new file mode 100644
index 0000000..6b50877
--- /dev/null
+++ b/tests/lib/rules/spin-button-needs-labelling-v9.js
@@ -0,0 +1,46 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+"use strict";
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const rule = require("../../../lib/rules/spin-button-needs-labelling-v9"),
+ RuleTester = require("eslint").RuleTester;
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester();
+ruleTester.run("spin-button-needs-labelling-v9", rule, {
+ valid: [
+ "<>>",
+ `<>>`,
+ `<>>`,
+ `<>>`,
+ `<>>`,
+ `<>>`
+ ],
+ invalid: [
+ {
+ code: ``,
+ errors: [{ messageId: "noUnlabelledSpinButton" }]
+ },
+ {
+ code: `<>>`,
+ errors: [{ messageId: "noUnlabelledSpinButton" }]
+ },
+ {
+ code: `<>>`,
+ errors: [{ messageId: "noUnlabelledSpinButton" }]
+ },
+ {
+ code: `<>>`,
+ errors: [{ messageId: "noUnlabelledSpinButton" }]
+ }
+ ]
+});
+
diff --git a/tests/lib/rules/spin-button-unrecommended-labelling-v9.js b/tests/lib/rules/spin-button-unrecommended-labelling-v9.js
new file mode 100644
index 0000000..68c0079
--- /dev/null
+++ b/tests/lib/rules/spin-button-unrecommended-labelling-v9.js
@@ -0,0 +1,31 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+"use strict";
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const rule = require("../../../lib/rules/spin-button-unrecommended-labelling-v9"),
+ RuleTester = require("eslint").RuleTester;
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester();
+ruleTester.run("spin-button-unrecommended-labelling-v9", rule, {
+ valid: [],
+ invalid: [
+ {
+ code: ``,
+ errors: [{ messageId: "unRecommendedlabellingSpinButton" }]
+ },
+ {
+ code: `<>This is a spin button>`,
+ errors: [{ messageId: "unRecommendedlabellingSpinButton" }]
+ }
+ ]
+});
+
diff --git a/tests/lib/rules/switch-needs-labelling-v9.js b/tests/lib/rules/switch-needs-labelling-v9.js
index 6bed97e..2390b6d 100644
--- a/tests/lib/rules/switch-needs-labelling-v9.js
+++ b/tests/lib/rules/switch-needs-labelling-v9.js
@@ -45,3 +45,4 @@ ruleTester.run("switch-needs-labelling-v9", rule, {
}
]
});
+