graphen 2.0.8 → 2.0.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graphen",
3
- "version": "2.0.8",
3
+ "version": "2.0.9",
4
4
  "description": "Graphen is a small library, that keeps reusable blocks of UI and helps making application design consistent across multiple projects.",
5
5
  "main": "src/index.ts",
6
6
  "files": [
@@ -1,6 +1,6 @@
1
1
  describe("dropdown menu", () => {
2
2
  it("should open dropdown and pick a value", () => {
3
- cy.visit("http://localhost:3000");
3
+ cy.visit("/");
4
4
 
5
5
  cy.get("#dropdown").scrollIntoView();
6
6
 
@@ -1,6 +1,6 @@
1
1
  .gc-submenu {
2
2
  display: none;
3
- position: absolute;
3
+ position: fixed;
4
4
  background-color: transparent;
5
5
  z-index: $zlayer-top;
6
6
 
@@ -1,6 +1,8 @@
1
- import React, { useCallback, useState } from "react";
1
+ import React, { useCallback, useLayoutEffect, useRef, useState } from "react";
2
2
  import classNames from "classnames";
3
3
 
4
+ const SUBMENU_VIEWPORT_MARGIN = 10;
5
+
4
6
  type Props = {
5
7
  className?: string;
6
8
  label: React.ReactNode;
@@ -17,11 +19,52 @@ function NavigationOption({
17
19
  children = null,
18
20
  }: Props) {
19
21
  const [isOpened, setIsOpened] = useState(false);
22
+ const [submenuPosition, setSubmenuPosition] = useState({ top: 0, left: 0 });
23
+ const triggerReference = useRef<HTMLButtonElement>(null);
24
+ const submenuReference = useRef<HTMLDivElement>(null);
20
25
 
21
26
  const toggleSubmenu = useCallback(() => {
22
27
  setIsOpened((previousIsOpened) => !previousIsOpened);
23
28
  }, [setIsOpened]);
24
29
 
30
+ const updateSubmenuPosition = useCallback(() => {
31
+ const trigger = triggerReference.current;
32
+ const submenu = submenuReference.current;
33
+
34
+ if (!trigger || !submenu) {
35
+ return;
36
+ }
37
+
38
+ const triggerRect = trigger.getBoundingClientRect();
39
+ const maximumLeft =
40
+ window.innerWidth - submenu.offsetWidth - SUBMENU_VIEWPORT_MARGIN;
41
+
42
+ setSubmenuPosition({
43
+ top: triggerRect.bottom,
44
+ left: Math.max(
45
+ SUBMENU_VIEWPORT_MARGIN,
46
+ Math.min(triggerRect.left, maximumLeft)
47
+ ),
48
+ });
49
+ }, [setSubmenuPosition]);
50
+
51
+ useLayoutEffect(() => {
52
+ if (!isOpened) {
53
+ return undefined;
54
+ }
55
+
56
+ updateSubmenuPosition();
57
+
58
+ // capture phase catches scrolling inside consumer overflow containers
59
+ window.addEventListener("scroll", updateSubmenuPosition, true);
60
+ window.addEventListener("resize", updateSubmenuPosition);
61
+
62
+ return () => {
63
+ window.removeEventListener("scroll", updateSubmenuPosition, true);
64
+ window.removeEventListener("resize", updateSubmenuPosition);
65
+ };
66
+ }, [isOpened, updateSubmenuPosition]);
67
+
25
68
  const closeSubmenuOnBlur = useCallback(
26
69
  (event: React.FocusEvent<HTMLLIElement>) => {
27
70
  if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {
@@ -65,6 +108,7 @@ function NavigationOption({
65
108
  onKeyDown={closeSubmenuOnEscape}
66
109
  >
67
110
  <button
111
+ ref={triggerReference}
68
112
  type="button"
69
113
  className="gc-navigation__link"
70
114
  aria-haspopup="true"
@@ -73,7 +117,11 @@ function NavigationOption({
73
117
  >
74
118
  {label}
75
119
  </button>
76
- <div className={submenuClasses}>
120
+ <div
121
+ ref={submenuReference}
122
+ className={submenuClasses}
123
+ style={{ top: submenuPosition.top, left: submenuPosition.left }}
124
+ >
77
125
  <div className="gc-submenu__content">{children}</div>
78
126
  </div>
79
127
  </li>
@@ -0,0 +1,30 @@
1
+ describe("navigation submenu", () => {
2
+ it("should anchor submenu to its parent option in a scrolled navigation", () => {
3
+ cy.visit("/");
4
+
5
+ cy.get("#navigation").scrollIntoView();
6
+
7
+ cy.get("#navigation .gc-navigation")
8
+ .eq(1)
9
+ .parent()
10
+ .scrollTo("right")
11
+ .within(() => {
12
+ cy.contains("button.gc-navigation__link", "Platform").click();
13
+ });
14
+
15
+ cy.window().then((win) => {
16
+ cy.contains("button.gc-navigation__link", "Platform").then(($trigger) => {
17
+ const triggerRect = $trigger[0].getBoundingClientRect();
18
+
19
+ cy.get(".gc-submenu--opened").then(($submenu) => {
20
+ const submenuRect = $submenu[0].getBoundingClientRect();
21
+
22
+ expect(submenuRect.top).to.be.closeTo(triggerRect.bottom, 1);
23
+ expect(submenuRect.left).to.be.at.most(triggerRect.left + 1);
24
+ expect(submenuRect.left).to.be.at.least(0);
25
+ expect(submenuRect.right).to.be.at.most(win.innerWidth);
26
+ });
27
+ });
28
+ });
29
+ });
30
+ });
package/src/example.tsx CHANGED
@@ -1190,6 +1190,43 @@ function App() {
1190
1190
  <NavigationOption label="Resources" href="#" />
1191
1191
  </Navigation>
1192
1192
  </Demo>
1193
+ <p className="docs-section-desc">
1194
+ Inside a narrow container the navigation scrolls horizontally. The
1195
+ submenu follows its scrolled parent option and stays within the
1196
+ viewport.
1197
+ </p>
1198
+ <Demo
1199
+ stageClass="column"
1200
+ code={`<div style={{ maxWidth: 280, overflowX: "auto" }}>
1201
+ <Navigation>
1202
+ <NavigationOption label="Overview" href="#" isActive />
1203
+ <NavigationOption label="Admin" href="#" />
1204
+ <NavigationOption label="Platform">
1205
+ <a className="gc-submenu__item" href="#">Users</a>
1206
+ <a className="gc-submenu__item" href="#">Events</a>
1207
+ </NavigationOption>
1208
+ <NavigationOption label="e-Shop" href="#" />
1209
+ <NavigationOption label="Account" href="#" />
1210
+ </Navigation>
1211
+ </div>`}
1212
+ >
1213
+ <div style={{ maxWidth: 280, overflowX: "auto" }}>
1214
+ <Navigation>
1215
+ <NavigationOption label="Overview" href="#" isActive />
1216
+ <NavigationOption label="Admin" href="#" />
1217
+ <NavigationOption label="Platform">
1218
+ <a className="gc-submenu__item" href="#">
1219
+ Users
1220
+ </a>
1221
+ <a className="gc-submenu__item" href="#">
1222
+ Events
1223
+ </a>
1224
+ </NavigationOption>
1225
+ <NavigationOption label="e-Shop" href="#" />
1226
+ <NavigationOption label="Account" href="#" />
1227
+ </Navigation>
1228
+ </div>
1229
+ </Demo>
1193
1230
  </section>
1194
1231
 
1195
1232
  <section className="docs-section" id="segmented-control">
@@ -1,6 +1,6 @@
1
1
  describe("Colors", () => {
2
2
  before(() => {
3
- cy.visit("http://localhost:3000");
3
+ cy.visit("/");
4
4
  });
5
5
 
6
6
  it("should make a screenshot", () => {