|
| 1 | +# Prefer 'disabledFocusable' over 'disabled' when component has loading state to maintain keyboard navigation accessibility (`@microsoft/fluentui-jsx-a11y/prefer-disabledfocusable-over-disabled`) |
| 2 | + |
| 3 | +⚠️ This rule _warns_ in the ✅ `recommended` config. |
| 4 | + |
| 5 | +🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). |
| 6 | + |
| 7 | +<!-- end auto-generated rule header --> |
| 8 | + |
| 9 | +When components are in a loading state, prefer using `disabledFocusable` over `disabled` to maintain proper keyboard navigation flow and accessibility. |
| 10 | + |
| 11 | +## Rule Details |
| 12 | + |
| 13 | +This rule encourages the use of `disabledFocusable` instead of `disabled` when components have loading state indicators. This ensures: |
| 14 | + |
| 15 | +1. **Keyboard Navigation**: The component remains in the keyboard tab order, allowing users to discover and navigate to it |
| 16 | +2. **Screen Reader Compatibility**: Screen reader users can still navigate to and understand the component's state |
| 17 | +3. **Loading State Awareness**: Users understand that the component is temporarily unavailable due to loading, not permanently disabled |
| 18 | +4. **Consistent UX**: Provides a more predictable and accessible user experience |
| 19 | + |
| 20 | +### Accessibility Impact |
| 21 | + |
| 22 | +- `disabled` removes elements completely from the tab order (tabindex="-1") |
| 23 | +- `disabledFocusable` keeps elements in the tab order while conveying disabled state via `aria-disabled="true"` |
| 24 | +- Loading states are temporary conditions where users benefit from knowing the component exists and will become available |
| 25 | + |
| 26 | +### Applicable Components |
| 27 | + |
| 28 | +This rule applies to FluentUI components that support both `disabled` and `disabledFocusable` props: |
| 29 | + |
| 30 | +**Button Components:** `Button`, `ToggleButton`, `CompoundButton`, `MenuButton`, `SplitButton` |
| 31 | +**Form Controls:** `Checkbox`, `Radio`, `Switch` |
| 32 | +**Input Components:** `Input`, `Textarea`, `Combobox`, `Dropdown`, `SpinButton`, `Slider`, `DatePicker`, `TimePicker` |
| 33 | +**Other Interactive:** `Link`, `Tab` |
| 34 | + |
| 35 | +### Loading State Indicators |
| 36 | + |
| 37 | +The rule detects these loading-related props: |
| 38 | +- `loading` |
| 39 | +- `isLoading` |
| 40 | +- `pending` |
| 41 | +- `isPending` |
| 42 | +- `busy` |
| 43 | +- `isBusy` |
| 44 | + |
| 45 | +## Examples |
| 46 | + |
| 47 | +### ❌ Incorrect |
| 48 | + |
| 49 | +```jsx |
| 50 | +<Button disabled loading>Submit</Button> |
| 51 | +<ToggleButton disabled isLoading /> |
| 52 | +<Checkbox disabled pending /> |
| 53 | +<Input disabled={true} busy={isBusy} /> |
| 54 | +<SpinButton disabled={isDisabled} loading={isSubmitting} /> |
| 55 | +<Combobox disabled pending /> |
| 56 | +``` |
| 57 | + |
| 58 | +### ✅ Correct |
| 59 | + |
| 60 | +```jsx |
| 61 | +<Button disabledFocusable loading>Submit</Button> |
| 62 | +<ToggleButton disabledFocusable isLoading /> |
| 63 | +<Checkbox disabledFocusable pending /> |
| 64 | +<Input disabledFocusable={true} busy={isBusy} /> |
| 65 | +<SpinButton disabledFocusable={isDisabled} loading={isSubmitting} /> |
| 66 | +<Combobox disabledFocusable pending /> |
| 67 | + |
| 68 | +<!-- These are acceptable since no loading state is present --> |
| 69 | +<Button disabled>Cancel</Button> |
| 70 | +<Checkbox disabled /> |
| 71 | +<Input disabled={permanentlyDisabled} /> |
| 72 | + |
| 73 | +<!-- These are acceptable since component is not disabled --> |
| 74 | +<Button loading>Submit</Button> |
| 75 | +<SpinButton isLoading /> |
| 76 | +<Input busy /> |
| 77 | +``` |
| 78 | + |
| 79 | +## Edge Cases & Considerations |
| 80 | + |
| 81 | +### Both Props Present |
| 82 | +If both `disabled` and `disabledFocusable` are present, this rule will not trigger as it represents a different configuration issue. |
| 83 | + |
| 84 | +```jsx |
| 85 | +<!-- Rule will not trigger - different concern --> |
| 86 | +<Button disabled disabledFocusable loading>Submit</Button> |
| 87 | +``` |
| 88 | + |
| 89 | +### Non-Loading Disabled States |
| 90 | +The rule only applies when both disabled AND loading states are present: |
| 91 | + |
| 92 | +```jsx |
| 93 | +<!-- ✅ Acceptable - no loading state --> |
| 94 | +<Button disabled>Permanently Disabled Action</Button> |
| 95 | +``` |
| 96 | + |
| 97 | +### Complex Expressions |
| 98 | +The rule works with boolean expressions and variables: |
| 99 | + |
| 100 | +```jsx |
| 101 | +<!-- ❌ Will trigger --> |
| 102 | +<Button disabled={!isEnabled} loading={isSubmitting}>Submit</Button> |
| 103 | + |
| 104 | +<!-- ✅ Correct --> |
| 105 | +<Button disabledFocusable={!isEnabled} loading={isSubmitting}>Submit</Button> |
| 106 | +``` |
| 107 | + |
| 108 | +## When Not To Use It |
| 109 | + |
| 110 | +You may want to disable this rule if: |
| 111 | + |
| 112 | +1. **Intentional UX Decision**: You specifically want loading components removed from tab order |
| 113 | +2. **Legacy Codebase**: Existing implementations rely on specific disabled behavior during loading |
| 114 | +3. **Custom Loading Patterns**: Your application uses non-standard loading state management |
| 115 | + |
| 116 | +However, disabling this rule is generally **not recommended** as it reduces accessibility. |
| 117 | + |
| 118 | +## Automatic Fixes |
| 119 | + |
| 120 | +The rule provides automatic fixes that replace `disabled` with `disabledFocusable` while preserving the original prop value: |
| 121 | + |
| 122 | +```jsx |
| 123 | +// Before fix |
| 124 | +<Button disabled={isSubmitting} loading>Submit</Button> |
| 125 | + |
| 126 | +// After fix |
| 127 | +<Button disabledFocusable={isSubmitting} loading>Submit</Button> |
| 128 | +``` |
| 129 | + |
| 130 | +## Related Rules |
| 131 | + |
| 132 | +- [`no-empty-buttons`](./no-empty-buttons.md) - Ensures buttons have content or accessible labeling |
| 133 | +- [`prefer-aria-over-title-attribute`](./prefer-aria-over-title-attribute.md) - Improves screen reader compatibility |
| 134 | + |
| 135 | +## Further Reading |
| 136 | + |
| 137 | +- [WAI-ARIA: Keyboard Interface](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/) |
| 138 | +- [FluentUI Accessibility Guidelines](https://react.fluentui.dev/?path=/docs/concepts-developer-accessibility--page) |
| 139 | +- [Understanding ARIA: disabled vs aria-disabled](https://css-tricks.com/making-disabled-buttons-more-inclusive/) |
0 commit comments