Data grid / foldkit-mixins-data-grid
What it draws
root role="grid", tabindex 0, aria-rowcount, aria-colcount, aria-activedescendant
header role="rowgroup", sticky at the top
headerRow role="row" aria-rowindex="1"
headerCell role="columnheader" aria-colindex
resizeHandle role="separator", its width as aria-valuenow
body role="rowgroup", the full height of every row
row role="row" aria-rowindex (2 for the first data row)
cell role="gridcell" aria-colindex, id for the active descendant
placeholder a row counted but not loaded yet
status "No rows." when there are none- Counts and indexes are logical.
aria-rowcountis every row plus the header, or-1when the count is unknown;aria-rowindexandaria-colindexare a cell's place among all rows and visible columns, so a screen reader says "row 5,002" of a window that holds twenty. - One tab stop. DOM focus stays on
root, andaria-activedescendantnames the current cell while it is drawn. A cell scrolled out of the window is gone from the DOM, so the attribute is left off until it is back. - Keys are
GridFocus.target's: arrows, Home and End, Ctrl for the corners, PageUp and PageDown by the rows the viewport holds. A key the grid does not handle (Enter, Tab, anything with Shift, Alt or Meta) keeps its default. - Resize handles sit at each resizable column's end edge. A pointer drag
resizes from where it began, and a cancelled pointer (
pointercancel, or a lost capture) puts the width back; a focused handle steps 16px with the arrow keys, its own keys only, so the grid's focus stays put. Handles are out of the tab order, so the grid stays one tab stop. - Selection, when the grid has it: rows say
aria-selectedwithrowSelection, cells in the range say it withcellSelection, and the grid isaria-multiselectablefor either kind of several. The pointer goes through one listener on the body: a click focuses a cell, Shift with a click spans a range from the focused cell, and Ctrl or Meta with a click toggles the cell's row. On the keyboard, Shift with an arrow, a page, Home or End moves a range's far corner (scrolling it into view), Space selects the focused row and Shift+Space extends the rows to it, Ctrl or Meta with A selects every row (or every cell), and Escape lets a range go. - Undo and redo keys. Ctrl or Meta with Z sends
UndoRequested, with Shift (or Ctrl+Y)RedoRequested; the application keeps the history. Keys in an open editor stay the input's. - Fill, in a grid that edits. Ctrl or Meta with D carries the range's
first row down it (a lone cell or one row, the row above), with R its
first column across it. The
fillHandleSlot, a square at the end corner of the range or the focused cell, is dragged to fill along one axis; the cells it would write saydata-fill="target"until it is let go, and Escape lets it go back. The handle is pointer-only and hidden from assistive technology, since the keys do the same. Held near the grid's edge or past it, the drag scrolls the grid that way and fills on into the rows it brings in. - The header row is part of the grid. ArrowUp from the first row goes up
to the header (
aria-activedescendantnames the header cell), the arrows and Home and End walk it, ArrowDown or Escape go back to the row focus came from. On a header, Shift with an arrow resizes the column 16px, and Ctrl or Meta with Shift and an arrow moves it within its region; both are mirrored in right-to-left text. - The editor follows the column's schema (
Grid.editorFor): a schema whose text is one of a few literals (Schema.Literals) is a choice of them: arole="combobox"showing the draft (thechoiceslot) beside itsrole="listbox"(choiceList,choiceOption), drawn by the grid and styled with the page. It opens on the cell's value, below the cell or above it when the cell is low in the view; the arrows, Home, End and the page keys walk it, a letter finds the next option it starts, Enter and Tab keep the draft and move on, Alt+ArrowUp keeps it in place, Escape drops it, and a press on an option chooses it. WithchoiceEditor: 'native'it is the platform'sselect, whose picker suits a touch screen, its list open at once where the browser allows (showPicker). One that decodes to a number is a text field with a decimal keypad on touch; anything else is text (editor). - Editing, for a column with
edit: Enter, F2 or a double-click opens a text field in the cell on its text, and a printable key opens it on that character (EditTyped); keys typed before the field is drawn add to it, and Escape before then cancels it. In the field, Enter commits and moves down (Shift+Enter up), Tab commits and moves across (Shift+Tab back), and Escape cancels; the arrows are the field's own. Focus leaving the field (a click elsewhere) commits it, as Enter does without moving. A draft the column'sschemarefuses stays open witharia-invalid, and its error is shown below the field (editorError,role="alert", which the field names witharia-describedby), until the draft is fixed or Escape drops it. Focus comes back to the grid when the field goes. Clicking another cell commits first. In a grid where some columns edit, the others' headers and cells carryaria-readonly, whichGridStyledraws as a padlock before the header's label and muted text in the cells (a grid where nothing edits marks nothing). An editable cell carriesdata-editable(GridStylegives it a text cursor), and the cell being editeddata-editing, whichGridStylelifts with a shadow and hands its padding to the field, outlined in the focus colour with a caret of it, so an edit does not look like a cell that only has focus. - The clipboard works on the range, or the focused cell. Copy puts it on
the clipboard as tab-separated text, as spreadsheets read it; paste lays
text from the range's corner onto editable cells and the application
hears one
Out.Pasted; cut copies and clears the editable cells, and Delete or Backspace clears them without copying. While a cell is edited, the field has the clipboard. - Where the rows stand, from
status(GridCrud.statusfor a Remote page): the grid isaria-busywhile loading or refreshing; with no rows it says loading, or the failure with a retry whenonRetryis given; with rows, a failure is said in thefooter, below them.onMoreputs a More button in the footer while the count is not known. AttachMoreOnScroll(DataGridView<Message>().define(Grid).pipe(MoreOnScroll)) and the button also sends it when it comes within 200px of the grid's visible box, and again after each load while it stays there. Nothing is asked while the grid is busy, so givestatustoo: without it, the button asks again each time it comes back into view before the page lands. - The column menu, with
columnMenu: true: each header has a button (menuButton,aria-haspopup="menu") that opens arole="menu"below it, listingGrid.menuItems: pin to the start or the end or unpin, hide, and show each hidden column. On a focused header Alt+ArrowDown, Shift+F10 or the menu key opens it. The menu takes focus and points at its active item witharia-activedescendant; the arrows, Home and End walk it, Enter or Space or a click runs an item, and Escape, a choice, or focus leaving closes it, focus going back to the grid unless the user sent it elsewhere. Its words aremenu,pinStart,pinEnd,unpin,hideandshowinwords. - Marks are the application's states on single cells, such as an edit
not yet saved:
marks: address => Option.some({ name, description }). The name is drawn asdata-markon the cell's Slot, which the default style gives a dot in the corner and an application styles by name (&[data-mark="refused"]in its ownStyle.forSlots(GridSlots)), and the description is the cell'saria-description, so a screen reader says what the dot shows. It is asked only for the cells drawn. Five names are shared, for a grid over a server (GridMarks:pending,saved,refused,replaced,peer):GridMarkStyle, attached afterGridStyle, draws them on the theme's tokens, a shape for each, not only a colour (and an outline where colours are forced), with a peer's colour from--fk-grid-peerwhen the application sets one.GridLegend<Message>()draws what they mean, each beside a swatchGridLegendStylepaints with the cells' own rules, so the two cannot disagree. - Sorting is the application's:
sortgives a column its direction and the Message that sorts it next (foldkit-crud'sSorthas this shape). The header saysaria-sort, and its label is a button the pointer sorts with, markeddata-sort(ascordesc) while sorted, whichGridStyledraws as a small chevron after the label; its place is kept while unsorted, so sorting moves nothing, and a header label never wraps. Enter on the focused header sends it too.GridStyleparts columns with a faint line, which on a header is the resize handle, darker under the pointer. - Pinned columns stick to their edge with
position: stickyand carrydata-pinned="start"or"end"; the pinned column next to the scrolling ones also carriesdata-pinned-edge, header and cells, whereGridStyledraws the divider they pass under. The focused cell carriesdata-focused="true".
Styling
Every element above is a Slot. A Style may paint anything, but the geometry
the window is worked out from is the view's: a Style that sets a cell's
width, position, insetInlineStart or boxSizing, a row's height, or
the root's overflow is refused with slot "cell" protects style property.
Cells are border-box, so padding stays inside the width the window assumed.