@thecb/components 11.2.13 → 11.2.14-beta.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thecb/components",
3
- "version": "11.2.13",
3
+ "version": "11.2.14-beta.0",
4
4
  "description": "Common lib for CityBase react components",
5
5
  "main": "dist/index.cjs.js",
6
6
  "typings": "dist/index.d.ts",
Binary file
@@ -0,0 +1,258 @@
1
+ import React, {
2
+ useCallback,
3
+ useContext,
4
+ useEffect,
5
+ useRef,
6
+ useState
7
+ } from "react";
8
+ import { test } from "ramda";
9
+ import {
10
+ Box,
11
+ Cluster,
12
+ constants,
13
+ Stack,
14
+ Paragraph,
15
+ ExternalLink,
16
+ InternalLink,
17
+ Switcher,
18
+ withWindowSize
19
+ } from "@thecb/components";
20
+ import { ThemeContext } from "styled-components";
21
+ import { URL_TEST } from "../../../constants/regex_constants";
22
+ import HeaderItem from "./HeaderItem";
23
+ import RightArrowIcon from "../../icons/RightArrowIcon";
24
+ import NavLabel from "./NavLabel";
25
+ import NavLink, { NavLinkLevels } from "./NavLink";
26
+ import { getCallToActionInfo } from "../../../util/dataAdapters";
27
+
28
+ const KEY_ARROW_DOWN = 40;
29
+ const KEY_ARROW_UP = 38;
30
+ const KEY_TAB = 9;
31
+
32
+ const Link = ({ url, id, onSetRef, children }) => {
33
+ return test(URL_TEST, url) ? (
34
+ <ExternalLink href={url} ref={element => onSetRef(id, element)}>
35
+ {children}
36
+ </ExternalLink>
37
+ ) : (
38
+ <InternalLink to={url} ref={element => onSetRef(id, element)}>
39
+ {children}
40
+ </InternalLink>
41
+ );
42
+ };
43
+
44
+ const LinkMenu = ({
45
+ link,
46
+ onChangeMenu,
47
+ hamburgerMenuStyle,
48
+ kbNavUsed,
49
+ themeValues
50
+ }) => {
51
+ const { isMobile } = useContext(ThemeContext);
52
+ const linkElements = useRef({});
53
+ const [focusLink, setFocusLink] = useState(-1);
54
+
55
+ const setElementFocus = key => {
56
+ if (kbNavUsed) {
57
+ linkElements.current[key].focus();
58
+ }
59
+ };
60
+
61
+ const handleSetRef = useCallback((id, element) => {
62
+ return (linkElements.current[id] = element);
63
+ }, []);
64
+
65
+ useEffect(() => {
66
+ const ref1Key = Object.keys(linkElements.current)[0];
67
+ setElementFocus(ref1Key);
68
+ setFocusLink(0);
69
+ }, []);
70
+
71
+ const SmallLink = ({ text, showArrow = false }) => (
72
+ <Cluster justify="flex-start" align="center">
73
+ <Paragraph variant={isMobile ? "p" : "pS"} color={themeValues.linkColor}>
74
+ {text}
75
+ </Paragraph>
76
+ {showArrow && <RightArrowIcon size={18} color={themeValues.linkColor} />}
77
+ </Cluster>
78
+ );
79
+
80
+ const handleKeyDown = event => {
81
+ switch (event.keyCode) {
82
+ case KEY_TAB:
83
+ event.preventDefault();
84
+ onChangeMenu(event.shiftKey ? "previous" : "next");
85
+ break;
86
+ case KEY_ARROW_DOWN:
87
+ case KEY_ARROW_UP:
88
+ event.preventDefault();
89
+ const increment = event.keyCode === KEY_ARROW_DOWN ? 1 : -1;
90
+ const length = Object.keys(linkElements.current).length;
91
+ const newFocus = (focusLink + (increment % length) + length) % length;
92
+ const newFocusKey = Object.keys(linkElements.current)[newFocus];
93
+ setFocusLink(newFocus);
94
+ setElementFocus(newFocusKey);
95
+ break;
96
+ default:
97
+ break;
98
+ }
99
+ };
100
+
101
+ const renderSubLinkSectionName = subLink => {
102
+ const { ctaUrl: subLinkUrl } = getCallToActionInfo(
103
+ subLink.callToActionLink,
104
+ "",
105
+ subLink.nameUrl
106
+ );
107
+ if (hamburgerMenuStyle) {
108
+ return subLinkUrl ? (
109
+ <NavLink
110
+ url={subLinkUrl}
111
+ label={subLink.name}
112
+ themeValues={themeValues}
113
+ navLevel={NavLinkLevels.SUB_MENU}
114
+ />
115
+ ) : (
116
+ <NavLabel text={subLink.name} themeValues={themeValues} navLevel={1} />
117
+ );
118
+ } else {
119
+ return subLinkUrl ? (
120
+ <Link url={subLinkUrl} id={subLink.id} onSetRef={handleSetRef}>
121
+ <HeaderItem
122
+ name={subLink?.name}
123
+ showArrow={!hamburgerMenuStyle}
124
+ themeValues={themeValues}
125
+ />
126
+ </Link>
127
+ ) : (
128
+ <HeaderItem
129
+ name={subLink?.name}
130
+ showArrow={false}
131
+ themeValues={themeValues}
132
+ />
133
+ );
134
+ }
135
+ };
136
+
137
+ const renderSubLinkLink = (smallLink, index) => {
138
+ if (hamburgerMenuStyle) {
139
+ return (
140
+ <NavLink
141
+ key={`navlink-${index}`}
142
+ url={smallLink.externalUrl}
143
+ label={smallLink.text}
144
+ themeValues={themeValues}
145
+ navLevel={NavLinkLevels.DETAIL}
146
+ />
147
+ );
148
+ } else {
149
+ return (
150
+ <Box padding={"0"} key={`link-${index}`}>
151
+ <Link
152
+ url={smallLink.externalUrl}
153
+ id={smallLink.id}
154
+ onSetRef={handleSetRef}
155
+ >
156
+ <SmallLink text={smallLink.text} />
157
+ </Link>
158
+ </Box>
159
+ );
160
+ }
161
+ };
162
+
163
+ const { ctaUrl } = getCallToActionInfo(
164
+ link.callToActionLink,
165
+ "",
166
+ link.nameUrl
167
+ );
168
+
169
+ return (
170
+ <Box
171
+ padding={hamburgerMenuStyle ? "16px 0" : "0"}
172
+ background={
173
+ hamburgerMenuStyle ? constants.colors.ATHENS_GREY : "transparent"
174
+ }
175
+ onKeyDown={handleKeyDown}
176
+ extraStyles={
177
+ hamburgerMenuStyle ? "margin-left: 16px; margin-right: 16px;" : ""
178
+ }
179
+ >
180
+ <Switcher breakpoint="45rem" largeChild="1" largeChildSize="2">
181
+ {hamburgerMenuStyle ? (
182
+ <Box padding={hamburgerMenuStyle ? "0 16px 16px" : 0}>
183
+ <Paragraph variant="pS" color={constants.colors.CHARADE_GREY}>
184
+ {link?.description}
185
+ </Paragraph>
186
+ </Box>
187
+ ) : (
188
+ <Box key="title-desc" padding={"0 16px 0 4px"}>
189
+ <Stack childGap="8px">
190
+ <Box padding="0">
191
+ {ctaUrl ? (
192
+ <Box padding="0">
193
+ <Link url={ctaUrl} id={link.id} onSetRef={handleSetRef}>
194
+ <HeaderItem
195
+ name={link?.name}
196
+ showArrow={!hamburgerMenuStyle}
197
+ themeValues={themeValues}
198
+ />
199
+ </Link>
200
+ </Box>
201
+ ) : (
202
+ <HeaderItem
203
+ name={link?.name}
204
+ showArrow={ctaUrl !== ""}
205
+ themeValues={themeValues}
206
+ />
207
+ )}
208
+ </Box>
209
+ <Box padding={"16px 24px 16px 0"}>
210
+ <Paragraph variant="pS">{link?.description}</Paragraph>
211
+ </Box>
212
+ </Stack>
213
+ </Box>
214
+ )}
215
+ {link?.linkLists?.map((subLink, index) => (
216
+ <Box
217
+ padding={hamburgerMenuStyle ? "0" : "0 24px"}
218
+ key={`subLink-${index}`}
219
+ >
220
+ <Stack childGap={"1rem"}>
221
+ {renderSubLinkSectionName(subLink)}
222
+ {subLink?.links?.map((smallLink, index) =>
223
+ renderSubLinkLink(smallLink, index)
224
+ )}
225
+ </Stack>
226
+ </Box>
227
+ ))}
228
+ <Box padding={hamburgerMenuStyle ? "16px 0" : "0 24px"}>
229
+ <Stack childGap={"1rem"}>
230
+ <Box padding={"0"}>
231
+ {hamburgerMenuStyle ? (
232
+ <NavLabel
233
+ text={"Featured Services"}
234
+ themeValues={themeValues}
235
+ navLevel={1}
236
+ />
237
+ ) : (
238
+ <HeaderItem
239
+ name="Featured Services"
240
+ showArrow={false}
241
+ themeValues={themeValues}
242
+ color={themeValues.linkColor}
243
+ weight={constants.fontWeights.FONT_WEIGHT_SEMIBOLD}
244
+ variant="p"
245
+ />
246
+ )}
247
+ </Box>
248
+ {link?.featuredLinks?.links?.map((featuredLink, index) =>
249
+ renderSubLinkLink(featuredLink, index)
250
+ )}
251
+ </Stack>
252
+ </Box>
253
+ </Switcher>
254
+ </Box>
255
+ );
256
+ };
257
+
258
+ export default withWindowSize(LinkMenu);
@@ -0,0 +1,138 @@
1
+ import React, { useContext } from "react";
2
+ import { Box, Cover, Heading, Motion, withWindowSize } from "@thecb/components";
3
+ import { ThemeContext } from "styled-components";
4
+ import LinkMenu from "./LinkMenu";
5
+ import HamburgerNavSection from "./HamburgerNavSection";
6
+ import { borderWrapper } from "./style";
7
+
8
+ const NavLinks = ({
9
+ navigation,
10
+ hamburgerMenuStyle,
11
+ themeValues,
12
+ navMenuOpen,
13
+ hamburgerOpenSection,
14
+ selectedNavMenu,
15
+ setSearchMenuOpen,
16
+ setSelectedMenu,
17
+ setNavMenuOpen,
18
+ setIsAnimating,
19
+ setKeyboardNavUsed,
20
+ setHamburgerOpenSection
21
+ }) => {
22
+ const { isMobile } = useContext(ThemeContext);
23
+ const selectMenu = (index, kbNavUsed) => {
24
+ setKeyboardNavUsed(kbNavUsed);
25
+ if (navMenuOpen !== true) {
26
+ setSearchMenuOpen(false);
27
+ setSelectedMenu(index);
28
+ setNavMenuOpen(true);
29
+ setIsAnimating(true);
30
+ setTimeout(() => {
31
+ setIsAnimating(false);
32
+ }, 600);
33
+ } else if (selectedNavMenu !== index) {
34
+ setSelectedMenu(index);
35
+ setSearchMenuOpen(false);
36
+ }
37
+ };
38
+
39
+ return (
40
+ <>
41
+ {navigation?.sections?.map((link, index) => {
42
+ debugger;
43
+ return (
44
+ <Box
45
+ padding="0"
46
+ key={`nav-link-${index}`}
47
+ extraStyles={!hamburgerMenuStyle && `height: 102px;`}
48
+ >
49
+ <Cover
50
+ singleChild
51
+ minHeight={isMobile ? "0%" : "100%"}
52
+ key={`section-${index}`}
53
+ >
54
+ {hamburgerMenuStyle ? (
55
+ <HamburgerNavSection
56
+ link={link}
57
+ toggleSection={() => {
58
+ link?.id === hamburgerOpenSection
59
+ ? setHamburgerOpenSection("")
60
+ : setHamburgerOpenSection(link?.id);
61
+ }}
62
+ isOpen={hamburgerOpenSection === link?.id}
63
+ name={link?.name}
64
+ themeValues={themeValues}
65
+ >
66
+ <Motion
67
+ padding="0"
68
+ transition={{ duration: 0.3 }}
69
+ positionTransition
70
+ extraStyles={`transform-origin: 100% 0;`}
71
+ >
72
+ <LinkMenu
73
+ key={link.id}
74
+ link={link}
75
+ hamburgerMenuStyle={hamburgerMenuStyle}
76
+ themeValues={themeValues}
77
+ kbNavUsed={false}
78
+ onChangeMenu={() => {
79
+ setNavMenuOpen(false);
80
+ setSelectedMenu(100);
81
+ }}
82
+ />
83
+ </Motion>
84
+ </HamburgerNavSection>
85
+ ) : (
86
+ <Box
87
+ padding="0"
88
+ as="button"
89
+ background="transparent"
90
+ tabIndex="0"
91
+ onClick={() => {
92
+ setSearchMenuOpen(false);
93
+ setSelectedMenu(index);
94
+ setNavMenuOpen(!navMenuOpen);
95
+ setIsAnimating(true);
96
+ setTimeout(() => {
97
+ setIsAnimating(false);
98
+ }, 600);
99
+ }}
100
+ onMouseEnter={() => selectMenu(index, false)}
101
+ onFocus={() => selectMenu(index, true)}
102
+ >
103
+ <Box padding="0 1.5rem">
104
+ <Heading
105
+ variant="h6"
106
+ color={isMobile ? "#FFFFFF" : themeValues.linkColor}
107
+ extraStyles={
108
+ isMobile ? "font-size: 14px;" : `font-size: 16px;`
109
+ }
110
+ >
111
+ {link?.name}
112
+ </Heading>
113
+ </Box>
114
+ </Box>
115
+ )}
116
+ {!hamburgerMenuStyle && (
117
+ <Motion
118
+ padding="0"
119
+ minWidth="100%"
120
+ variants={borderWrapper}
121
+ animate={navMenuOpen ? "open" : "closed"}
122
+ layoutTransition
123
+ extraStyles={`border-bottom: 3px solid ${
124
+ selectedNavMenu === index
125
+ ? `${themeValues.linkColor}`
126
+ : `transparent`
127
+ }`}
128
+ />
129
+ )}
130
+ </Cover>
131
+ </Box>
132
+ );
133
+ })}
134
+ </>
135
+ );
136
+ };
137
+
138
+ export default NavLinks;
@@ -4,33 +4,20 @@ import { Box, Motion } from "../../atoms/layouts";
4
4
  import { fallbackValues } from "./NavMenu.theme.js";
