@react-x11/components 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/html/css/animation.d.ts +91 -0
  2. package/dist/html/css/animation.d.ts.map +1 -0
  3. package/dist/html/css/animation.js +430 -0
  4. package/dist/html/css/animation.js.map +1 -0
  5. package/dist/html/css/cascade.d.ts +27 -4
  6. package/dist/html/css/cascade.d.ts.map +1 -1
  7. package/dist/html/css/cascade.js +94 -6
  8. package/dist/html/css/cascade.js.map +1 -1
  9. package/dist/html/css/parse.d.ts +35 -0
  10. package/dist/html/css/parse.d.ts.map +1 -1
  11. package/dist/html/css/parse.js +115 -2
  12. package/dist/html/css/parse.js.map +1 -1
  13. package/dist/html/css/style.d.ts +5 -0
  14. package/dist/html/css/style.d.ts.map +1 -1
  15. package/dist/html/css/style.js +57 -7
  16. package/dist/html/css/style.js.map +1 -1
  17. package/dist/html/css/ua.js +12 -8
  18. package/dist/html/css/ua.js.map +1 -1
  19. package/dist/html/layout/boxes.d.ts +13 -0
  20. package/dist/html/layout/boxes.d.ts.map +1 -1
  21. package/dist/html/layout/boxes.js +28 -0
  22. package/dist/html/layout/boxes.js.map +1 -1
  23. package/dist/html/layout/flex.d.ts.map +1 -1
  24. package/dist/html/layout/flex.js +237 -67
  25. package/dist/html/layout/flex.js.map +1 -1
  26. package/dist/html/layout/inline.d.ts.map +1 -1
  27. package/dist/html/layout/inline.js +41 -11
  28. package/dist/html/layout/inline.js.map +1 -1
  29. package/dist/html/node.d.ts.map +1 -1
  30. package/dist/html/node.js +12 -0
  31. package/dist/html/node.js.map +1 -1
  32. package/package.json +1 -1
  33. package/src/html/css/animation.ts +466 -0
  34. package/src/html/css/cascade.ts +130 -7
  35. package/src/html/css/parse.ts +151 -2
  36. package/src/html/css/style.ts +69 -7
  37. package/src/html/css/ua.ts +12 -8
  38. package/src/html/layout/boxes.ts +40 -0
  39. package/src/html/layout/flex.ts +251 -75
  40. package/src/html/layout/inline.ts +43 -9
  41. package/src/html/node.ts +12 -0
@@ -252,6 +252,10 @@ export function layoutFlex(
252
252
  const direction =
253
253
  box.style.direction === 'rtl' ? Y.DIRECTION_RTL : Y.DIRECTION_LTR;
254
254
  const row = box.style.flexDirection.startsWith('row');
255
+ const baselines =
256
+ row &&
257
+ (box.style.alignItems === 'baseline' ||
258
+ items.some(({ box: child }) => child.style.alignSelf === 'baseline'));
255
259
  // An `auto` margin across a row that wraps takes the room its line has
256
260
  // past the item (CSS Flexbox 8.1, 9.6), where Yoga's pass that sets the
257
261
  // items across their lines reads an item's alignment and not its
@@ -264,6 +268,43 @@ export function layoutFlex(
264
268
  const aligned = autoAcross(child.style, reverse);
265
269
  if (aligned) node.setAlignSelf(ALIGN[aligned]);
266
270
  }
271
+ } else if (baselines) {
272
+ // Yoga takes a row aligned by baselines through that pass whether it
273
+ // wraps or not, and sets an item with an `auto` margin there by its
274
+ // baseline: at the top, where the margin puts it at the bottom, and
275
+ // every item beside it as far down as its height reached. That item
276
+ // takes no part in aligning the line by baselines (9.4 step 8), and
277
+ // Yoga is told the alignment its margins come to.
278
+ for (const { box: child, node } of items) {
279
+ const aligned = autoAcross(child.style, false);
280
+ if (aligned) node.setAlignSelf(ALIGN[aligned]);
281
+ }
282
+ }
283
+ // Items aligned by their baselines, which Yoga cannot see, are told they
284
+ // are at their lines' starts, which is where one is that shares its line
285
+ // with no other (8.3); those that do share one are lined up by their
286
+ // baselines once those are known (`baselineLines`), and the row laid out
287
+ // again. Told an item is aligned by its baseline, Yoga sizes each line
288
+ // of the row as the most any item on it reaches down to its bottom edge
289
+ // — a baseline, to Yoga — and the largest bottom margin besides,
290
+ // whichever items those are, and stretches the items on the line to
291
+ // that: a row of no height of its own was as tall as both, and
292
+ // `align-content` had that much less room to share out. And it takes a
293
+ // row that does not wrap through its pass for lines, which sets the one
294
+ // line there as tall as its items and not the row: an item centred or at
295
+ // the end of a row of a height was centred or ended in the items'
296
+ // height. Told none is, it takes neither. A row whose lines wrap in
297
+ // reverse is left to Yoga.
298
+ const started = new Set<Box>();
299
+ if (baselines && box.style.flexWrap !== 'wrap-reverse') {
300
+ for (const { box: child, node } of items) {
301
+ if (!byBaseline(box.style, child.style)) continue;
302
+ node.setAlignSelf(Y.ALIGN_FLEX_START);
303
+ started.add(child);
304
+ }
305
+ if (box.style.alignItems === 'baseline') {
306
+ root.setAlignItems(Y.ALIGN_FLEX_START);
307
+ }
267
308
  }
