"Table header cell has assigned cells" [d0f69e]: UA/AT seems to do more association than HTML specs.
<table>
<tr> <td>1</td> <th>Foo</th> </tr>
<tr> <td>2</td> <th>Bar</th> </tr>
</table>
As per the HTML specs, the headers have no assigned cells because The internal scanning algorithm, step 3/no header/3-4 uses Δx/Δy of 0/-1, i.e. only look on the left or above of the td in order to find th. But here the header is on the right.
Of course, adding a headers attribute fixes this.
However, a colleague ran quick tests on a table like this, and it seems that ATs are actually announcing the headers when navigating the table. (at least in some UA/AT combination). This means that this is not an actual problem and should not be flagged. This also probably means that AT are doing some other sort of magic than just using the HTML table building spec 😖
Summoning @ajanec01 since you are working on this rule and may have some insights on this 🤔
Hey @Jym77
As for your above example, the row header looks like a row header but it is not exposed as a row header when using VO/Safari. I think it is still worth adhering to the spec as it is a popular combination. However, I should include that in the accessibility support section. What do you think?
The reason why I haven't made much progress on #1971 (apart from some personal constraints) is that I don't fully understand the way in which the spec formulates those relationships. Hopefully this is a good place to discuss this so that I could move forwards.
I somewhat agree with the above interpretation but wonder how do you know that the scanning for column headers starts from the top and goes down, as in which part of the spec says that? Δx=−1 looks to the left but Δy=−1 looks below not above as you mentioned. It would work providing that the scanning for row headers starts from the principal cell (the cell that queries for headers) but scanning for column headers starts from the top of the table and moves down to the principal cell (seems logical but I don't think the spec confirms that).
As for your above example, the row header looks like a row header but it is not exposed as a row header when using VO/Safari. I think it is still worth adhering to the spec as it is a popular combination. However, I should include that in the accessibility support section. What do you think?
Yes, that would work.
The reason why I haven't made much progress on #1971 (apart from some personal constraints) is that I don't fully understand the way in which the spec formulates those relationships. Hopefully this is a good place to discuss this so that I could move forwards.
This algo is a mess 😖 I know it 'coz I've implemented it 😂 Here's trying to run down the main steps commenting what I understand…
The algorithm for assigning header cells to a cell principal cell is as follows.
This means that principal cell is a data cell and we're looking for its headers. (well, not really because headers cell can also be assigned to other headers cells, but for data cells, we look from the data cell and search for headers).
If the principal cell has a headers attribute specified.
Skipping the bit about headers attribute, if there is one, use it.
If principal cell does not have a headers attribute specified For each value of y from principaly to principaly+principalheight-1, run the internal algorithm for scanning and assigning header cells, with the principal cell, the header list, the initial coordinate (principalx,y), and the increments Δx=−1 and Δy=0. For each value of x from principalx to principalx+principalwidth-1, run the internal algorithm for scanning and assigning header cells, with the principal cell, the header list, the initial coordinate (x,principaly), and the increments Δx=0 and Δy=−1.
Now, this is an interesting bit. The "for" parts on both x and y is just here to look at all the rows and columns occupied by the cell (for big cells). So, if a (data) cell spans 5 columns (principalwidth = 5), we look for headers in each of these columns separately.
This also says that for each y (row), we look with Δx=-1 (to the left of the principal cell), and for each x (column), we look with Δy=-1 (to the top).
If the principal cell is anchored in a row group Remove all the empty cells from the header list.
Skipping groups and the rest of this algo.
The internal algorithm for scanning and assigning header cells, given a principal cell, a header list, an initial coordinate (initialx, initialy), and Δx and Δy increments, is as follows:
Now comes the internal scanning algorithm.
Let x equal initialx. Let y equal initialy.
Let opaque headers be an empty list of cells.
If principal cell is a header cell Let in header block be true, and let headers from current header block be a list of cells containing just the principal cell.
Otherwise Let in header block be false and let headers from current header block be an empty list of cells.
That where we start having a mess. The end goal of these bit is to ensure we find a header for every row (or column) covered by the principal cell. The opaque headers contains previously encountered headers, to see if the newly encountered ones cover more or less row/column than the existing one. They are "opaque" because the effectively block the internal search on the row/column they cover.
Loop: Increment x by Δx; increment y by Δy.
If either x or y are less than 0, then abort this internal algorithm.
"abort" seems a bit strong, since this is just the exit condition… 🤔 But the headers list has been updated (at step 9.5), so this is fine… These algos do a lot of side effect updates 😖
If there is no cell covering slot (x, y), or if there is more than one cell covering slot (x, y), return to the substep labeled loop.
Just some error correction
Let current cell be the cell covering slot (x, y). If current cell is a header cell Set in header block to true. Add current cell to headers from current header block. Let blocked be false. If Δx is 0
So Δy=-1 and we are going up the column to look for headers.
If there are any cells in the opaque headers list anchored with the same x-coordinate as the current cell, and with the same width as current cell, then let blocked be true.
So, here we look if there is a previously encountered header, from a previous header block (the opaque headers) that covers exactly the same columns (x-coordinates, same anchor and width) as the one we currently look at. If so, this current header is "invisible" from the principal cell because the view is "blocked" by the previously encountered "opaque" header. Note that this doesn't look if the combination of existing headers cover the current one, but if a single existing header covers it.
If the current cell is not a column header, then let blocked be true.
If we're hitting a row header (or other) while looking for column header, just skip it.
If Δy is 0
Symmetrical.
If blocked is false, then add the current cell to the headers list.
If current cell is a data cell and in header block is true Set in header block to false. Add all the cells in headers from current header block to the opaque headers list, and empty the headers from current header block list.
This is the first data cell after exiting a "header block", so we dump all the headers found so far into the opaque list.
Return to the step labeled loop.
So, on 1×1 cells, the stuff is much simpler…
| H1 | D1 | H2 | H3 | D2 |
|---|
- We see H3, add it to headers while remembering we are in a header block, and also add it to the headers from current block.
- We see H2, it is similarly added to the global-ish headers and to the headers from current block.
- We see D1, this is a data cell, so we exit the header block and dump the [H2, H3] list into the opaque list. Now, any header matching exactly these row won't be seen from D2.
- We see H1, but since the opaque list already contains headers that cover the exact same row(s), we skip it.
Things get trickier with spanning cells…
| H1 | D1.1 | H2.1 | D2.1 |
|---|---|---|---|
| D1.2 | H2.2 | D2.2 |
- Now, when looking for D2.1 and encountering H1, the opaque list only contains H2.1 which does not cover as many rows as H1 does, so H1 should also be a header for D2.1. (this is essentially the case with the double column in the first example)
- If H2.1 and H2.2 would be merged, they would be opaque and neither D2.x would see H1.
- If D2.1 and D2.2 would be merged, it would see both H2.1, H2.2, and H1. Even though H2.1+H2.2 completely cover H1, none of them covers it exactly, so none of them is opaque enough (alone) to block the view…
And things get super complicated when there are headers cells that partially overlap each other… I'd say that then there are other problems in the table 🙃
Thanks @Jym77!
Just to clarify again. You mentioned Δy=-1 in two places.
for each x (column), we look with Δy=-1 (to the top).
So Δy=-1 and we are going up the column to look for headers.
This is the most counter intuitive thing for me. Lower Y values look down and not up, meaning that if we incrementally decrease the y values because of Δy=-1 the looping would have to start from the top cell covering the x coordinates of the principal cell instead of starting from the principal cell itself.
It's exactly what I don't get. I can rightly assume that Δx=-1 moves to the left on the x values. I think that the spec is indicating that the scanning for headers starts from the principal cell but as I understand it, in order to check for column headers and move up the block with the x coordinatesincrementally it would have to be Δy=+1.
Ah, no! the cells are numbered from the top left corner, not bottom left 🙃
That kind of makes sense because the "first row" of the table is the top one, not the bottom one (in reading order), so it is the one with y=0. That's implicit when forming a table which starts with y=0 (steps 1 and then again 10 and 1 when processing rows groups, this is again more complicated than needed because it needs to handle cells spanning multiple rows); and then increases it (step 1.2 for ending rows groups, then used in step 12 for processing rows).
Thanks @Jym77 I see the point and it makes sense that way. My main issue is that I still don't see a clear confirmation of it in the spec. I understand the forming a table steps as the following in respect to the Y axis:
- Initially the width and height of the table are 0 (point 1 and 2),
- The process starts from the first element within the table and moves according to the order of elements as they appear in the DOM (step 7),
- If, for example, the
tris the first element (the current element), then its y is 0 to start with (step 10. - The overall Y increases by at least the height of the current element (step 1 of the algorithm for processing rows).
It's clear that the table is being built from top down but it does not necessarily mean that once the UA finished building the table the Y axis needs to be interpreted as positive numbers go down from 0. I can't see any confirmation of that in the spec either. It feels like once the table is formed the Y axis is just like a normal Y axis, meaning that the positive numbers grow upward and not downward. Hence, Δy=-1 when looping means looking at cells below, those with lower y values, and not up.
All in all, it is very clear that a cell is looking at cells to the left and top to find headers but to me Δy=-1 makes sense if the scanning starts from the top of the table and not the current cell.
BTW, thanks for that :exploding_head: ... :stuck_out_tongue_closed_eyes:
It's clear that the table is being built from top down but it does not necessarily mean that once the UA finished building the table the Y axis needs to be interpreted as positive numbers go down from 0. I can't see any confirmation of that in the spec either. It feels like once the table is formed the Y axis is just like a normal Y axis, meaning that the positive numbers grow upward and not downward. Hence, Δy=-1 when looping means looking at cells below, those with lower y values, and not up.
As long as we stay within the table model described by the HTML spec, y=0 is the top row (because it never says to inverse that). The algorithm for associating headers is part of the HTML spec, moreover it is called by the table building algorithm at step 13 of the algorithm for processing rows (which means it's actually called on a partially formed table where the rows below haven't been processed yet).
Yes, it does make sense. I got too hooked up on the convention but then when looking at drawing on <canvas> the higher y values also offset the drawing downwards and not upwards.
Thanks @Jym77