5
5
  import { themeComponent } from "../../../util/themeUtils";
6
6
 
7
+ // Use transforms (x) rather than left/right
7
8
  const menuVariants = {
8
9
  invisible: {
9
- left: "-100vw",
10
- right: "100vw",
10
+ x: "-100vw",
11
11
  transition: {
12
- right: {
13
- ease: "easeOut",
14
- duration: 500
15
- },
16
- left: {
17
- ease: "easeOut",
18
- duration: 500
19
- }
12
+ ease: "easeOut",
13
+ duration: 0.5
20
14
  }
21
15
  },
22
16
  visible: {
23
- left: "0",
24
- right: "0",
17
+ x: "0",
25
18
  transition: {
26
- left: {
27
- ease: "easeIn",
28
- duration: 500
29
- },
30
- right: {
31
- ease: "easeIn",
32
- duration: 500
33
- }
19
+ ease: "easeIn",
20
+ duration: 0.5
34
21
  }
35
22
  }
36
23
  };
@@ -38,6 +25,8 @@ const menuVariants = {
38
25
  const ImposterMenu = styled(Motion)`
39
26
  position: fixed;
40
27
  top: ${({ headerSize }) => headerSize};
28
+ left: 0;
29
+ right: 0;
41
30
  `;
42
31
 
43
32
  const NavMenuMobile = ({
@@ -58,7 +47,11 @@ const NavMenuMobile = ({
58
47
  <Box
59
48
  width="100vw"
60
49
  padding="1rem 0.5rem"
61
- extraStyles={`position: relative; max-width: 400px; height: calc(100vh - 72px);`}
50
+ extraStyles={`
51
+ position: relative;
52
+ max-width: 400px;
53
+ height: calc(100vh - 72px);
54
+ `}
62
55
  background={themeValues.backgroundColor}
63
56
  >
64
57
  {menuContent}
@@ -0,0 +1,9 @@
1
+ import { Canvas, Meta, Title, Story, Controls } from '@storybook/blocks';
2
+
3
+ import * as NavMenuMobileStories from './NavMenuMobile.stories.js';
4
+
5
+ <Meta of={NavMenuMobileStories} />
6
+
7
+ <Title />
8
+
9
+ <Story />
@@ -0,0 +1,124 @@
1
+ import React, { useState } from "react";
2
+ import NavMenuMobile from "./NavMenuMobile";
3
+ // import { Box, Button } from "../../layouts";
4
+ import { Box } from "../../atoms/layouts";
5
+ import { ButtonWithAction } from "../../atoms";
6
+
7
+ import { fallbackValues } from "./NavMenu.theme";
8
+
9
+ export default {
10
+ title: "Molecules/NavMenuMobile",
11
+ component: NavMenuMobile,
12
+ tags: ["!autodocs"],
13
+ parameters: {
14
+ layout: "centered",
15
+ controls: { expanded: true }
16
+ },
17
+ args: {
18
+ id: "nav-menu-mobile",
19
+ menuContent: (
20
+ <Box>
21
+ <p>Menu Item 1</p>
22
+ <p>Menu Item 2</p>
23
+ <p>Menu Item 3</p>
24
+ </Box>
25
+ ),
26
+ visible: false,
27
+ headerSize: "72px",
28
+ themeValues: fallbackValues
29
+ },
30
+ argTypes: {
31
+ id: {
32
+ description: "Unique identifier for the menu",
33
+ table: {
34
+ type: { summary: "string" },
35
+ defaultValue: { summary: "nav-menu-mobile" }
36
+ }
37
+ },
38
+ menuContent: {
39
+ description: "Content to display inside the menu",
40
+ table: {
41
+ type: { summary: "ReactNode" },
42
+ defaultValue: { summary: "undefined" }
43
+ }
44
+ },
45
+ visible: {
46
+ description: "Whether the menu is visible",
47
+ table: {
48
+ type: { summary: "boolean" },
49
+ defaultValue: { summary: false }
50
+ }
51
+ },
52
+ headerSize: {
53
+ description: "Height of the header, used to position the menu",
54
+ table: {
55
+ type: { summary: "string" },
56
+ defaultValue: { summary: "72px" }
57
+ }
58
+ },
59
+ themeValues: {
60
+ description: "Theme values for styling the menu",
61
+ table: {
62
+ type: { summary: "object" },
63
+ defaultValue: { summary: "fallbackValues" }
64
+ }
65
+ }
66
+ }
67
+ };
68
+
69
+ export const ProfileMenu = {
70
+ args: {
71
+ id: "profile-menu",
72
+ menuContent: (
73
+ <Box>
74
+ <p>Profile Item 1</p>
75
+ <p>Profile Item 2</p>
76
+ <p>Profile Item 3</p>
77
+ </Box>
78
+ ),
79
+ visible: false,
80
+ headerSize: "72px",
81
+ themeValues: fallbackValues
82
+ },
83
+ render: args => {
84
+ const [visible, setVisible] = useState(false);
85
+
86
+ const toggleMenu = () => {
87
+ setVisible(prev => !prev);
88
+ };
89
+
90
+ return (
91
+ <Box>
92
+ <ButtonWithAction onClick={toggleMenu}>
93
+ {visible ? "Hide Menu" : "Show Menu"}
94
+ </ButtonWithAction>
95
+ <NavMenuMobile {...args} visible={visible} />
96
+ </Box>
97
+ );
98
+ }
99
+ };
100
+
101
+ export const DefaultMenu = {
102
+ args: {
103
+ visible: true
104
+ }
105
+ };
106
+
107
+ export const InteractiveMenu = {
108
+ render: args => {
109
+ const [visible, setVisible] = useState(false);
110
+
111
+ const toggleMenu = () => {
112
+ setVisible(prev => !prev);
113
+ };
114
+
115
+ return (
116
+ <Box>
117
+ <ButtonWithAction onClick={toggleMenu}>
118
+ {visible ? "Hide Menu" : "Show Menu"}
119
+ </ButtonWithAction>
120
+ <NavMenuMobile {...args} visible={visible} />
121
+ </Box>
122
+ );
123
+ }
124
+ };