268
309
  // frozen at its least size on the one line of a box that does not
269
310
  // wrap, by a basis where Yoga takes one along the main axis
@@ -288,12 +329,13 @@ export function layoutFlex(
288
329
  // `align-content` spaces out come out of its layout as tall as their
289
330
  // line and the room after it as well (`unspace`): laid out so that each
290
331
  // is as tall as its line, where the box has room to space them out in —
291
- // a height, or a minimum one
332
+ // a height, or a minimum one — and with the lines at its start where it
333
+ // may be too short for them, a height or a maximum one
292
334
  const spaced =
293
335
  row &&
294
336
  bounded &&
295
337
  !hold.frozen &&
296
- (height !== null || floor > 0) &&
338
+ (height !== null || floor > 0 || ceiling < Infinity) &&
297
339
  spacesLines(box.style);
298
340
  const ask =
299
341
  restretched.length > 0
@@ -409,42 +451,33 @@ export function layoutFlex(
409
451
  }
410
452
  }
411
453
 
412
- // Yoga aligns the lines of a box that wraps, or aligns by baselines, in
413
- // one pass of its own, which drops a margin (`lineMarginFix`)
414
- const baselines =
415
- box.style.alignItems === 'baseline' ||
416
- items.some(({ box: child }) => child.style.alignSelf === 'baseline');
454
+ // Yoga aligns the lines of a box that wraps in one pass of its own, which
455
+ // drops a margin (`lineMarginFix`), and keeps the margins of one that
456
+ // does not, which it takes through that pass only where it is told an
457
+ // item is aligned by its baseline: here, never
458
+ const lined = box.style.flexWrap !== 'nowrap';
417
459
  let bottom = placeItems(
418
460
  box,
419
461
  ctx,
420
462
  items,
421
463
  row,
422
464
  own,
423
- box.style.flexWrap !== 'nowrap' || baselines,
465
+ lined,
424
466
  contentWidth,
425
- undefined,
467
+ started,
426
468
  crossed ? crossLines(box.style, items, contentWidth, columnGap) : undefined,
427
469
  );
