@visns-studio/visns-components 6.32.2 → 6.32.3
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.
- package/README.md +34 -0
- package/package.json +1 -1
- package/src/components/Navigation.jsx +80 -29
- package/src/components/styles/Navigation.module.scss +32 -10
package/README.md
CHANGED
|
@@ -9,6 +9,40 @@ A comprehensive React component library used by the VISNS Studio team for CRM an
|
|
|
9
9
|
|
|
10
10
|
VISNS Components is a React-based UI component library that provides a set of reusable, consistent, and customizable components for building web applications. It includes components for authentication, data grids, forms, navigation, and more, designed to work seamlessly together.
|
|
11
11
|
|
|
12
|
+
## Recent Updates (v6.32.3)
|
|
13
|
+
|
|
14
|
+
### The nav dropdowns open where the pointer left them
|
|
15
|
+
|
|
16
|
+
A group in the header bar — Reports, say — opened its sub-menu and then shut
|
|
17
|
+
it again the instant the pointer arrived, and on a strip scrolled to its end
|
|
18
|
+
the item visibly jumped left as it happened. The bar (`.navwrap`) is
|
|
19
|
+
`overflow-x: auto` so it can scroll on narrow screens, and a scroll container
|
|
20
|
+
clips on BOTH axes however `visible` the other one is written, so the panel
|
|
21
|
+
hanging below the bar was being cut off at the bar's bottom edge. The library
|
|
22
|
+
had been working around that by flipping the strip to `overflow-x: visible`
|
|
23
|
+
on hover — which destroys the scroll container: the browser resets
|
|
24
|
+
`scrollLeft` to 0, the item slides out from under the pointer, and the menu
|
|
25
|
+
loses the hover that opened it.
|
|
26
|
+
|
|
27
|
+
The panel is now `position: fixed`, placed per item from coordinates the
|
|
28
|
+
component measures (`--nav-sub-top` / `--nav-sub-left`, set on the `<li>` from
|
|
29
|
+
its own bounding rect and clamped into the viewport so the last group does not
|
|
30
|
+
open off the right edge). A fixed panel is outside the scroller's flow, so the
|
|
31
|
+
clip never applies and the strip's scroll position is never touched. Pinning
|
|
32
|
+
happens on `mouseenter` and on `focus` — and since React's `onFocus` bubbles,
|
|
33
|
+
tabbing to a group heading now opens and places its menu too, where keyboard
|
|
34
|
+
users previously got a menu that `:focus-within` opened inside the clip and
|
|
35
|
+
nothing un-clipped. Scrolling the strip re-pins whatever is open, so a wheeled
|
|
36
|
+
bar keeps its menu under its item. A group that ALSO has a `url` renders its
|
|
37
|
+
button (and its real `aria-expanded`) on a coarse pointer instead of a plain
|
|
38
|
+
link, so a tap opens the menu rather than only navigating; on a fine pointer
|
|
39
|
+
the heading stays a clickable link.
|
|
40
|
+
|
|
41
|
+
One caveat comes with `position: fixed`: an ancestor carrying `transform`,
|
|
42
|
+
`filter` or `contain: paint` becomes the containing block for fixed
|
|
43
|
+
descendants and would trap the panel inside the bar again. Nothing in the
|
|
44
|
+
shipped layouts does that.
|
|
45
|
+
|
|
12
46
|
## Recent Updates (v6.32.2)
|
|
13
47
|
|
|
14
48
|
### Boxed overview: no panel around the cards
|
package/package.json
CHANGED
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
94
94
|
},
|
|
95
95
|
"name": "@visns-studio/visns-components",
|
|
96
|
-
"version": "6.32.
|
|
96
|
+
"version": "6.32.3",
|
|
97
97
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
98
98
|
"main": "src/index.js",
|
|
99
99
|
"files": [
|
|
@@ -767,26 +767,65 @@ function Navigation({
|
|
|
767
767
|
* point of declaration (`const [, setActiveDropdown]`): it re-rendered the
|
|
768
768
|
* whole bar on every hover to change nothing, because the only thing it
|
|
769
769
|
* did was toggle the scroll container's overflow, which is a DOM poke.
|
|
770
|
-
* That poke is `
|
|
771
|
-
*
|
|
772
|
-
*
|
|
770
|
+
* That poke is `pinDropdown` now, and it is called from the tap path as
|
|
771
|
+
* well — the dropdown has to be placed before it is shown, wherever the
|
|
772
|
+
* pointer has or has not been.
|
|
773
773
|
*/
|
|
774
774
|
const [openGroup, setOpenGroup] = useState(null);
|
|
775
775
|
const navWrapRef = useRef(null);
|
|
776
776
|
|
|
777
777
|
/**
|
|
778
|
-
*
|
|
778
|
+
* Put an item's dropdown directly underneath the item.
|
|
779
779
|
*
|
|
780
|
-
* The
|
|
781
|
-
*
|
|
782
|
-
*
|
|
780
|
+
* The panel is `position: fixed` (the stylesheet carries the long note):
|
|
781
|
+
* that is what keeps it out of the strip's `overflow-x: auto` clip, since
|
|
782
|
+
* a scroll container clips on BOTH axes and the alternative — flipping
|
|
783
|
+
* the strip to `overflow-x: visible` — destroyed the scroll container,
|
|
784
|
+
* reset `scrollLeft` to 0 and slid the item out from under the pointer
|
|
785
|
+
* while the menu was opening. The price of fixed is that the panel no
|
|
786
|
+
* longer inherits the item's position, so measure the <li> and hand the
|
|
787
|
+
* two coordinates down as custom properties. The scroller is never
|
|
788
|
+
* touched.
|
|
789
|
+
*
|
|
790
|
+
* The left edge is clamped into the viewport, so the last group in the
|
|
791
|
+
* strip opens its 220px panel on screen rather than off the right edge.
|
|
783
792
|
*/
|
|
784
|
-
const
|
|
785
|
-
if (
|
|
786
|
-
|
|
793
|
+
const pinDropdown = (li) => {
|
|
794
|
+
if (!li) {
|
|
795
|
+
return;
|
|
787
796
|
}
|
|
797
|
+
|
|
798
|
+
const rect = li.getBoundingClientRect();
|
|
799
|
+
li.style.setProperty('--nav-sub-top', `${rect.bottom}px`);
|
|
800
|
+
li.style.setProperty(
|
|
801
|
+
'--nav-sub-left',
|
|
802
|
+
`${Math.max(8, Math.min(rect.left, window.innerWidth - 220 - 8))}px`
|
|
803
|
+
);
|
|
788
804
|
};
|
|
789
805
|
|
|
806
|
+
// A scrolled strip moves its items but not a fixed panel, so a menu that
|
|
807
|
+
// is already open would sail away from the group it belongs to. Re-pin
|
|
808
|
+
// whatever is open as the strip scrolls — `:hover` is the mouse, and
|
|
809
|
+
// `:focus-within` covers both the keyboard and the group a touch reader
|
|
810
|
+
// has tapped open. Passive: this only reads geometry and writes two
|
|
811
|
+
// custom properties, it never cancels the scroll.
|
|
812
|
+
useEffect(() => {
|
|
813
|
+
const strip = navWrapRef.current;
|
|
814
|
+
|
|
815
|
+
if (!strip) {
|
|
816
|
+
return undefined;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const onScroll = () => {
|
|
820
|
+
strip
|
|
821
|
+
.querySelectorAll('li:hover, li:focus-within')
|
|
822
|
+
.forEach(pinDropdown);
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
strip.addEventListener('scroll', onScroll, { passive: true });
|
|
826
|
+
return () => strip.removeEventListener('scroll', onScroll);
|
|
827
|
+
}, []);
|
|
828
|
+
|
|
790
829
|
// A tap anywhere else closes the group a touch reader opened. Without it
|
|
791
830
|
// the only way to dismiss the menu is to tap the heading again, which is
|
|
792
831
|
// not how any other menu on the device behaves. Mouse users never get
|
|
@@ -799,7 +838,6 @@ function Navigation({
|
|
|
799
838
|
const onDown = (event) => {
|
|
800
839
|
if (!navWrapRef.current?.contains(event.target)) {
|
|
801
840
|
setOpenGroup(null);
|
|
802
|
-
liftNavClipping(false);
|
|
803
841
|
}
|
|
804
842
|
};
|
|
805
843
|
|
|
@@ -828,15 +866,14 @@ function Navigation({
|
|
|
828
866
|
|
|
829
867
|
const hasChildren = n.children && n.children.length > 0;
|
|
830
868
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
const handleMouseLeave = () => {
|
|
869
|
+
// Place the panel before it is shown. Hover is the mouse; focus is
|
|
870
|
+
// the keyboard, and React's onFocus bubbles, so focus landing on the
|
|
871
|
+
// group heading OR on any row inside the open menu pins it just the
|
|
872
|
+
// same. Nothing to undo on the way out: an unopened panel is
|
|
873
|
+
// `display: none` wherever its coordinates happen to say.
|
|
874
|
+
const handlePin = (event) => {
|
|
838
875
|
if (hasChildren) {
|
|
839
|
-
|
|
876
|
+
pinDropdown(event.currentTarget);
|
|
840
877
|
}
|
|
841
878
|
};
|
|
842
879
|
|
|
@@ -880,7 +917,6 @@ function Navigation({
|
|
|
880
917
|
onClick={() => {
|
|
881
918
|
setOpen(false);
|
|
882
919
|
setOpenGroup(null);
|
|
883
|
-
liftNavClipping(false);
|
|
884
920
|
}}
|
|
885
921
|
>
|
|
886
922
|
{child.label}
|
|
@@ -914,12 +950,23 @@ function Navigation({
|
|
|
914
950
|
return (
|
|
915
951
|
<li
|
|
916
952
|
className={navItemClasses}
|
|
917
|
-
onMouseEnter={
|
|
918
|
-
|
|
953
|
+
onMouseEnter={handlePin}
|
|
954
|
+
onFocus={handlePin}
|
|
919
955
|
>
|
|
920
|
-
{n.url ? (
|
|
956
|
+
{n.url && !(hasChildren && isCoarsePointer) ? (
|
|
921
957
|
// Close the mobile drawer when navigating; no-op for the
|
|
922
958
|
// header layout where the drawer state is never opened.
|
|
959
|
+
//
|
|
960
|
+
// A GROUP THAT ALSO HAS A URL goes the other way on a
|
|
961
|
+
// coarse pointer. A <Link> carries no `aria-expanded`, and
|
|
962
|
+
// the touch stylesheet opens the panel off that attribute
|
|
963
|
+
// alone — so on a touch screen the only thing a tap on
|
|
964
|
+
// such a heading could ever do was navigate, and the pages
|
|
965
|
+
// inside the group were unreachable. There the button
|
|
966
|
+
// branch below wins and the parent page is reached from
|
|
967
|
+
// the menu instead (its first row, by convention). On a
|
|
968
|
+
// fine pointer the link stays: hover opens the panel, so
|
|
969
|
+
// nothing is lost by keeping the heading clickable.
|
|
923
970
|
<Link
|
|
924
971
|
to={n.url}
|
|
925
972
|
aria-label={ariaLabel}
|
|
@@ -929,8 +976,8 @@ function Navigation({
|
|
|
929
976
|
</Link>
|
|
930
977
|
) : hasChildren ? (
|
|
931
978
|
/*
|
|
932
|
-
* A group heading — no page of its own, so there
|
|
933
|
-
* nothing to link to, but it still has to be REACHABLE.
|
|
979
|
+
* A group heading — usually no page of its own, so there
|
|
980
|
+
* is nothing to link to, but it still has to be REACHABLE.
|
|
934
981
|
* As a <span> it was not focusable and the menu opened on
|
|
935
982
|
* :hover alone, which left every page inside a group
|
|
936
983
|
* unreachable by keyboard and untappable on a touch
|
|
@@ -969,14 +1016,18 @@ function Navigation({
|
|
|
969
1016
|
what made the menu impossible to open by tapping,
|
|
970
1017
|
since the tap that opened it removed the focus
|
|
971
1018
|
holding it open in the same gesture. Toggle instead,
|
|
972
|
-
and
|
|
973
|
-
|
|
974
|
-
|
|
1019
|
+
and pin the panel to the item the way a hover does,
|
|
1020
|
+
since a fixed panel has no position of its own until
|
|
1021
|
+
somebody measures the item for it. */
|
|
975
1022
|
onClick={(e) => {
|
|
976
1023
|
if (isCoarsePointer) {
|
|
977
1024
|
const next = openGroup === n.id ? null : n.id;
|
|
978
1025
|
setOpenGroup(next);
|
|
979
|
-
|
|
1026
|
+
|
|
1027
|
+
if (next !== null) {
|
|
1028
|
+
pinDropdown(e.currentTarget.closest('li'));
|
|
1029
|
+
}
|
|
1030
|
+
|
|
980
1031
|
return;
|
|
981
1032
|
}
|
|
982
1033
|
|
|
@@ -363,13 +363,29 @@
|
|
|
363
363
|
|
|
364
364
|
> ul {
|
|
365
365
|
width: 220px;
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
366
|
+
/* FIXED, not absolute, and the strip above is the reason.
|
|
367
|
+
`.navwrap` is `overflow-x: auto`, and a scroll container
|
|
368
|
+
clips on BOTH axes however `visible` the other one is
|
|
369
|
+
written — so an absolutely-positioned panel hanging below
|
|
370
|
+
the bar was cut off at the bar's own bottom edge. The old
|
|
371
|
+
way out was to flip the strip to `overflow-x: visible`
|
|
372
|
+
while a group was hovered, which destroys the scroll
|
|
373
|
+
container: the browser resets `scrollLeft` to 0, the item
|
|
374
|
+
slides out from under the pointer, and the menu closes
|
|
375
|
+
itself in the same gesture. A fixed panel is outside the
|
|
376
|
+
scroller's flow altogether, so the clip never applies and
|
|
377
|
+
the scroller is never touched — Navigation.jsx measures the
|
|
378
|
+
item and hands its two coordinates down as custom
|
|
379
|
+
properties. The fallbacks below are the old anchoring, for
|
|
380
|
+
the frame before a measurement lands.
|
|
381
|
+
|
|
382
|
+
ONE TRADE-OFF: an ancestor carrying `transform`, `filter`
|
|
383
|
+
or `contain: paint` becomes the containing block for its
|
|
384
|
+
fixed descendants, and would trap the panel inside the bar
|
|
385
|
+
again. */
|
|
386
|
+
position: fixed;
|
|
387
|
+
top: var(--nav-sub-top, 100%);
|
|
388
|
+
left: var(--nav-sub-left, 0);
|
|
373
389
|
height: auto;
|
|
374
390
|
margin: 0;
|
|
375
391
|
/* The rows used to sit flush against the panel edge, which is
|
|
@@ -552,9 +568,15 @@
|
|
|
552
568
|
|
|
553
569
|
> ul {
|
|
554
570
|
width: 220px;
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
571
|
+
/* Fixed for the same reason as `.nav-item` above — the strip
|
|
572
|
+
is a scroll container and clips on both axes, and flipping
|
|
573
|
+
its overflow to escape the clip reset `scrollLeft` out from
|
|
574
|
+
under the pointer. Coordinates come from Navigation.jsx per
|
|
575
|
+
item; see the long note there for the transform/filter
|
|
576
|
+
caveat. */
|
|
577
|
+
position: fixed;
|
|
578
|
+
top: var(--nav-sub-top, 100%);
|
|
579
|
+
left: var(--nav-sub-left, 0);
|
|
558
580
|
height: auto;
|
|
559
581
|
margin: 0;
|
|
560
582
|
padding: 4px;
|