@jsenv/navi 0.29.228 → 0.29.230

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.
@@ -63429,158 +63429,163 @@ const ListSelectable = props => {
63429
63429
  }
63430
63430
  setCurrentId(initialEl.id);
63431
63431
  }, []);
63432
- const listVnode = jsx(Next, {
63433
- "navi-selectable": "",
63434
- ...listControlRootProps,
63435
- ...listControlProps,
63436
- // "loading" is a control prop, so useControlgroupProps consumes it (into
63437
- // aria-busy / the :-navi-loading pseudo state) and it does not survive
63438
- // into the props below. ListUI needs it too — it is what makes the list
63439
- // render skeleton rows instead of its (not yet known) items.
63440
- loading: props.loading,
63441
- name: undefined,
63442
- value: undefined,
63443
- defaultValue: undefined,
63444
- selectable: undefined,
63445
- multiple: undefined,
63446
- deselectable: undefined,
63447
- focusGroupDirection: undefined,
63448
- focusGroupWrap: undefined
63449
- // Track focus inside the list: whichever item gets focus becomes current.
63450
- ,
63451
- onFocusIn: e => {
63452
- const realInput = e.target.closest("[navi-selectable-real-input]");
63453
- if (!realInput) {
63454
- return;
63455
- }
63456
- const itemEl = realInput.closest("[navi-list-item-real]");
63457
- if (itemEl && itemEl.id) {
63458
- setCurrentId(itemEl.id, e);
63459
- }
63460
- },
63461
- onnavi_request_select: e => {
63462
- const {
63463
- id
63464
- } = e.detail;
63465
- if (id === undefined) {
63466
- return;
63467
- }
63468
- const inputId = `${id}_input`;
63469
- const childController = uiGroupStateController.findChildById(inputId);
63470
- if (!childController) {
63471
- return;
63472
- }
63473
- const list = ref.current;
63474
- dispatchRequestInteraction(list, {
63475
- event: e,
63476
- name: "select",
63477
- prevented: () => e.preventDefault(),
63478
- // tell the requester that we don't want to select this item
63479
- // Asked of the item too, not only of the list: an item can be the one
63480
- // refusing (the list already holds all it accepts, see maxLength), and
63481
- // it is the one that then says why.
63482
- allowed: () => {
63483
- dispatchRequestInteraction(childController.ref.current, {
63484
- event: e,
63485
- name: "select",
63486
- prevented: () => e.preventDefault(),
63487
- allowed: () => childController.setUIState(childController.value, e)
63488
- });
63432
+
63433
+ // The group's contexts stand around the whole list, never between the <ul>
63434
+ // and the rows: the walk that gives the rows their places is over the <ul>'s
63435
+ // children, and a component sitting there is one child — every row would then
63436
+ // stand in a single slot, and reordering them (a search) would move nothing.
63437
+ const listVnode = jsx(ControlgroupChildrenWrapper, {
63438
+ ...childrenWrapperProps,
63439
+ children: jsx(Next, {
63440
+ "navi-selectable": "",
63441
+ ...listControlRootProps,
63442
+ ...listControlProps,
63443
+ // "loading" is a control prop, so useControlgroupProps consumes it (into
63444
+ // aria-busy / the :-navi-loading pseudo state) and it does not survive
63445
+ // into the props below. ListUI needs it too — it is what makes the list
63446
+ // render skeleton rows instead of its (not yet known) items.
63447
+ loading: props.loading,
63448
+ name: undefined,
63449
+ value: undefined,
63450
+ defaultValue: undefined,
63451
+ selectable: undefined,
63452
+ multiple: undefined,
63453
+ deselectable: undefined,
63454
+ focusGroupDirection: undefined,
63455
+ focusGroupWrap: undefined
63456
+ // Track focus inside the list: whichever item gets focus becomes current.
63457
+ ,
63458
+ onFocusIn: e => {
63459
+ const realInput = e.target.closest("[navi-selectable-real-input]");
63460
+ if (!realInput) {
63461
+ return;
63489
63462
  }
63490
- });
63491
- },
63492
- onnavi_request_unselect: e => {
63493
- const {
63494
- id
63495
- } = e.detail;
63496
- if (id === undefined) {
63497
- return;
63498
- }
63499
- const inputId = `${id}_input`;
63500
- const childController = uiGroupStateController.findChildById(inputId);
63501
- if (!childController) {
63502
- return;
63503
- }
63504
- const list = ref.current;
63505
- dispatchRequestInteraction(list, {
63506
- event: e,
63507
- name: "unselect",
63508
- prevented: () => e.preventDefault(),
63509
- // tell the requester that we don't want to unselect this item
63510
- allowed: () => childController.setUIState(undefined, e)
63511
- });
63512
- }
63513
- // "previous"/"next", not "up"/"down": a list is a line of items whichever
63514
- // way it is laid out, and a horizontal one walks sideways. The keys that
63515
- // drive it (arrows, Home/End) map onto that here, and so do the commands
63516
- // (--navi-previous / --navi-next / --navi-first / --navi-last).
63517
- ,
63518
- onnavi_request_nav: e => {
63519
- const {
63520
- goal
63521
- } = e.detail;
63522
- const navigableEls = getNavigableElements();
63523
- if (navigableEls.length === 0) {
63524
- return;
63463
+ const itemEl = realInput.closest("[navi-list-item-real]");
63464
+ if (itemEl && itemEl.id) {
63465
+ setCurrentId(itemEl.id, e);
63466
+ }
63467
+ },
63468
+ onnavi_request_select: e => {
63469
+ const {
63470
+ id
63471
+ } = e.detail;
63472
+ if (id === undefined) {
63473
+ return;
63474
+ }
63475
+ const inputId = `${id}_input`;
63476
+ const childController = uiGroupStateController.findChildById(inputId);
63477
+ if (!childController) {
63478
+ return;
63479
+ }
63480
+ const list = ref.current;
63481
+ dispatchRequestInteraction(list, {
63482
+ event: e,
63483
+ name: "select",
63484
+ prevented: () => e.preventDefault(),
63485
+ // tell the requester that we don't want to select this item
63486
+ // Asked of the item too, not only of the list: an item can be the one
63487
+ // refusing (the list already holds all it accepts, see maxLength), and
63488
+ // it is the one that then says why.
63489
+ allowed: () => {
63490
+ dispatchRequestInteraction(childController.ref.current, {
63491
+ event: e,
63492
+ name: "select",
63493
+ prevented: () => e.preventDefault(),
63494
+ allowed: () => childController.setUIState(childController.value, e)
63495
+ });
63496
+ }
63497
+ });
63498
+ },
63499
+ onnavi_request_unselect: e => {
63500
+ const {
63501
+ id
63502
+ } = e.detail;
63503
+ if (id === undefined) {
63504
+ return;
63505
+ }
63506
+ const inputId = `${id}_input`;
63507
+ const childController = uiGroupStateController.findChildById(inputId);
63508
+ if (!childController) {
63509
+ return;
63510
+ }
63511
+ const list = ref.current;
63512
+ dispatchRequestInteraction(list, {
63513
+ event: e,
63514
+ name: "unselect",
63515
+ prevented: () => e.preventDefault(),
63516
+ // tell the requester that we don't want to unselect this item
63517
+ allowed: () => childController.setUIState(undefined, e)
63518
+ });
63525
63519
  }
63526
- const currentId = currentIdRef.current;
63527
- let currentIndex = -1;
63528
- if (currentId) {
63529
- currentIndex = navigableEls.findIndex(el => el.id === currentId);
63530
- }
63531
- let targetEl;
63532
- if (goal === "first") {
63533
- targetEl = navigableEls[0];
63534
- } else if (goal === "last") {
63535
- targetEl = navigableEls[navigableEls.length - 1];
63536
- } else if (goal === "next") {
63537
- if (currentIndex === -1) {
63520
+ // "previous"/"next", not "up"/"down": a list is a line of items whichever
63521
+ // way it is laid out, and a horizontal one walks sideways. The keys that
63522
+ // drive it (arrows, Home/End) map onto that here, and so do the commands
63523
+ // (--navi-previous / --navi-next / --navi-first / --navi-last).
63524
+ ,
63525
+ onnavi_request_nav: e => {
63526
+ const {
63527
+ goal
63528
+ } = e.detail;
63529
+ const navigableEls = getNavigableElements();
63530
+ if (navigableEls.length === 0) {
63531
+ return;
63532
+ }
63533
+ const currentId = currentIdRef.current;
63534
+ let currentIndex = -1;
63535
+ if (currentId) {
63536
+ currentIndex = navigableEls.findIndex(el => el.id === currentId);
63537
+ }
63538
+ let targetEl;
63539
+ if (goal === "first") {
63538
63540
  targetEl = navigableEls[0];
63539
- } else if (currentIndex < navigableEls.length - 1) {
63540
- targetEl = navigableEls[currentIndex + 1];
63541
- } else {
63541
+ } else if (goal === "last") {
63542
63542
  targetEl = navigableEls[navigableEls.length - 1];
63543
+ } else if (goal === "next") {
63544
+ if (currentIndex === -1) {
63545
+ targetEl = navigableEls[0];
63546
+ } else if (currentIndex < navigableEls.length - 1) {
63547
+ targetEl = navigableEls[currentIndex + 1];
63548
+ } else {
63549
+ targetEl = navigableEls[navigableEls.length - 1];
63550
+ }
63551
+ } else if (goal === "previous") {
63552
+ if (currentIndex === -1) {
63553
+ targetEl = navigableEls[0];
63554
+ } else if (currentIndex > 0) {
63555
+ targetEl = navigableEls[currentIndex - 1];
63556
+ } else {
63557
+ targetEl = navigableEls[0];
63558
+ }
63543
63559
  }
63544
- } else if (goal === "previous") {
63545
- if (currentIndex === -1) {
63546
- targetEl = navigableEls[0];
63547
- } else if (currentIndex > 0) {
63548
- targetEl = navigableEls[currentIndex - 1];
63549
- } else {
63550
- targetEl = navigableEls[0];
63560
+ if (!targetEl) {
63561
+ return;
63551
63562
  }
63552
- }
63553
- if (!targetEl) {
63554
- return;
63555
- }
63556
- setCurrentId(targetEl.id, e);
63557
- dispatchCustomEvent(ref.current, "navi_request_scroll", {
63558
- event: e,
63559
- id: targetEl.id
63560
- });
63561
- },
63562
- onnavi_request_activate: e => {
63563
- const currentId = currentIdRef.current;
63564
- if (!currentId) {
63565
- return;
63566
- }
63567
- if (multiple || deselectable) {
63568
- const inputId = `${currentId}_input`;
63569
- const childController = uiGroupStateController.findChildById(inputId);
63570
- const isSelected = childController && childController.uiState;
63571
- dispatchCustomEvent(ref.current, isSelected ? "navi_request_unselect" : "navi_request_select", {
63563
+ setCurrentId(targetEl.id, e);
63564
+ dispatchCustomEvent(ref.current, "navi_request_scroll", {
63565
+ event: e,
63566
+ id: targetEl.id
63567
+ });
63568
+ },
63569
+ onnavi_request_activate: e => {
63570
+ const currentId = currentIdRef.current;
63571
+ if (!currentId) {
63572
+ return;
63573
+ }
63574
+ if (multiple || deselectable) {
63575
+ const inputId = `${currentId}_input`;
63576
+ const childController = uiGroupStateController.findChildById(inputId);
63577
+ const isSelected = childController && childController.uiState;
63578
+ dispatchCustomEvent(ref.current, isSelected ? "navi_request_unselect" : "navi_request_select", {
63579
+ event: e,
63580
+ id: currentId
63581
+ });
63582
+ return;
63583
+ }
63584
+ dispatchCustomEvent(ref.current, "navi_request_select", {
63572
63585
  event: e,
63573
63586
  id: currentId
63574
63587
  });
63575
- return;
63576
- }
63577
- dispatchCustomEvent(ref.current, "navi_request_select", {
63578
- event: e,
63579
- id: currentId
63580
- });
63581
- },
63582
- children: jsx(ControlgroupChildrenWrapper, {
63583
- ...childrenWrapperProps,
63588
+ },
63584
63589
  children: props.children
63585
63590
  })
63586
63591
  });
@@ -64960,52 +64965,59 @@ const ListContent = ({
64960
64965
  children
64961
64966
  }) => {
64962
64967
  const listProps = useContext(BoxForwardedPropsContext);
64963
- return jsx(Box, {
64964
- className: "navi_list_scroll_container",
64965
- overflow: overflow,
64966
- overflowX: overflowX,
64967
- overflowY: overflowY,
64968
- ...scrollBoxPaddingProps,
64969
- children: jsx(UnorderedList, {
64970
- role: role,
64971
- fallback: fallback,
64972
- fallbackShown: fallbackShown,
64973
- searchFallback: searchFallback,
64974
- searchFallbackShown: searchFallbackShown,
64975
- loadingPlaceholderShown: loadingPlaceholderShown,
64976
- error: error,
64977
- searchNoMatchMode: searchNoMatchMode,
64978
- separator: separator,
64979
- itemTransition: itemTransition,
64980
- expandX: expandX
64981
- // Deliberately not expandY here (unlike expandX above): the outer
64982
- // .navi_list_container already gets its own expandY treatment (see
64983
- // ListUI's own Box above) to fill whatever space its *own* parent
64984
- // gives it (e.g. a flex-column ancestor's flex-grow) — the <ul>
64985
- // itself must stay auto-height regardless, or it gets capped to
64986
- // match .navi_list_scroll_container's own (possibly much smaller)
64987
- // flex-resolved height instead of its real content height. That
64988
- // breaks two things at once: virtual scroll's own filler sizing
64989
- // (nothing to overflow into the scroll container in the first
64990
- // place) and any sticky List.Item header/footer inside it (their
64991
- // sticky "containing block" — the <ul>'s own box — would be
64992
- // artificially small, so they run out of room to stay stuck well
64993
- // before the user has actually scrolled through all the content).
64994
- ,
64995
- horizontal: horizontal,
64996
- spacing: spacing,
64997
- columns: columns,
64998
- itemColumns: itemColumns,
64999
- ...listProps,
65000
- tracker: tracker,
65001
- renderWindow: renderWindow,
65002
- virtual: virtual,
65003
- children: jsx(PendingScrollRefContext.Provider, {
65004
- value: pendingScrollRef,
65005
- children: children
64968
+ return (
64969
+ // Every provider the list puts around its items stands OUTSIDE the <ul>:
64970
+ // the walk that gives the rows their places (see ListDeclaredChildren) is
64971
+ // over the <ul>'s children, and one component between it and the caller's
64972
+ // rows is one child — all the rows would then stand in a single slot, and
64973
+ // reordering them would move nothing.
64974
+ jsx(PendingScrollRefContext.Provider, {
64975
+ value: pendingScrollRef,
64976
+ children: jsx(Box, {
64977
+ className: "navi_list_scroll_container",
64978
+ overflow: overflow,
64979
+ overflowX: overflowX,
64980
+ overflowY: overflowY,
64981
+ ...scrollBoxPaddingProps,
64982
+ children: jsx(UnorderedList, {
64983
+ role: role,
64984
+ fallback: fallback,
64985
+ fallbackShown: fallbackShown,
64986
+ searchFallback: searchFallback,
64987
+ searchFallbackShown: searchFallbackShown,
64988
+ loadingPlaceholderShown: loadingPlaceholderShown,
64989
+ error: error,
64990
+ searchNoMatchMode: searchNoMatchMode,
64991
+ separator: separator,
64992
+ itemTransition: itemTransition,
64993
+ expandX: expandX
64994
+ // Deliberately not expandY here (unlike expandX above): the outer
64995
+ // .navi_list_container already gets its own expandY treatment (see
64996
+ // ListUI's own Box above) to fill whatever space its *own* parent
64997
+ // gives it (e.g. a flex-column ancestor's flex-grow) — the <ul>
64998
+ // itself must stay auto-height regardless, or it gets capped to
64999
+ // match .navi_list_scroll_container's own (possibly much smaller)
65000
+ // flex-resolved height instead of its real content height. That
65001
+ // breaks two things at once: virtual scroll's own filler sizing
65002
+ // (nothing to overflow into the scroll container in the first
65003
+ // place) and any sticky List.Item header/footer inside it (their
65004
+ // sticky "containing block" — the <ul>'s own box — would be
65005
+ // artificially small, so they run out of room to stay stuck well
65006
+ // before the user has actually scrolled through all the content).
65007
+ ,
65008
+ horizontal: horizontal,
65009
+ spacing: spacing,
65010
+ columns: columns,
65011
+ itemColumns: itemColumns,
65012
+ ...listProps,
65013
+ tracker: tracker,
65014
+ renderWindow: renderWindow,
65015
+ virtual: virtual,
65016
+ children: children
65017
+ })
65006
65018
  })
65007
65019
  })
65008
- });
65020
+ );
65009
65021
  };
65010
65022
  // Where the items sit is a question about the track (the <ul>), not about the
65011
65023
  // frame around it: the frame's only child is the scroll box, which fills it and
@@ -65200,6 +65212,10 @@ const useListScrollSync = ({
65200
65212
  const block = blockRequested || (event.type === "navi_displayed" ? "center" : "nearest");
65201
65213
  const scrollToItemCall = `${getElementSignature(itemEl)}.scrollIntoView({ block: "${block}", container: "nearest" })`;
65202
65214
  debugScroll(`${trigger} -> ${scrollToItemCall}`);
65215
+ // The list is going somewhere on purpose, so there is no view to hold
65216
+ // still any more: an anchor captured before this drop it, or it would
65217
+ // put the list back where it was the moment the rows move under it.
65218
+ anchorRef.current = null;
65203
65219
  scrollIntoViewScoped(itemEl, {
65204
65220
  container: getScroller(),
65205
65221
  block
@@ -65210,17 +65226,14 @@ const useListScrollSync = ({
65210
65226
  item
65211
65227
  });
65212
65228
  };
65213
- const {
65214
- start,
65215
- end
65216
- } = renderWindowRef.current;
65217
- const isInWindow = index >= start && index < end;
65218
- if (isInWindow) {
65219
- const itemEl = findRowElement(getListEl(), item.id);
65220
- if (itemEl) {
65221
- scrollItemIntoView(itemEl);
65222
- return;
65223
- }
65229
+
65230
+ // Whether the row is drawn is asked of the dom, not of the render window:
65231
+ // the window says what a run draws, and a list whose rows are declared one
65232
+ // by one has them all in the dom whatever the window says.
65233
+ const itemEl = findRowElement(getListEl(), item.id);
65234
+ if (itemEl) {
65235
+ scrollItemIntoView(itemEl);
65236
+ return;
65224
65237
  }
65225
65238
  // Not in DOM — shift the render window. The item will read
65226
65239
  // pendingScrollRef on mount and scroll into view.
@@ -65342,9 +65355,8 @@ const useListScrollSync = ({
65342
65355
  }, []);
65343
65356
  // Watch scores of the top renderBudget items.
65344
65357
  // When scores change during an active search, scroll to top to reveal the most relevant items.
65345
- // When search becomes empty, restore the scroll position from before the search started.
65346
- // We save the first-visible item ID so restoration is item-precise
65347
- // and survives render-window shifts or item reordering.
65358
+ // When search becomes empty, put the list back at the offset (and the render
65359
+ // window) it was at when the search started — see docs/scroll.md.
65348
65360
 
65349
65361
  // NOTE POUR LE JOUR OU ON A LE MULTISELECT:
65350
65362
  // Lorsqu'on selectionne quelque chose pendant une recherche, alors ensuite meme si on clear
@@ -67252,6 +67264,28 @@ const createListVirtual = () => {
67252
67264
  } else {
67253
67265
  ownerIdsBySlot.set(slotId, [ownerId]);
67254
67266
  }
67267
+ warnIfEveryRowInOneSlot(slotId);
67268
+ };
67269
+ // Rows that all stand in the same slot keep the order they first mounted in:
67270
+ // the walk is over the children the list is given, and a component holding
67271
+ // them is one child however many rows it renders. Everything about a place
67272
+ // then stops following what the caller writes — a search reordering the rows
67273
+ // moves nothing. Said once, and only for the shape that can be nothing else:
67274
+ // the list's whole content is one child, and several rows came out of it.
67275
+ let everyRowInOneSlotWarned = false;
67276
+ const warnIfEveryRowInOneSlot = slotId => {
67277
+ if (everyRowInOneSlotWarned) {
67278
+ return;
67279
+ }
67280
+ const rootSlotIds = slotIdsByParent.get(null);
67281
+ if (!rootSlotIds || rootSlotIds.length !== 1 || rootSlotIds[0] !== slotId) {
67282
+ return;
67283
+ }
67284
+ if (ownerIdsBySlot.get(slotId).length < 2) {
67285
+ return;
67286
+ }
67287
+ everyRowInOneSlotWarned = true;
67288
+ console.warn(`List: every row stands in the same slot, so they keep the order they first rendered in — reordering them (a search, a sort) will not move them. The list's rows must be its own children: give it the rows (or a <List.Items>), not a component rendering them.`);
67255
67289
  };
67256
67290
  const removeFromSlot = (slotId, ownerId) => {
67257
67291
  const ownerIds = ownerIdsBySlot.get(slotId);