Data Display
Make the loaded scope, comparison relationships and actual task outcomes visible before adding density or decoration.
Choose a representation for the question
| Question | Useful representation and evidence |
|---|---|
| Which record should I open? | A named list with distinguishing context and a real link/action. Portraits, badges and order do not establish verified identity or source authority. |
| How do these records compare? | A native table with shared column meanings, units, scopes and explicit sort/selection. Keep details available separately. |
| What changed over time? | A chart with actual time/value data, units, missing-data policy and an accessible underlying table/summary. Do not draw an implied trend from arbitrary percentages. |
| How much work is complete? | A scalar progress/meter with a known denominator, measured current amount and clearly separate failure/unknown state. It is not a decorative statistic. |
Working comparison, filter and selection scope
This shared checked example is a real loaded-data task. Its total, current-page selection and result count are derived from the displayed model. Sorting changes actual records, View opens actual detail and summary creation snapshots selected IDs. Read the complete Table guide for part APIs and dimensions.
Six synthetic English-only local collections. Filtering searches their titles; this is not a language translation, source audit or remote corpus search.
Scroll this named region horizontally to compare all columns. Use Tab to reach ordinary sort, selection and View controls; the table has no spreadsheet arrow-key model.
6 of 6 loaded collections match. Page 1 of 2. 0 selected; 0 outside this page.
| Loaded entries | Action | ||
|---|---|---|---|
| Local audio preparation | 3 | ||
| Local bookmarked pages | 4 | ||
| Local draft entries | 12 | ||
| Matching loaded entries, all filtered pages | 53 | 6 collections | |
No collection opened yet.
No selection summary created yet.
Filtering, paging and sorting retain selection and opened content by stable ID. The summary button snapshots all selected loaded records, including selections outside this page. Clear selection does not erase that prior outcome. No remote save, qualified review, playback or authorization is inferred.
'use client';
import {useRef, useState, type CSSProperties} from 'react';
import {Button, Checkbox, Input, Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption, Pagination, PaginationContent, PaginationItem, PaginationButton, PaginationPrevious, PaginationNext} from '@mobtranslate/ui';
const records = [
{id:'reading', title:'Local reading notes', count:18, detail:'English-only notes for a local reading task.'},
{id:'examples', title:'Local example collection', count:7, detail:'English-only examples for comparison; no source authority is implied.'},
{id:'drafts', title:'Local draft entries', count:12, detail:'English-only drafts awaiting a separate review workflow.'},
{id:'bookmarks', title:'Local bookmarked pages', count:4, detail:'English-only bookmarks retained in this fixture.'},
{id:'audio', title:'Local audio preparation', count:3, detail:'English-only preparation notes; these are not playable audio assets.'},
{id:'checklist', title:'Local verification checklist', count:9, detail:'English-only task checks; a listed check is not proof that it passed.'},
];
const action:CSSProperties = {minHeight:44, height:'auto', minWidth:0, maxWidth:'100%', whiteSpace:'normal', overflowWrap:'anywhere'};
const collator = new Intl.Collator('en', {numeric:true});
export function TableRecordsExample(){
const [query, setQuery] = useState('');
const [descending, setDescending] = useState(false);
const [page, setPage] = useState(0);
const [selected, setSelected] = useState<Set<string>>(new Set());
const [loaded, setLoaded] = useState<string|null>(null);
const [bundle, setBundle] = useState<string[]>([]);
const [rtl, setRtl] = useState(false);
const [compact, setCompact] = useState(false);
const table = useRef<HTMLTableElement>(null);
const container = useRef<HTMLDivElement>(null);
const filtered = records.filter(record=>record.title.toLocaleLowerCase('en').includes(query.trim().toLocaleLowerCase('en')))
.sort((a,b)=>(descending?-1:1)*collator.compare(a.title,b.title));
const pages = Math.max(1, Math.ceil(filtered.length/3));
const current = Math.min(page,pages-1);
const visible = filtered.slice(current*3,current*3+3);
const visibleSelected = visible.filter(record=>selected.has(record.id)).length;
const hiddenSelected = records.filter(record=>selected.has(record.id)&&!visible.some(row=>row.id===record.id)).length;
const loadedRecord = records.find(record=>record.id===loaded);
function toggleVisible(checked:boolean){setSelected(previous=>{
const next = new Set(previous);
visible.forEach(record=>checked?next.add(record.id):next.delete(record.id));
return next;
});}
return <div data-table-lab lang="en" dir={rtl?'rtl':'ltr'} className="space-y-4">
<p dir="auto">Six synthetic English-only local collections. Filtering searches their titles; this is not a language translation, source audit or remote corpus search.</p>
<label htmlFor="table-filter" className="block">Filter loaded collection titles</label>
<Input id="table-filter" value={query} onChange={event=>{setQuery(event.target.value);setPage(0);}} />
<div className="flex flex-wrap gap-3">
<Button style={action} variant="outline" type="button" onClick={()=>{setQuery('');setPage(0);}}>Clear title filter</Button>
<Button style={action} variant="outline" type="button" aria-pressed={rtl} onClick={()=>setRtl(value=>!value)}>Use RTL table direction</Button>
<Button style={action} variant="outline" type="button" aria-pressed={compact} onClick={()=>setCompact(value=>!value)}>Use compact cell padding</Button>
<Button style={action} variant="outline" type="button" onClick={()=>{container.current?.focus();table.current?.scrollIntoView({block:'center'});}}>Focus collection table</Button>
</div>
<p dir="auto" id="table-scroll-help">Scroll this named region horizontally to compare all columns. Use Tab to reach ordinary sort, selection and View controls; the table has no spreadsheet arrow-key model.</p>
<p dir="auto" role="status" data-table-status>{filtered.length} of {records.length} loaded collections match. Page {current+1} of {pages}. {selected.size} selected; {hiddenSelected} outside this page.</p>
<Table ref={table} containerRef={container} containerProps={{role:'region',tabIndex:0,'aria-label':'Loaded local collection comparison','aria-describedby':'table-scroll-help','data-table-container':true}} className={compact?'mt-table-compact':undefined} style={{minWidth:'42rem'}}>
<TableCaption>Local collections — loaded fixture only, three rows per page</TableCaption>
<TableHeader><TableRow>
<TableHead scope="col"><Checkbox label="Select visible page" checked={visible.length>0&&visibleSelected===visible.length} indeterminate={visibleSelected>0&&visibleSelected<visible.length} disabled={visible.length===0} onCheckedChange={toggleVisible} /></TableHead>
<TableHead scope="col" aria-sort={descending?'descending':'ascending'}><Button id="table-sort" style={action} type="button" variant="ghost" onClick={()=>{setDescending(value=>!value);setPage(0);}}>Collection title {descending?'↓':'↑'}</Button></TableHead>
<TableHead scope="col" style={{textAlign:'end'}}>Loaded entries</TableHead>
<TableHead scope="col">Action</TableHead>
</TableRow></TableHeader>
<TableBody>{visible.length?visible.map(record=><TableRow key={record.id} data-record-id={record.id} data-selected={selected.has(record.id)?'':undefined}>
<TableCell><Checkbox label={`Select ${record.title}`} checked={selected.has(record.id)} onCheckedChange={checked=>setSelected(previous=>{const next=new Set(previous);if(checked)next.add(record.id);else next.delete(record.id);return next;})} /></TableCell>
<TableHead scope="row" style={{whiteSpace:'normal'}}>{record.title}</TableHead>
<TableCell style={{textAlign:'end',fontVariantNumeric:'tabular-nums'}}>{record.count}</TableCell>
<TableCell><Button style={action} type="button" variant="outline" aria-label={`View ${record.title}`} onClick={()=>setLoaded(record.id)}>View</Button></TableCell>
</TableRow>):<TableRow><TableCell colSpan={4}>No loaded titles match. Clear the title filter to restore all six local records.</TableCell></TableRow>}</TableBody>
<TableFooter><TableRow><TableHead scope="row" colSpan={2}>Matching loaded entries, all filtered pages</TableHead><TableCell data-table-total style={{textAlign:'end',fontVariantNumeric:'tabular-nums'}}>{filtered.reduce((sum,record)=>sum+record.count,0)}</TableCell><TableCell>{filtered.length} collections</TableCell></TableRow></TableFooter>
</Table>
<Pagination aria-label="Loaded collection pages"><PaginationContent style={{flexWrap:'wrap'}}>
<PaginationItem><PaginationPrevious disabled={current===0} onClick={()=>setPage(current-1)} /></PaginationItem>
{Array.from({length:pages},(_,index)=><PaginationItem key={index}><PaginationButton aria-label={`Collection page ${index+1}`} isActive={current===index} onClick={()=>setPage(index)}>{index+1}</PaginationButton></PaginationItem>)}
<PaginationItem><PaginationNext disabled={current===pages-1} onClick={()=>setPage(current+1)} /></PaginationItem>
</PaginationContent></Pagination>
<div className="flex flex-wrap gap-3"><Button style={action} type="button" disabled={!selected.size} onClick={()=>setBundle(records.filter(record=>selected.has(record.id)).map(record=>record.title))}>Create local selection summary</Button><Button style={action} variant="outline" type="button" disabled={!selected.size} onClick={()=>setSelected(new Set())}>Clear all collection selections</Button></div>
<p dir="auto" role="status" data-table-result>{loadedRecord?`${loadedRecord.title}: ${loadedRecord.detail}`:'No collection opened yet.'}</p>
<p dir="auto" data-table-bundle>{bundle.length?`Actual local summary snapshot: ${bundle.join('; ')}.`:'No selection summary created yet.'}</p>
<p dir="auto">Filtering, paging and sorting retain selection and opened content by stable ID. The summary button snapshots all selected loaded records, including selections outside this page. Clear selection does not erase that prior outcome. No remote save, qualified review, playback or authorization is inferred.</p>
</div>;
}
Define the dataset boundary
- Name what is loaded, filtered, paged and selected. “3 of 6 loaded records” does not mean “3 of all available records”; page count requires a known query result or an explicit cursor policy.
- Keep query, requested sort, returned result, selected IDs and loaded detail separate. Remote responses need request identity and a defined stale-response policy.
- Count only the values actually available for the declared aggregate. Preserve missing/not-applicable/unavailable distinctions and use units or time-zone labels where interpretation depends on them.
- Review, permission, source attribution and cultural authority are evidence-bearing fields with their own workflow. A populated row or positive badge is not proof of any of them.
Search, sorting and refinement
- State the searched fields and corpus scope. This fixture uses ordinary case-insensitive substring title filtering over six English records; it is not semantic language matching.
- Immediate local filtering can retain query focus and update a concise result status. Large/remote searches need explicit pending/failure/cancel and stale-result handling.
- Reset/clamp paging when the query or sort changes. Retain selections by stable ID, show selections outside the current page and provide clear-all independently from filter reset.
- Keep a visible empty explanation and recovery action. Do not silently widen a query or fabricate matches when none were returned.
Selection and batch task lifetime
- Choose current-page versus all-loaded versus all-query selection deliberately. Label select-all scope and mixed state; do not infer an all-corpus command from a header checkbox.
- Separate selecting records from acting on them. Preview the batch scope, reject duplicates while pending and represent partial per-record outcomes explicitly.
- Preserve failed items for correction/retry. Destructive operations require appropriate confirmation/reversal and real authorization; this local summary makes no remote write.
- An opened record may remain available when filters exclude its row. Label the relationship instead of silently switching the detail to another record.
State matrix and recovery
| Visible state | Required distinction |
|---|---|
| Pending initial result | Requested scope plus honest progress/unknown denominator; no invented rows. |
| Pending refresh | Last successful result marked stale, changed request scope and available cancel/retry policy. |
| Loaded result | Real record IDs/values, current query/page and exact loaded/total distinction. |
| Loaded empty | No corpus records versus no filter matches, with a reachable recovery action. |
| Error / offline | Transport failure versus unavailable permission; retain useful query/draft/result and state freshness. |
| Partial batch | Successful, failed and unattempted records; no blanket success from one completed row. |
Adaptation, accessibility and performance
- Keep native lists/tables and meaningful headings. Add grid semantics only when the full grid keyboard model is implemented and tested.
- Make rows scannable through readable labels, aligned comparable values, restrained density and explicit units. Use text/state controls alongside color.
- At narrow widths, use bounded table scrolling or a relationship-preserving alternative. Keep filter/recovery/paging reachable and page layout reflowing.
- Retain focused controls through updates when possible; otherwise move focus to a surviving useful target and explain the result. Avoid live announcements of the full dataset.
- Virtualization, server paging and caching are product capabilities requiring measured loaded/output boundaries. This fixture proves a six-record local task and renders supplied HTML rows.
Implementation and acceptance
Compose Table, Checkbox and Pagination around application-owned records/selection/query/result. Use containerProps/containerRef to configure the actual native scrolling surface while keeping the Table ref/attributes on the table. The checked source is compiled and exported verbatim above.
- Run test:table for actual sorting, filtering, selection, paging, details, summaries, totals, empty recovery and named scroll geometry; then verify the complete route crawl, lint/types/build and source freshness.
- Audit the intended real data source and authorization separately. DOM tests of local fixtures do not prove remote persistence, cultural review, screen-reader speech or performance for a large corpus.