Skip to content

Commit 24cf5b4

Browse files
authored
Merge pull request #120 from mgilardi/dev
Basic shadow DOM + Salesforce support
2 parents f01d5d8 + ea0802b commit 24cf5b4

File tree

7 files changed

+112
-14
lines changed

7 files changed

+112
-14
lines changed

packages/components-library/README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,3 @@ yarn test
107107

108108
## Use on static HTML page
109109

110-

packages/components-library/src/components/Header/index.js

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useEffect, useRef } from "preact/compat";
1+
import { useState, useEffect, useRef, useCallback } from "preact/compat";
22
import PropTypes from "prop-types";
33
import * as S from "./styles";
44
import { Nav } from "../Nav";
@@ -52,7 +52,62 @@ const Header = ({
5252
setSrollPosition(position);
5353
};
5454

55+
const killEvent = useCallback((e) => {
56+
e.preventDefault();
57+
e.stopPropagation();
58+
e.stopImmediatePropagation();
59+
}, []);
60+
61+
/**
62+
* This function solves a bug where all onclick functions are overwritten
63+
* with the first declared onclick function. This is known to occur
64+
* when inserting the header component in a Lightning Web Component on
65+
* Salesforce and may occur in other scenarios as well.
66+
*
67+
* When this function overwrites all other onclick functions, it then
68+
* checks the event's target's data-onclick-identifier attribute to determine
69+
* what was clicked on and intiates the correct process.
70+
*
71+
* When the bug does not occur, this function should never be called.
72+
*/
73+
const onClickCallbackOverride = useCallback((e) => {
74+
let whatWasClicked = e.target;
75+
let limit = 100; // prevent infinite loop
76+
77+
// Climb DOM tree until someone has an identifier
78+
while (limit > 0 && whatWasClicked.dataset.onclickIdentifier === undefined) {
79+
whatWasClicked = whatWasClicked.parentNode;
80+
limit--;
81+
}
5582

83+
// Action depends on what was clicked
84+
const identifier = whatWasClicked.dataset.onclickIdentifier;
85+
if (identifier === "universal-search-bar") {
86+
setSearchOpen(true);
87+
} else if (identifier === "mobile-dropdown") {
88+
toggle();
89+
} else if (identifier.includes("toggle-dropdown.")) {
90+
setSearchOpen(false);
91+
clearSearchBar(whatWasClicked);
92+
toggleNavDropdown(whatWasClicked)
93+
} else if (identifier === "leave-open") {
94+
killEvent();
95+
}
96+
}, []);
97+
const toggleNavDropdown = (clickedDOM) => {
98+
navRef.current.forceToggle(clickedDOM);
99+
}
100+
const clearSearchBar = (domElement) => {
101+
let searchUp = domElement;
102+
let limit = 100; // prevent infinite loop
103+
while (limit > 0 && searchUp.dataset.onclickIdentifier !== 'top-of-header') {
104+
limit--;
105+
searchUp = searchUp.parentNode
106+
}
107+
if (searchUp.querySelector('[data-onclick-identifier = "universal-search-bar"]').querySelector("input").value.length > 0){
108+
searchUp.querySelector('[data-onclick-identifier = "universal-search-bar"]').querySelector("input").value = "";
109+
}
110+
}
56111

57112
// Attach scroll event lister which will update the scrollPosition state
58113
// when window scrolled
@@ -68,6 +123,7 @@ const Header = ({
68123
const universalRef = useRef(null);
69124
const logoRef = useRef(null);
70125
const titleRef = useRef(null);
126+
const navRef = useRef(null);
71127

72128
// Calculate the mobile nav menu max-height every time the mobile nav is opened
73129
// or the viewport changes size
@@ -94,7 +150,9 @@ const Header = ({
94150
? "scrolled"
95151
: ""
96152
}
153+
data-onclick-identifier = "top-of-header"
97154
>
155+
<div onmousedown={killEvent} onclick={onClickCallbackOverride} data-onclick-identifier="no-action"></div>
98156
<S.UniversalNav open={mobileOpen} ref={universalRef} {...{ searchOpen }}>
99157
<S.UniversalNavLinks>
100158
<a href="https://www.asu.edu/">ASU home</a>
@@ -115,6 +173,7 @@ const Header = ({
115173
}}
116174
mobileOpen={mobileOpen}
117175
logo={<Logo {...logo} ref={logoRef} />}
176+
data-onclick-identifier = "mobile-dropdown"
118177
>
119178
{props.dangerouslyGenerateStub ? (
120179
<div id="asu-generated-stub" />
@@ -137,6 +196,7 @@ const Header = ({
137196
breakpoint,
138197
expandOnHover
139198
}}
199+
ref={navRef}
140200
/>
141201
</>
142202
)}

packages/components-library/src/components/Nav/DropNav.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,11 @@ const DropNav = forwardRef(
5959
//navOpen(pIndex);
6060
setFocus([pIndex, -1, -1]);
6161
}}
62+
onClick={e => {}}
6263
tabIndex="0"
6364
ref={ref}
65+
data-onclick-identifier = {"toggle-dropdown." + pIndex}
66+
data-onclick-dropdown-open = "false"
6467
>
6568
{text}{" "}
6669
<S.IconChevronDown sr={text} className={isOpen ? "open" : ""} />

packages/components-library/src/components/Nav/index.js

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useState, useMemo, useRef, createRef } from "preact/compat";
1+
import { useEffect, useState, useMemo, useRef, createRef, forwardRef, useImperativeHandle } from "preact/compat";
22
import PropTypes from "prop-types";
33
import NavItem from "../NavItem";
44
import DropNav from "./DropNav";
@@ -10,21 +10,55 @@ import * as S from "./styles";
1010
* Render entire Nav.
1111
* @param {} props
1212
*/
13-
const Nav = ({
13+
const Nav = forwardRef (({
1414
navTree,
1515
width,
1616
mobileOpen,
1717
maxMobileHeight,
1818
buttons,
1919
injectStyles,
2020
breakpoint,
21-
expandOnHover,
22-
}) => {
21+
expandOnHover
22+
}, ref) => {
2323
/** State to keep track of currently focused Nav Item */
2424
const [focused, setFocus] = useState([-1, -1, -1]);
2525
/** State for keeping track of open dropdown nav */
2626
const [open, setOpen] = useState(-1);
2727

28+
useImperativeHandle(
29+
ref,
30+
() => ({
31+
forceToggle(whatWasClicked) {
32+
let isAriaOpen = false;
33+
34+
// Check if already open
35+
for (let i=0; i<whatWasClicked.attributes.length; i++) {
36+
let attribute = whatWasClicked.attributes[i];
37+
if (attribute.name === 'aria-expanded') {
38+
if (attribute.value === 'true') {
39+
isAriaOpen = true;
40+
}
41+
}
42+
}
43+
44+
const id = whatWasClicked.dataset.onclickIdentifier;
45+
if (!isAriaOpen || (isAriaOpen !== (whatWasClicked.dataset.onclickDropdownOpen === 'true'))) {
46+
setOpen(parseInt(id.substring(id.indexOf(".")+1)));
47+
whatWasClicked.dataset.onclickDropdownOpen = 'true';
48+
} else {
49+
setOpen(-1);
50+
whatWasClicked.dataset.onclickDropdownOpen = 'false';
51+
}
52+
},
53+
54+
forceOpen(whatWasClicked) {
55+
const id = whatWasClicked.dataset.onclickIdentifier;
56+
setOpen(parseInt(id.substring(id.indexOf(".")+1)));
57+
whatWasClicked.dataset.onclickDropdownOpen = 'true';
58+
}
59+
}),
60+
)
61+
2862
const setFocusCallback = newFocus => {
2963
setFocus(newFocus);
3064
};
@@ -304,7 +338,7 @@ const Nav = ({
304338
}
305339
</S.Nav>
306340
);
307-
};
341+
});
308342

309343
Nav.propTypes = {
310344
navTree: PropTypes.arrayOf(PropTypes.object),

packages/components-library/src/components/Nav/styles.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ const dropdownContainerStyles = breakpoint => css`
334334

335335
const DropdownContainer = props => {
336336
return (
337-
<div class={cx("dropdown", props.open ? "open" : "", props.class)}>
337+
<div class={cx("dropdown", props.open ? "open" : "", props.class)} data-onclick-identifier = {"leave-open"} onMouseDown={e => {}} onClick={e => {}}>
338338
<div>{props.children}</div>
339339
{props.buttons ? (
340340
<div class="button-row">

packages/components-library/src/components/Search/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ const UniversalSearch = ({ type, open, setOpen, mobile}) => {
103103
onfocusin={onFocusCallback}
104104
onfocusout={onBlurCallBack}
105105
onClick={onClickCallback}
106+
data-onclick-identifier="universal-search-bar"
106107
>
107108
<Search {...{ open, type, inputRef, mobile }} />
108109
</S.UniversalSearch>

packages/components-library/src/helpers.js

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,13 @@ const checkSSOCookie = () => {
2323
const cookies = document.cookie.split(";"); // try to parse out the username from SSONAME cookie
2424

2525
for (let i = 0; i < cookies.length; i++) {
26-
if (cookies[i].indexOf("SSONAME") > 0) {
27-
if (cookies[i].substring(9) == "") {
26+
const cookie = cookies[i];
27+
if (cookie.includes("SSONAME")) {
28+
if (cookie.substring(cookie.indexOf('=')+1) == "") {
2829
break;
2930
}
3031

31-
loginStatus.userName = cookies[i].substring(9);
32+
loginStatus.userName = cookie.substring(cookie.indexOf('=')+1);
3233
loginStatus.loggedIn = true;
3334
break;
3435
}
@@ -87,7 +88,7 @@ const checkFirstLoad = root => {
8788
* @param {*} target - The ID of the containing <div> where the header should
8889
* be either hydrated or rendered.
8990
*/
90-
const initHeader = (props, target = "headerContainer", hydrate = false) => {
91+
const initHeader = (props, target = "headerContainer", hydrate = false, rootOfDOM = document) => {
9192
const { loggedIn, userName, loginLink, ...theRest } = props;
9293
const fullLoginUrl = loginLink
9394
? loginLink
@@ -122,9 +123,9 @@ const initHeader = (props, target = "headerContainer", hydrate = false) => {
122123
};
123124

124125
if (hydrate) {
125-
HydratePreact(Header, headerProps, document.getElementById(target));
126+
HydratePreact(Header, headerProps, rootOfDOM.querySelector('#' + target));
126127
} else {
127-
RenderPreact(Header, headerProps, document.getElementById(target));
128+
RenderPreact(Header, headerProps, rootOfDOM.querySelector('#' + target));
128129
}
129130
};
130131

0 commit comments

Comments
 (0)