428
- // Items aligned by their baselines, which Yoga cannot see, are aligned
429
- // with their baselines known (`baselineLines`)
430
- // by the flex layout again, set at their lines' starts with the margins
431
- // that put their baselines together, which makes each line as tall as
432
- // that does
433
- const lines = row && baselines ? baselineLines(box.style, items) : null;
470
+ // Items that share a line and are aligned by their baselines are laid
471
+ // out by the flex layout again, set at their lines' starts with the
472
+ // margins that put their baselines together (`baselineLines`), which
473
+ // makes each line as tall as that does
474
+ const lines = started.size > 1 ? baselineLines(box.style, items) : null;
434
475
  if (lines) {
435
- // every one of them, and the box's own `baseline` too, which is what
436
- // takes Yoga down that pass
437
- const started = new Set<Box>();
438
476
  for (const line of lines) {
439
- line.forEach(({ box: child, node }, i) => {
440
- node.setAlignSelf(Y.ALIGN_FLEX_START);
477
+ line.forEach(({ node }, i) => {
441
478
  node.setMargin(Y.EDGE_TOP, line.margins[i]);
442
- started.add(child);
443
479
  });
444
480
  }
445
- if (box.style.alignItems === 'baseline') {
446
- root.setAlignItems(Y.ALIGN_FLEX_START);
447
- }
448
481
  ctx.flexDepth = depth + 1;
449
482
  try {
450
483
  calculate();
@@ -457,7 +490,7 @@ export function layoutFlex(
457
490
  items,
458
491
  row,
459
492
  own,
460
- box.style.flexWrap !== 'nowrap',
493
+ lined,
461
494
  contentWidth,
462
495
  started,
463
496
  );
@@ -485,8 +518,9 @@ export function layoutFlex(
485
518
  items,
486
519
  row,
487
520
  own,
488
- box.style.flexWrap !== 'nowrap' || baselines,
521
+ lined,
489
522
  contentWidth,
523
+ started,
490
524
  );
491
525
  }
492
526
 
@@ -676,16 +710,26 @@ function placeItems(
676
710
  let bottom = 0;
677
711
  // the lines of a row that wraps in reverse are turned round to run up it
678
712
  const reverse = row && box.style.flexWrap === 'wrap-reverse';
713
+ // the left edge a column's items aligned by baselines share, where it
714
+ // does not wrap (`baselineEdge`); a column that wraps is `crossed`
715
+ const edge =
716
+ row || box.style.flexWrap !== 'nowrap'
717
+ ? NaN
718
+ : baselineEdge(box.style, items);
679
719
  for (const { box: child, node, laid } of items) {
680
720
  const placed = crossed?.get(child);
681
721
  const across =
682
722
  lined && !placed
683
- ? lineMarginFix(box.style, child, node, row, started?.has(child))
723
+ ? lineMarginFix(box.style, child, node, row, own, started?.has(child))
684
724
  : 0;
685
725
  const itemHeight = row ? heightOf(node, laid) : laid.main;
686
726
  const left =
687
727
  box.contentX +
688
- (placed ? placed.x : node.getComputedLeft() + (row ? 0 : across));
728
+ (placed
729
+ ? placed.x
730
+ : !Number.isNaN(edge) && byBaseline(box.style, child.style, false)
731
+ ? edge
732
+ : node.getComputedLeft() + (row ? 0 : across));
689
733
  // which Yoga turns an item round in by the height it laid it out at, and
690
734
  // not the one it is (`unspace`)
691
735
  const top =
@@ -896,6 +940,14 @@ function applyItem(
896
940
  if (style.flexShrink) node.setFlexShrink(style.flexShrink);
897
941
  if (style.alignSelf !== AUTO)
898
942
  node.setAlignSelf(ALIGN[style.alignSelf] ?? Y.ALIGN_AUTO);
943
+ // `stretch` sets an item whose height is a percentage of none at its
944
+ // line's start, as tall as its content (`heightOfNone`), where Yoga,
945
+ // which is handed no height for it, stretched it across the line
946
+ if (row && box.parent && heightOfNone(style, own)) {
947
+ const align =
948
+ style.alignSelf === AUTO ? box.parent.style.alignItems : style.alignSelf;
949
+ if (align === 'stretch') node.setAlignSelf(Y.ALIGN_FLEX_START);
950
+ }
899
951
 
900
952
  // Yoga's sizes are border boxes, as `box-sizing: border-box` has them;
901
953
  // a `content-box` length is the content's, and the item's padding and
@@ -2820,17 +2872,18 @@ function contentBottom(box: Box): number {
2820
2872
  * its bottom edge, so it lined them up by their bottoms. With the items laid
2821
2873
  * out, each line's, and for each the top margin that sets it at the line's
2822
2874
  * start with its first baseline where the lowest-reaching one's is — or
2823
- * null where no line has two to align.
2875
+ * null where no line has two to align. Not for a row whose lines wrap in
2876
+ * reverse, which grow from their ends and are left to Yoga (`layoutFlex`).
2824
2877
  */
2825
- function baselineLines<T extends { box: Box }>(
2878
+ function baselineLines<T extends { box: Box; node: YogaNode }>(
2826
2879
  style: ComputedStyle,
2827
2880
  items: readonly T[],
2828
2881
  ): (T[] & { margins: number[] })[] | null {
2829
- // a line that grows from its end is aligned from there, and left to Yoga
2830
- if (style.flexWrap === 'wrap-reverse') return null;
2831
2882
  const wraps = style.flexWrap !== 'nowrap';
2832
- // along a line the items go the way the main axis does, and the next
2833
- // line starts where one does not
2883
+ // along a line the items go the way the main axis does, each margin box
2884
+ // after the last one's end, and the next line starts before it (9.3): at
2885
+ // the line's start, where an item with a margin before it was further
2886
+ // along than one alone on the line before
2834
2887
  const back =
2835
2888
  (style.flexDirection === 'row-reverse') !== (style.direction === 'rtl');
2836
2889
  const out: (T[] & { margins: number[] })[] = [];
@@ -2852,28 +2905,76 @@ function baselineLines<T extends { box: Box }>(
2852
2905
  out.push(Object.assign(line, { margins }));
2853
2906
  };
2854
2907
  for (const item of items) {
2855
- const box = item.box;
2856
- if (wraps && line.length && (back ? box.x >= last : box.x <= last)) {
2857
- close();
2858
- line = [];
2859
- }
2860
- last = box.x;
2861
- const own = box.style;
2862
- const aligned = own.alignSelf === AUTO ? style.alignItems : own.alignSelf;
2863
- // an `auto` margin across the line takes it over alignment
2908
+ const { box, node } = item;
2909
+ const left = node.getComputedLeft() - node.getComputedMargin(Y.EDGE_LEFT);
2910
+ const right =
2911
+ node.getComputedLeft() +
2912
+ node.getComputedWidth() +
2913
+ node.getComputedMargin(Y.EDGE_RIGHT);
2864
2914
  if (
2865
- aligned === 'baseline' &&
2866
- own.marginTop !== AUTO &&
2867
- own.marginBottom !== AUTO
2915
+ wraps &&
2916
+ line.length &&
2917
+ (back ? right > last + 0.01 : left < last - 0.01)
2868
2918
  ) {
2869
- line.push(item);
2919
+ close();
2920
+ line = [];
2870
2921
  }
2922
+ last = back ? left : right;
2923
+ if (byBaseline(style, box.style)) line.push(item);
2871
2924
  }
2872
2925
  close();
2873
2926
  // one alone on its line is at its start already
2874
2927
  return several ? out : null;
2875
2928
  }
2876
2929
 
2930
+ /** Whether an item takes part in aligning its line by baselines (CSS
2931
+ * Flexbox 8.3, 9.4 step 8): aligned by its baseline, with no `auto`
2932
+ * margin across the line, which takes it over its alignment. */
2933
+ function byBaseline(
2934
+ container: ComputedStyle,
2935
+ item: ComputedStyle,
2936
+ /** Whether the line is a row's, across which its margins are its top
2937
+ * and bottom ones; a column's are its left and right ones. */
2938
+ row = true,
2939
+ ): boolean {
2940
+ const aligned =
2941
+ item.alignSelf === AUTO ? container.alignItems : item.alignSelf;
2942
+ return (
2943
+ aligned === 'baseline' &&
2944
+ (row ? item.marginTop : item.marginLeft) !== AUTO &&
2945
+ (row ? item.marginBottom : item.marginRight) !== AUTO
2946
+ );
2947
+ }
2948
+
2949
+ /**
2950
+ * Where the items a column that does not wrap aligns by baselines have
2951
+ * their left edges, from its content box's: NaN where it has none.
2952
+ *
2953
+ * A horizontal item has no baseline across a column, and one is made from
2954
+ * its border box — the line-under edge of the vertical writing mode it is
2955
+ * taken to have, its left, whichever way its text runs (CSS Box Alignment
2956
+ * 3, 9.1). So the items share their left edges, and are set together at
2957
+ * the line's cross start, the column's left or, where it runs right to
2958
+ * left, its right (9.3; CSS Flexbox 8.3), as Chrome sets them: as far from
2959
+ * there as the most any of them reaches past its edge on that side. Yoga
2960
+ * never aligns a column by baselines, and sets each at its line's start
2961
+ * with its margin there, so that is the furthest any of them is from the
2962
+ * start: the rightmost left edge, or from the right the leftmost.
2963
+ */
2964
+ function baselineEdge(
2965
+ container: ComputedStyle,
2966
+ items: readonly { box: Box; node: YogaNode }[],
2967
+ ): number {
2968
+ const rtl = container.direction === 'rtl';
2969
+ let edge = NaN;
2970
+ for (const { box, node } of items) {
2971
+ if (!byBaseline(container, box.style, false)) continue;
2972
+ const left = node.getComputedLeft();
2973
+ if (!(rtl ? left >= edge : left <= edge)) edge = left;
2974
+ }
2975
+ return edge;
2976
+ }
2977
+
2877
2978
  /**
2878
2979
  * Whether Yoga made a line of a row that wraps the row's height, where its
2879
2980
  * items make it another (CSS Flexbox 9.4, steps 7 and 8; `layoutFlex`).
@@ -2983,13 +3084,13 @@ function autoAcross(
2983
3084
 
2984
3085
  /**
2985
3086
  * How far an item is from where it belongs across its line, where Yoga
2986
- * (3.2.1) aligned the lines in the pass it takes for a box that wraps or
2987
- * aligns by baselines: it sets an item aligned to its line's start there
2988
- * as though it had no margin at that side, and one centred as though it had
2989
- * none at either — `items-start` in a wrapping row put every card's top
2990
- * margin under it. By the margin at the start of the line's cross axis, and
2991
- * by half what it exceeds the one at the end by, as Yoga holds them: for an
2992
- * item set at its line's start (`started`), its baseline's.
3087
+ * (3.2.1) aligned the lines in the pass it takes for a box that wraps: it
3088
+ * sets an item aligned to its line's start there as though it had no
3089
+ * margin at that side, and one centred as though it had none at either —
3090
+ * `items-start` in a wrapping row put every card's top margin under it. By
3091
+ * the margin at the start of the line's cross axis, and by half what it
3092
+ * exceeds the one at the end by, as Yoga holds them: for an item set at
3093
+ * its line's start (`started`), its baseline's.
2993
3094
  *
2994
3095
  * A row whose lines wrap in reverse Yoga lays out as though they wrapped
2995
3096
  * forward, and then turns each item over across the box, its margins with
@@ -2998,14 +3099,15 @@ function autoAcross(
2998
3099
  * stretched, is its bottom margin down from its line's top where it is its
2999
3100
  * top one; and one centred, half the difference. A column's, whose cross
3000
3101
  * axis runs right to left, or that wraps in reverse, is left as Yoga has
3001
- * it, and so is an item an `auto` margin aligns in a row that does not
3002
- * wrap.
3102
+ * it, and so is an item an `auto` margin aligns across a column.
3003
3103
  */
3004
3104
  function lineMarginFix(
3005
3105
  container: ComputedStyle,
3006
3106
  item: Box,
3007
3107
  node: YogaNode,
3008
3108
  row: boolean,
3109
+ /** Whether the flex box has a definite height. */
3110
+ own: boolean,
3009
3111
  started = false,
3010
3112
  ): number {
3011
3113
  const reverse = container.flexWrap === 'wrap-reverse';
@@ -3018,12 +3120,14 @@ function lineMarginFix(
3018
3120
  let align: string | null;
3019
3121
  if (started) align = 'flex-start';
3020
3122
  else if (lead === AUTO || trail === AUTO) {
3021
- align =
3022
- row && container.flexWrap !== 'nowrap'
3023
- ? autoAcross(style, reverse)
3024
- : null;
3123
+ align = row ? autoAcross(style, reverse) : null;
3025
3124
  } else {
3026
3125
  align = style.alignSelf === AUTO ? container.alignItems : style.alignSelf;
3126
+ // which sets one whose height is a percentage of none at its line's
3127
+ // start, as Yoga is told it does (`applyItem`)
3128
+ if (align === 'stretch' && row && heightOfNone(style, own)) {
3129
+ align = 'flex-start';
3130
+ }
3027
3131
  }
3028
3132
  const leading = node.getComputedMargin(row ? Y.EDGE_TOP : Y.EDGE_LEFT);
3029
3133
  const trailing = node.getComputedMargin(row ? Y.EDGE_BOTTOM : Y.EDGE_RIGHT);
@@ -3041,11 +3145,14 @@ function lineMarginFix(
3041
3145
 
3042
3146
  /** An item's first baseline, down from its border edge: its content's
3043
3147
  * first line's, or where it has none, or is replaced, one synthesized
3044
- * from its border box — its bottom edge. */
3148
+ * from its border box — its bottom edge. Past that edge where the line
3149
+ * is: only a scroll container's is held to its box (`firstBaselineIn`),
3150
+ * and an item shorter than its text, or whose padding puts the text
3151
+ * under it, sits on its text where it had sat on its bottom. */
3045
3152
  function itemBaseline(item: Box): number {
3046
3153
  if (item.kind !== 'replaced') {
3047
3154
  const found = firstBaselineIn(item);
3048
- if (found !== null) return Math.min(found - item.y, item.height);
3155
+ if (found !== null) return found - item.y;
3049
3156
  }
3050
3157
  return item.height;
3051
3158
  }
@@ -3197,6 +3304,16 @@ function spacesLines(style: ComputedStyle): boolean {
3197
3304
  * makes too, and is kept. Where they do, Yoga is asked again as the row
3198
3305
  * is, for where the lines and the items are, and the heights are kept from
3199
3306
  * the first layout.
3307
+ *
3308
+ * And where the lines are taller than the row, `space-around` and
3309
+ * `space-evenly` fall back to `safe center`, which sets them at the row's
3310
+ * own start, its top (CSS Box Alignment 3, 4.3 and 5.1; CSS Flexbox 8.4).
3311
+ * Yoga falls back to `flex-start` for both, which is the bottom where the
3312
+ * lines run bottom to top, and ran them out past the row's top. It sets
3313
+ * the lines of a row that wraps in reverse top to bottom and turns them
3314
+ * over once they are set, so it is asked for them at `flex-end` there,
3315
+ * which comes out at the top — where the lines it set at their start, or
3316
+ * spaced, reach past the row's bottom.
3200
3317
  */
3201
3318
  function unspace(
3202
3319
  root: YogaNode,
@@ -3206,6 +3323,16 @@ function unspace(
3206
3323
  layout: () => void,
3207
3324
  ): void {
3208
3325
  for (const { laid } of items) laid.unstretched = NaN;
3326
+ const lines = root.getAlignContent();
3327
+ // where the lines start at the row's bottom, its start is their end
3328
+ const safe =
3329
+ reverse &&
3330
+ (lines === Y.ALIGN_SPACE_AROUND || lines === Y.ALIGN_SPACE_EVENLY);
3331
+ const atEnd = (): void => {
3332
+ root.setAlignContent(Y.ALIGN_FLEX_END);
3333
+ layout();
3334
+ root.setAlignContent(lines);
3335
+ };
3209
3336
  // each item Yoga stretches: `stretch`, with no height of its own
3210
3337
  const align = root.getAlignItems();
3211
3338
  const stretched = items.filter(({ node }) => {
@@ -3216,15 +3343,24 @@ function unspace(
3216
3343
  });
3217
3344
  if (stretched.length === 0) {
3218
3345
  layout();
3346
+ // spaced where there is room, and else at their start, past the bottom
3347
+ if (
3348
+ safe &&
3349
+ linesReach(root, items, reverse) - root.getComputedHeight() > NEAR
3350
+ ) {
3351
+ atEnd();
3352
+ }
3219
3353
  return;
3220
3354
  }
3221
- const lines = root.getAlignContent();
3222
3355
  root.setAlignContent(Y.ALIGN_FLEX_START);
3223
3356
  layout();
3224
3357
  root.setAlignContent(lines);
3225
- if (!(root.getComputedHeight() - linesReach(root, items, reverse) > NEAR)) {
3358
+ const room = root.getComputedHeight() - linesReach(root, items, reverse);
3359
+ if (safe && room < -NEAR) {
3360
+ atEnd();
3226
3361
  return;
3227
3362
  }
3363
+ if (!(room > NEAR)) return;
3228
3364
  const heights = stretched.map(({ node }) => node.getComputedHeight());
3229
3365
  layout();
3230
3366
  stretched.forEach(({ node, laid }, i) => {
@@ -3276,6 +3412,22 @@ function linesReach(
3276
3412
  return reach;
3277
3413
  }
3278
3414
 
3415
+ /**
3416
+ * Whether a row item's height is a percentage of a height its flex box has
3417
+ * none of. That is `auto` to the item's size (CSS 2.1 10.5), and is not
3418
+ * handed to Yoga (`applyItem`), but it is not `auto` to its alignment:
3419
+ * `stretch` stretches an item whose height computes to `auto` (CSS Flexbox
3420
+ * 8.3), and this one's computes to the percentage. Such an item is as tall
3421
+ * as its content, set at its line's start, which is what Yoga is told.
3422
+ */
3423
+ function heightOfNone(
3424
+ style: ComputedStyle,
3425
+ /** Whether the flex box has a definite height. */
3426
+ own: boolean,
3427
+ ): boolean {
3428
+ return !own && isPct(style.height);
3429
+ }
3430
+
3279
3431
  /** Whether an item is stretched across its line: `stretch`, its own or its
3280
3432
  * container's, with no height of its own and no `auto` margin across. */
3281
3433
  function stretches(
@@ -3331,15 +3483,17 @@ interface Across {
3331
3483
  * where they are short of the box; `center` and `flex-end` whether they are
3332
3484
  * or not, so that lines wider than the box run past both its sides, or its
3333
3485
  * start; and the spacing values only where there is room, setting the lines
3334
- * at the box's start where there is none (CSS Box Alignment 3, 4.3:
3335
- * `space-between` falls back to `flex-start`, `space-around` and
3336
- * `space-evenly` to `safe center`, the start where the lines overflow). An
3486
+ * at their start where there is none, or at the box's (CSS Box Alignment 3,
3487
+ * 4.3: `space-between` falls back to `flex-start`, `space-around` and
3488
+ * `space-evenly` to `safe center`, the box's own start where the lines
3489
+ * overflow, and not theirs where they wrap in reverse). An
3337
3490
  * item with an `auto` margin across its line takes the room the line has
3338
3491
  * past it on that side (8.1), and none where there is none; a stretched
3339
3492
  * item is as wide as its line less its margins, within its limits (9.4,
3340
- * step 11), and as tall as the column made it; and the rest are where
3341
- * `align-self` says, a baseline at the line's start, as a column has none
3342
- * across it to align by.
3493
+ * step 11), and as tall as the column made it; those aligned by baselines
3494
+ * share their left edges, set together at the line's start, where a column
3495
+ * that does not wrap sets them (`baselineEdge`); and the rest are where
3496
+ * `align-self` says.
3343
3497
  */
3344
3498
  function crossLines(
3345
3499
  style: ComputedStyle,
@@ -3373,14 +3527,34 @@ function crossLines(
3373
3527
  lead.push(fromRight ? r : l);
3374
3528
  trail.push(fromRight ? l : r);
3375
3529
  });
3376
- const across = lines.map((line) =>
3377
- line.reduce((most, i) => Math.max(most, sizes[i] + lead[i] + trail[i]), 0),
3378
- );
3530
+ // A line is as wide as its widest item, margins and all, and as the
3531
+ // items it aligns by baselines come to, sharing their left edges: the
3532
+ // most any reaches from the line's start to that edge, and the most any
3533
+ // reaches past it (`baselineEdge`; 9.4 step 8). `reach` is each line's
3534
+ // first, where those items' left edges are from its start.
3535
+ const reach: number[] = [];
3536
+ const across = lines.map((line) => {
3537
+ let most = 0;
3538
+ let before = -Infinity;
3539
+ let after = -Infinity;
3540
+ for (const i of line) {
3541
+ most = Math.max(most, sizes[i] + lead[i] + trail[i]);
3542
+ if (!byBaseline(style, items[i].box.style, false)) continue;
3543
+ before = Math.max(before, lead[i] + (fromRight ? sizes[i] : 0));
3544
+ after = Math.max(after, trail[i] + (fromRight ? 0 : sizes[i]));
3545
+ }
3546
+ reach.push(before);
3547
+ return Math.max(most, before + after);
3548
+ });
3379
3549
  const count = lines.length;
3380
3550
  let room = width - gap * (count - 1);
3381
3551
  for (const size of across) room -= size;
3382
3552
  let at = 0;
3383
3553
  let between = gap;
3554
+ // Where they are wider than the box, `space-around` and `space-evenly`
3555
+ // set the lines at the box's own start (`safe center`), which is where
3556
+ // their last one ends where they wrap in reverse: they run past its end
3557
+ const reverse = style.flexWrap === 'wrap-reverse';
3384
3558
  switch (crossContent(style)) {
3385
3559
  case 'flex-end':
3386
3560
  at = room;
@@ -3398,13 +3572,13 @@ function crossLines(
3398
3572
  if (room > 0) {
3399
3573
  at = room / count / 2;
3400
3574
  between += room / count;
3401
- }
3575
+ } else if (reverse) at = room;
3402
3576
  break;
3403
3577
  case 'space-evenly':
3404
3578
  if (room > 0) {
3405
3579
  at = room / (count + 1);
3406
3580
  between += room / (count + 1);
3407
- }
3581
+ } else if (reverse) at = room;
3408
3582
  break;
3409
3583
  }
3410
3584
  const placed = new Map<Box, Across>();
@@ -3429,6 +3603,8 @@ function crossLines(
3429
3603
  if (itemWidth > most) itemWidth = most;
3430
3604
  if (itemWidth < least) itemWidth = least;
3431
3605
  itemWidth = Math.max(itemWidth, box.horizontalExtra);
3606
+ } else if (byBaseline(style, own, false)) {
3607
+ offset = reach[k] - (fromRight ? itemWidth : 0);
3432
3608
  } else {
3433
3609
  const align = own.alignSelf === AUTO ? style.alignItems : own.alignSelf;
3434
3610
  if (align === 'center') offset += free / 2;
@@ -45,6 +45,7 @@ import { inkColor, isTransparent, resolve } from '../css/values.js';
45
45
  import { tagOf } from '../dom.js';
46
46
  import {
47
47
  BOX_RAISES,
48
+ CONTROL_BASELINES,
48
49
  GRID_TRACKS,
49
50
  LINE_BOX_RAISES,
50
51
  PADDED_FACES,
@@ -3329,6 +3330,22 @@ function atomicRaise(
3329
3330
  */
3330
3331
  function atomicBaseline(box: Box): number {
3331
3332
  const margin = box.height + box.marginTop + box.marginBottom;
3333
+ // a control on its text's baseline, or on its border box's bottom,
3334
+ // where layout containment does not keep its baseline in
3335
+ const control =
3336
+ box.style.contain & CONTAIN_LAYOUT ? undefined : CONTROL_BASELINES.get(box);
3337
+ if (control === 'border') return box.marginTop + box.height;
3338
+ if (control) {
3339
+ const content = box.height - box.verticalExtra;
3340
+ const text = control.ascent + control.descent;
3341
+ return (
3342
+ box.marginTop +
3343
+ box.borderTop +
3344
+ box.padTop +
3345
+ (content - text) / 2 +
3346
+ control.ascent
3347
+ );
3348
+ }
3332
3349
  const button = isButton(box);
3333
3350
  // A `<button>` with no line in it sits on the bottom of its content box,
3334
3351
  // not of its margin box: where its label would have sat. No specification
@@ -3468,14 +3485,14 @@ function tableBaseline(table: Box): number | null {
3468
3485
  * chevron, and the line it was on came out shorter than a browser's.
3469
3486
  */
3470
3487
  function lastBaselineIn(box: Box): number | null {
3471
- if (box.kind === 'flex') return flexBaseline(box);
3488
+ if (box.kind === 'flex') return heldIn(box, flexBaseline(box));
3472
3489
  if (box.lines?.length) {
3473
3490
  const line = box.lines[box.lines.length - 1];
3474
- return line.y + line.baseline;
3491
+ return heldIn(box, line.y + line.baseline);
3475
3492
  }
3476
3493
  for (let i = box.children.length - 1; i >= 0; i -= 1) {
3477
3494
  const found = childBaseline(box.children[i], lastBaselineIn);
3478
- if (found !== null) return found;
3495
+ if (found !== null) return heldIn(box, found);
3479
3496
  }
3480
3497
  return null;
3481
3498
  }
@@ -3483,15 +3500,32 @@ function lastBaselineIn(box: Box): number | null {
3483
3500
  /** The same, from the first line box or child: a table's baseline is its
3484
3501
  * first row's, and so is a table cell's. */
3485
3502
  export function firstBaselineIn(box: Box): number | null {
3486
- if (box.kind === 'flex') return flexBaseline(box);
3487
- if (box.lines?.length) return box.lines[0].y + box.lines[0].baseline;
3503
+ if (box.kind === 'flex') return heldIn(box, flexBaseline(box));
3504
+ if (box.lines?.length) {
3505
+ return heldIn(box, box.lines[0].y + box.lines[0].baseline);
3506
+ }
3488
3507
  for (const child of box.children) {
3489
3508
  const found = childBaseline(child, firstBaselineIn);
3490
- if (found !== null) return found;
3509
+ if (found !== null) return heldIn(box, found);
3491
3510
  }
3492
3511
  return null;
3493
3512
  }
3494
3513
 
3514
+ /**
3515
+ * A box's baseline as its content gives it, held to its border box where
3516
+ * the box is a scroll container (CSS Box Alignment 3, 9.1), at either edge
3517
+ * — and only there: a box whose text overflows it otherwise, or clips it
3518
+ * (`overflow: clip` is no scroll container), has the baseline its text
3519
+ * has, as Chrome has it. Held at every box, so a scroll container deep in
3520
+ * an item, or in an inline block, holds the baseline it gives to its box.
3521
+ * The flex layout held every item to its height, and an item shorter
3522
+ * than its text sat its text's descent below the items beside it.
3523
+ */
3524
+ function heldIn(box: Box, found: number | null): number | null {
3525
+ if (found === null || !scrolls(box.style)) return found;
3526
+ return Math.min(Math.max(found, box.y), box.y + box.height);
3527
+ }
3528
+
3495
3529
  function childBaseline(
3496
3530
  child: Box,
3497
3531
  inside: (box: Box) => number | null,
@@ -3502,9 +3536,9 @@ function childBaseline(
3502
3536
  // a table is passed over for a block's last baseline (`lastBaselineIn`)
3503
3537
  if (inside === lastBaselineIn && child.kind === 'table') return null;
3504
3538
  // the legacy rule is a block container's last baseline alone (CSS Box
3505
- // Alignment 3, 9.2): its first is its first line's, clipped or not —
3506
- // and a button that clips keeps its label's, here as on a line of its
3507
- // own (`atomicBaseline`)
3539
+ // Alignment 3, 9.2): its first is its first line's, clipped or not, held
3540
+ // to its border box (`heldIn`) — and a button that clips keeps its
3541
+ // label's, here as on a line of its own (`atomicBaseline`)
3508
3542
  if (
3509
3543
  inside === lastBaselineIn &&
3510
3544
  child.kind !== 'flex' &&
package/src/html/node.ts CHANGED
@@ -1051,6 +1051,10 @@ export class HtmlViewNode extends Node {
1051
1051
  const fonts = this._fonts();
1052
1052
  return fonts ? faceExtentOf(fonts, style).ascent : undefined;
1053
1053
  },
1054
+ faceExtent: (style) => {
1055
+ const fonts = this._fonts();
1056
+ return fonts ? faceExtentOf(fonts, style) : undefined;
1057
+ },
1054
1058
  controlSize: (el, kind, style) =>
1055
1059
  measureControl(el, kind, style, this._fonts(), look),
1056
1060
  });
@@ -3514,6 +3518,14 @@ function hoverChange(
3514
3518
  }
3515
3519
  continue;
3516
3520
  }
3521
+ // what an element's animations leave is in its other fields already
3522
+ // (`Cascade._computeStyle`); the lists themselves draw nothing
3523
+ if (key === 'animations') {
3524
+ if (!sameValue(a[key], b[key])) {
3525
+ change ??= { ink: false, reach: false, order: false, move: false };
3526
+ }
3527
+ continue;
3528
+ }
3517
3529
  if (sameValue(a[key], b[key])) continue;
3518
3530
  change ??= { ink: true, reach: false, order: false, move: false };
3519
3531
  change.ink = true;