|
| 1 | +import { Meta } from '@storybook/addon-docs/blocks'; |
| 2 | +import Mermaid from '../_internal/Mermaid'; |
| 3 | + |
| 4 | +<Meta title="NHS/Content/Table/SimpleSortableTable"/> |
| 5 | + |
| 6 | +# SortableTable Component Lifecycle Research |
| 7 | + |
| 8 | +This documentation provides detailed analysis of sortable table component patterns, |
| 9 | +based on research from MOJ Frontend's SortableTable implementation. |
| 10 | + |
| 11 | +## Component Lifecycle Diagram |
| 12 | + |
| 13 | +The following diagram illustrates the complete lifecycle of a sortable table component, |
| 14 | +from initialisation through user interaction and DOM updates. |
| 15 | + |
| 16 | +<Mermaid className="is-airy">{` |
| 17 | +--- |
| 18 | +title: SortableTable Component Lifecycle |
| 19 | +--- |
| 20 | +flowchart TD |
| 21 | + Start([Component Instantiation]) --> CheckElements{Check thead<br/>& tbody exist?} |
| 22 | + |
| 23 | + CheckElements -->|No| EarlyReturn([Early Return]) |
| 24 | + CheckElements -->|Yes| StoreRefs[Store DOM References<br/>$head, $body, $caption] |
| 25 | + |
| 26 | + StoreRefs --> DefineIcons[Define SVG Icons<br/>↑ ↓ ↕] |
| 27 | + DefineIcons --> FindHeadings[Query all th elements<br/>in thead] |
| 28 | + |
| 29 | + FindHeadings --> Phase1[Initialisation] |
| 30 | + |
| 31 | + Phase1 --> CreateButtons[createHeadingButtons] |
| 32 | + CreateButtons --> IterateHeadings{For each heading<br/>with aria-sort} |
| 33 | + IterateHeadings --> WrapButton[Wrap text content<br/>in button element<br/>data-index=column] |
| 34 | + WrapButton --> IterateHeadings |
| 35 | + |
| 36 | + IterateHeadings --> UpdateCaption[updateCaption] |
| 37 | + UpdateCaption --> CheckCaption{Caption exists?} |
| 38 | + CheckCaption -->|Yes| AddAssistive[Add visually-hidden<br/>assistive text about<br/>sortable columns] |
| 39 | + CheckCaption -->|No| UpdateIndicators |
| 40 | + AddAssistive --> UpdateIndicators |
| 41 | + |
| 42 | + UpdateIndicators[updateDirectionIndicators] --> SyncIcons[Sync SVG icons with<br/>current aria-sort state<br/>ascending→↑<br/>descending→↓<br/>none→↕] |
| 43 | + |
| 44 | + SyncIcons --> CreateStatus[createStatusBox] |
| 45 | + CreateStatus --> AddLiveRegion[Insert aria-live region<br/>after table for<br/>screen reader updates] |
| 46 | + |
| 47 | + AddLiveRegion --> InitSorted[initialiseSortedColumn] |
| 48 | + InitSorted --> CheckPreSort{Find column with<br/>aria-sort=ascending<br/>or descending?} |
| 49 | + CheckPreSort -->|Yes| DoInitialSort[Sort rows by<br/>that column] |
| 50 | + CheckPreSort -->|No| AttachListener |
| 51 | + DoInitialSort --> ApplyRows[Replace tbody rows<br/>with sorted order] |
| 52 | + ApplyRows --> AttachListener |
| 53 | + |
| 54 | + AttachListener[Attach click listener<br/>to thead] --> Ready([Ready State]) |
| 55 | + |
| 56 | + Ready --> UserClick([User Clicks<br/>Sort Button]) |
| 57 | + |
| 58 | + UserClick --> HandleClick[onSortButtonClick] |
| 59 | + HandleClick --> FindButton{Find button<br/>in event target?} |
| 60 | + FindButton -->|No| Ready |
| 61 | + FindButton -->|Yes| GetState[Get current<br/>aria-sort state] |
| 62 | + |
| 63 | + GetState --> CalcDirection{Current state?} |
| 64 | + CalcDirection -->|none or descending| SetAsc[Set direction:<br/>ascending] |
| 65 | + CalcDirection -->|ascending| SetDesc[Set direction:<br/>descending] |
| 66 | + |
| 67 | + SetAsc --> GetRows[getTableRowsArray<br/>Get all tbody rows] |
| 68 | + SetDesc --> GetRows |
| 69 | + |
| 70 | + GetRows --> SortRows[sort rows, columnNumber, direction] |
| 71 | + |
| 72 | + SortRows --> ExtractValues[For each row pair<br/>extract cell values] |
| 73 | + ExtractValues --> CheckDataAttr{Has data-sort-value<br/>attribute?} |
| 74 | + CheckDataAttr -->|Yes| UseDataAttr[Use attribute value] |
| 75 | + CheckDataAttr -->|No| UseInner[Use innerHTML] |
| 76 | + UseDataAttr --> IsNumber{Value is<br/>finite number?} |
| 77 | + UseInner --> IsNumber |
| 78 | + |
| 79 | + IsNumber -->|Yes| NumericCompare[Numeric subtraction<br/>valueA - valueB] |
| 80 | + IsNumber -->|No| StringCompare[String localeCompare<br/>valueA.localeCompare valueB] |
| 81 | + |
| 82 | + NumericCompare --> SwapOrder{Sort direction?} |
| 83 | + StringCompare --> SwapOrder |
| 84 | + SwapOrder -->|descending| ReverseAB[Swap A and B] |
| 85 | + SwapOrder -->|ascending| KeepAB[Keep A and B] |
| 86 | + |
| 87 | + ReverseAB --> SortComplete |
| 88 | + KeepAB --> SortComplete[Array.sort complete] |
| 89 | + |
| 90 | + SortComplete --> AddSorted[addRows<br/>Replace tbody content<br/>with sorted rows] |
| 91 | + |
| 92 | + AddSorted --> ClearStates[removeButtonStates<br/>Set all columns<br/>aria-sort=none] |
| 93 | + |
| 94 | + ClearStates --> UpdateButton[updateButtonState<br/>Set clicked column<br/>to new direction] |
| 95 | + |
| 96 | + UpdateButton --> AnnounceSort[Update aria-live status<br/>Sort by %heading% %direction%] |
| 97 | + |
| 98 | + AnnounceSort --> RefreshIcons[updateDirectionIndicators<br/>Update all SVG icons] |
| 99 | + |
| 100 | + RefreshIcons --> Ready |
| 101 | + |
| 102 | + class Start lifecycle-start |
| 103 | + class Ready lifecycle-ready |
| 104 | + class EarlyReturn lifecycle-error |
| 105 | + class Phase1 lifecycle-phase |
| 106 | + class UserClick lifecycle-user-action |
| 107 | + class SortComplete lifecycle-complete |
| 108 | + class AnnounceSort lifecycle-announce |
| 109 | + class CreateButtons,UpdateCaption,UpdateIndicators,CreateStatus,InitSorted initPhase |
| 110 | + class SortRows,ExtractValues,CheckDataAttr,IsNumber,NumericCompare,StringCompare sortPhase |
| 111 | + class AddSorted,ClearStates,UpdateButton,RefreshIcons domPhase |
| 112 | +`}</Mermaid> |
| 113 | + |
| 114 | +## Key Architectural Patterns |
| 115 | + |
| 116 | +### 1. Progressive Enhancement |
| 117 | +The SortableTable component exemplifies progressive enhancement by building JavaScript functionality on top of a fully functional HTML table that works without any scripting: |
| 118 | + |
| 119 | +- **Base Layer (HTML)**: A standard `<table>` with `<thead>` and `<tbody>` elements that displays all data in a readable tabular format. The table is fully accessible and usable even if JavaScript fails to load or is disabled. |
| 120 | + |
| 121 | +- **Enhancement Detection**: The component checks for required DOM elements (`thead` and `tbody`) before attempting any initialisation. If these elements aren't present, the component returns early without throwing errors, leaving the existing markup intact. |
| 122 | + |
| 123 | +- **Graceful Degradation**: If JavaScript is unavailable: |
| 124 | + - The table remains visible with all its data |
| 125 | + - Column headers are still readable (they just won't be sortable buttons) |
| 126 | + - Screen readers can still navigate and understand the table structure |
| 127 | + - Users can still access all information, just without the sorting convenience |
| 128 | + |
| 129 | +- **Enhancement Layer**: When JavaScript executes successfully, the component: |
| 130 | + - Wraps column header text in `<button>` elements for interaction |
| 131 | + - Adds ARIA attributes (`aria-sort`) to communicate state |
| 132 | + - Inserts visual indicators (SVG arrows) to show sort direction |
| 133 | + - Creates a live region for screen reader announcements |
| 134 | + - Enables dynamic reordering of rows based on user interaction |
| 135 | + |
| 136 | +This approach ensures that core functionality (viewing data) is never dependent on JavaScript, while the sorting interaction is treated as a convenient enhancement rather than a requirement. This is particularly important for accessibility and resilience in government services where users may have varied browser capabilities or network conditions. |
| 137 | + |
| 138 | +### 2. Accessibility First |
| 139 | +- **ARIA Live Regions**: Status box with `aria-live="polite"` announces sort changes |
| 140 | +- **Button Semantics**: Column headers become button elements when sortable |
| 141 | +- **Visual Indicators**: SVG icons synchronised with `aria-sort` state |
| 142 | +- **Screen Reader Support**: Visually-hidden caption text explains sortability |
| 143 | + |
| 144 | +### 3. Data Handling |
| 145 | +- **data-sort-value attribute**: Allows custom sort values separate from display |
| 146 | +- **Type Detection**: Automatic numeric vs string comparison |
| 147 | +- **Locale-Aware**: Uses `localeCompare()` for string sorting |
| 148 | + |
| 149 | +### 4. State Management |
| 150 | +- Single source of truth: `aria-sort` attribute on column headers |
| 151 | +- State flows: none → ascending → descending → ascending... |
| 152 | +- Only one column can have active sort state |
| 153 | + |
| 154 | +### 5. Event Delegation |
| 155 | +- Single click listener on thead (not per-button) |
| 156 | +- Event bubbling used for scalability |
| 157 | +- Closest() selector for target identification |
| 158 | + |
| 159 | +## Performance Considerations |
| 160 | + |
| 161 | +### DOM Manipulation Strategy |
| 162 | + |
| 163 | +1. **Batch Updates**: All row removals/additions happen together |
| 164 | +2. **Native Sort**: Leverages Array.prototype.sort for efficiency |
| 165 | +3. **Minimal Reflows**: Icon updates use `insertAdjacentHTML` |
| 166 | + |
| 167 | +### Potential Optimisations |
| 168 | + |
| 169 | +- Virtual scrolling for large datasets |
| 170 | +- Debouncing rapid clicks |
| 171 | +- Web Worker for sorting large datasets |
| 172 | +- Memoization of cell values |
| 173 | + |
| 174 | +## Comparison with AriaTabsDataGrid |
| 175 | + |
| 176 | +| Feature | MOJ SortableTable | AriaTabsDataGrid | |
| 177 | +|---------|-------------------|-------------------| |
| 178 | +| Framework | Vanilla JS | React | |
| 179 | +| ARIA Pattern | Table with sortable columns | Tabs + Grid composite | |
| 180 | +| State Management | DOM attributes | React state/hooks | |
| 181 | +| Sorting | Client-side DOM manipulation | Data transformation | |
| 182 | +| Accessibility | Manual ARIA + live regions | React Aria components | |
| 183 | +| Progressive Enhancement | Yes (works without JS) | Requires JavaScript | |
| 184 | +| Use Case | Simple sortable tables | Complex multi-panel data views | |
| 185 | + |
| 186 | +## Implementation Notes |
| 187 | + |
| 188 | +### Critical Success Factors |
| 189 | +1. **Initialise early**: Set up all ARIA attributes and buttons before user interaction |
| 190 | +2. **Maintain sync**: Keep visual indicators, ARIA state, and DOM order consistent |
| 191 | +3. **Announce changes**: Use aria-live regions for screen reader feedback |
| 192 | +4. **Preserve semantics**: Table structure remains valid HTML table |
| 193 | + |
| 194 | +### Common Pitfalls |
| 195 | +- Forgetting to remove previous sort indicators |
| 196 | +- Not updating live region with meaningful messages |
| 197 | +- Missing data-sort-value for formatted numbers (e.g., currency) |
| 198 | +- Inadequate keyboard navigation support |
| 199 | + |
| 200 | +## Testing Recommendations |
| 201 | + |
| 202 | +### Unit Tests |
| 203 | +- ✅ Button creation from sortable headers |
| 204 | +- ✅ Sort direction cycling (none → asc → desc → asc) |
| 205 | +- ✅ Numeric vs string comparison logic |
| 206 | +- ✅ data-sort-value precedence over innerHTML |
| 207 | + |
| 208 | +### Integration Tests |
| 209 | +- ✅ Full user interaction flow |
| 210 | +- ✅ ARIA live region announcements |
| 211 | +- ✅ Icon synchronisation with state |
| 212 | +- ✅ Multiple rapid clicks handling |
| 213 | + |
| 214 | +### Accessibility Tests |
| 215 | +- ✅ Screen reader announces sort changes |
| 216 | +- ✅ Keyboard navigation works correctly |
| 217 | +- ✅ Focus management after sorting |
| 218 | +- ✅ Color not sole indicator of sort state |
| 219 | + |
| 220 | +## References |
| 221 | + |
| 222 | +- **MOJ Frontend**: [SortableTable Component](https://moj-frontend.herokuapp.com/components/sortable-table) |
| 223 | +- **ARIA Authoring Practices**: [Sortable Table Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/table/) |
| 224 | +- **GOV.UK Frontend**: [ConfigurableComponent Base Class](https://frontend.design-system.service.gov.uk/js-api-reference/) |
| 225 | + |
| 226 | +## Future Enhancements |
| 227 | + |
| 228 | +1. **Multi-column Sort**: Hold shift to sort by multiple columns |
| 229 | +2. **Sort Persistence**: Remember sort preferences in sessionStorage |
| 230 | +3. **Keyboard Shortcuts**: Dedicated keys for common sort operations |
| 231 | +4. **Custom Comparators**: Plugin system for domain-specific sorting |
| 232 | +5. **Animation**: Subtle transitions when rows reorder |
| 233 | +6. **Filters**: Combine sorting with client-side filtering |
| 234 | +7. **Export**: Download sorted data as CSV |
| 235 | + |
| 236 | +--- |
| 237 | + |
| 238 | +*This research documentation is maintained as part of the NHS FDP Design System |
| 239 | +to inform future table component development and improvements.* |
0 commit comments