@codecademy/brand 3.30.0-alpha.2ad52d9890.0 → 3.30.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.
Files changed (20) hide show
  1. package/dist/AppHeader/AppHeaderElements/AppHeaderCatalogDropdown/MarketingBanner.d.ts +1 -2
  2. package/dist/AppHeader/AppHeaderElements/AppHeaderCatalogDropdown/MarketingBanner.js +3 -3
  3. package/dist/AppHeader/AppHeaderElements/AppHeaderCatalogDropdown/NavPanels.d.ts +0 -1
  4. package/dist/AppHeader/AppHeaderElements/AppHeaderCatalogDropdown/NavPanels.js +7 -8
  5. package/dist/AppHeader/AppHeaderElements/AppHeaderCatalogDropdown/consts.d.ts +1 -0
  6. package/dist/AppHeader/AppHeaderElements/AppHeaderCatalogDropdown/consts.js +2 -1
  7. package/dist/AppHeader/AppHeaderElements/AppHeaderSection/MobileBackButton.d.ts +0 -25
  8. package/dist/AppHeader/AppHeaderElements/AppHeaderSection/MobileBackButton.js +2 -13
  9. package/dist/AppHeader/AppHeaderElements/AppHeaderSection/NavTabs.d.ts +20 -0
  10. package/dist/AppHeader/AppHeaderElements/AppHeaderSection/NavTabs.js +144 -0
  11. package/dist/AppHeader/AppHeaderElements/AppHeaderSection/elements.d.ts +63 -1
  12. package/dist/AppHeader/AppHeaderElements/AppHeaderSection/elements.js +81 -16
  13. package/dist/AppHeader/AppHeaderElements/AppHeaderSection/index.d.ts +16 -10
  14. package/dist/AppHeader/AppHeaderElements/AppHeaderSection/index.js +89 -28
  15. package/dist/lib/catalogList/index.d.ts +1 -1
  16. package/dist/lib/catalogList/index.js +10 -1
  17. package/package.json +1 -1
  18. package/dist/AppHeader/AppHeaderElements/AppHeaderSection/AppHeaderSection.test.js +0 -193
  19. package/dist/AppHeader/AppHeaderElements/AppHeaderSection/NavSection.d.ts +0 -21
  20. package/dist/AppHeader/AppHeaderElements/AppHeaderSection/NavSection.js +0 -206
@@ -1,36 +1,77 @@
1
1
  import { Box } from '@codecademy/gamut';
2
- import { useEffect, useState } from 'react';
3
- import * as React from 'react';
4
- import { useGlobalHeaderDynamicDataContext } from '../../../GlobalHeader/context';
2
+ import { motion } from 'framer-motion';
3
+ import { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
4
+ import { useGlobalHeaderDynamicDataContext, useGlobalHeaderItemClick } from '../../../GlobalHeader/context';
5
5
  import { AppHeaderSectionContext } from './AppHeaderSectionContext';
6
- import { StyledGridBox, StyledSection } from './elements';
6
+ import { NavTabContainer, StyledSection } from './elements';
7
7
  import { MobileBackButton } from './MobileBackButton';
8
8
  import { MobileNavMenu } from './MobileNavMenu';
9
- import NavSection from './NavSection';
9
+ import { NavTab, NavTabList, NavTabPanel } from './NavTabs';
10
10
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
- export const AppHeaderSection = /*#__PURE__*/React.forwardRef(({
11
+ export const AppHeaderSection = /*#__PURE__*/forwardRef(({
12
12
  isOpen = true,
13
- keyDownEvents,
14
- navSections,
15
13
  MarketingBanner,
16
14
  isMobile = false,
17
15
  handleClose,
18
- type
16
+ type,
17
+ defaultSelectedKey,
18
+ navSections
19
19
  }, ref) => {
20
- const [activeTab, setActiveTab] = useState(0);
20
+ const firstTabId = navSections?.[0]?.item?.id;
21
+ const [activeTab, setActiveTab] = useState(defaultSelectedKey || firstTabId);
21
22
  const [activePanel, setActivePanel] = useState(null);
23
+ const tabListRef = useRef(null);
22
24
  const {
23
25
  globalHeaderDynamicData
24
26
  } = useGlobalHeaderDynamicDataContext();
27
+ const {
28
+ globalHeaderItemClick
29
+ } = useGlobalHeaderItemClick();
25
30
  const banner = globalHeaderDynamicData?.catalogDropdown?.banner;
26
31
  const showMarketingBanner = banner?.text && banner?.href && MarketingBanner;
27
32
  const tabIndex = isOpen ? 0 : -1;
28
33
  const activePanelSelected = activePanel !== null;
34
+ const onSelect = useCallback((key, event, item) => {
35
+ setActiveTab(key);
36
+ if (item && event) {
37
+ globalHeaderItemClick(event, item);
38
+ }
39
+ }, [setActiveTab, globalHeaderItemClick]);
40
+ const onKeyDown = useCallback(event => {
41
+ if (!tabListRef.current) return;
42
+
43
+ // get list of tabs available
44
+ const tabs = Array.from(tabListRef.current?.querySelectorAll('button[role="tab"]') || []);
45
+ // get the index of the active tab
46
+ const activeElement = tabs.indexOf(document.activeElement);
47
+ if (['ArrowUp', 'ArrowLeft'].includes(event.key)) {
48
+ // prevent scrollbar from moving when arrow keys are pressed on tabs
49
+ event.preventDefault();
50
+ const prevTab = tabs[activeElement - 1];
51
+ if (prevTab) {
52
+ prevTab.focus();
53
+ } else {
54
+ // focus the last tab
55
+ tabs[tabs.length - 1].focus();
56
+ }
57
+ }
58
+ if (['ArrowDown', 'ArrowRight'].includes(event.key)) {
59
+ // prevent scrollbar from moving when arrow keys are pressed on tabs
60
+ event.preventDefault();
61
+ const nextTab = tabs[activeElement + 1];
62
+ if (nextTab) {
63
+ nextTab.focus();
64
+ } else {
65
+ // focus the first tab
66
+ tabs[0].focus();
67
+ }
68
+ }
69
+ }, []);
29
70
  useEffect(() => {
30
- if (isOpen) {
31
- setActiveTab(0);
71
+ if (!isOpen) {
72
+ setActiveTab(firstTabId);
32
73
  }
33
- }, [isOpen]);
74
+ }, [isOpen, firstTabId]);
34
75
  return /*#__PURE__*/_jsx(AppHeaderSectionContext.Provider, {
35
76
  value: {
36
77
  activePanel,
@@ -38,6 +79,7 @@ export const AppHeaderSection = /*#__PURE__*/React.forwardRef(({
38
79
  tabIndex
39
80
  },
40
81
  children: /*#__PURE__*/_jsxs(StyledSection, {
82
+ ref: ref,
41
83
  activePanelSelected: activePanelSelected,
42
84
  children: [showMarketingBanner && /*#__PURE__*/_jsx(Box, {
43
85
  display: {
@@ -45,31 +87,50 @@ export const AppHeaderSection = /*#__PURE__*/React.forwardRef(({
45
87
  xs: 'block'
46
88
  },
47
89
  children: /*#__PURE__*/_jsx(MarketingBanner, {
48
- tabIndex: tabIndex,
49
90
  text: banner?.text,
50
- href: banner?.href
91
+ href: banner?.href,
92
+ tabIndex: tabIndex
51
93
  })
52
- }), /*#__PURE__*/_jsxs(StyledGridBox, {
53
- ref: ref,
54
- onKeyDown: keyDownEvents,
94
+ }), /*#__PURE__*/_jsxs(NavTabContainer, {
95
+ p: 8,
96
+ rowGap: 8,
55
97
  children: [isMobile && /*#__PURE__*/_jsx(MobileBackButton, {
56
98
  handleClose: handleClose,
57
99
  type: type
100
+ }), /*#__PURE__*/_jsx(NavTabList, {
101
+ type: type,
102
+ ref: tabListRef,
103
+ children: navSections.map(({
104
+ item
105
+ }, index) => /*#__PURE__*/_jsx(NavTab, {
106
+ item: item,
107
+ onSelect: onSelect,
108
+ isActive: activeTab === item.id,
109
+ onKeyDown: onKeyDown,
110
+ index: index
111
+ }, item.id))
58
112
  }), navSections.map((section, index) => {
59
113
  const {
60
- item,
114
+ item: {
115
+ id
116
+ },
61
117
  panel: Panel
62
118
  } = section;
63
- return /*#__PURE__*/_jsx(NavSection, {
64
- isActiveTab: activeTab === index,
65
- setActiveTab: setActiveTab,
66
- icon: item.icon,
67
- text: item.text,
119
+ return /*#__PURE__*/_jsx(NavTabPanel, {
68
120
  index: index,
69
- item: item,
70
- tabIndex: tabIndex,
71
- children: /*#__PURE__*/_jsx(Panel, {})
72
- }, item.id);
121
+ isActiveTab: activeTab === id,
122
+ id: id,
123
+ children: /*#__PURE__*/_jsx(motion.div, {
124
+ animate: {
125
+ opacity: activeTab === id ? 1 : 0
126
+ },
127
+ transition: {
128
+ duration: 0.4,
129
+ ease: 'easeInOut'
130
+ },
131
+ children: /*#__PURE__*/_jsx(Panel, {})
132
+ })
133
+ }, id);
73
134
  })]
74
135
  }), /*#__PURE__*/_jsx(MobileNavMenu, {
75
136
  handleClose: handleClose,
@@ -1,6 +1,6 @@
1
1
  import { AppHeaderLinkItem, AppHeaderResourcesDataItem } from '../../AppHeader/shared';
2
2
  export declare const careerPaths: AppHeaderLinkItem[];
3
3
  export declare const liveLearningHubItems: AppHeaderResourcesDataItem;
4
- export declare const liveLearningNavPanelItem: AppHeaderResourcesDataItem;
4
+ export declare const liveLearningNavPanelItems: AppHeaderResourcesDataItem;
5
5
  export declare const topLanguages: AppHeaderLinkItem[];
6
6
  export declare const getTopSubjects: () => AppHeaderLinkItem[];
@@ -61,7 +61,7 @@ export const liveLearningHubItems = {
61
61
  };
62
62
 
63
63
  /* Items for the variant global header. Tracking targets are kept the same for experiment */
64
- export const liveLearningNavPanelItem = {
64
+ export const liveLearningNavPanelItems = {
65
65
  title: 'Live Learning',
66
66
  description: 'Build skills faster through live, instructor-led sessions. Get real-time feedback, stay motivated, and deepen your understanding with expert guidance.',
67
67
  data: [{
@@ -73,6 +73,15 @@ export const liveLearningNavPanelItem = {
73
73
  type: 'link',
74
74
  newTab: true,
75
75
  badge: renderNewBadge()
76
+ }, {
77
+ id: 'gk',
78
+ href: '/live-learning#GKLiveLearning',
79
+ text: 'Codecademy x Global Knowledge',
80
+ description: 'Global Knowledge is part of the Skillsoft family (like us). These 1–2 day live courses fast-track your technical and professional growth through interactive lessons led by subject matter experts.',
81
+ trackingTarget: 'topnav_catalog_live_learning_gk_codecademy',
82
+ type: 'link',
83
+ newTab: true,
84
+ badge: renderNewBadge()
76
85
  }]
77
86
  };
78
87
  export const topLanguages = [{
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@codecademy/brand",
3
3
  "description": "Brand component library for Codecademy",
4
- "version": "3.30.0-alpha.2ad52d9890.0",
4
+ "version": "3.30.0",
5
5
  "author": "Codecademy Engineering <dev@codecademy.com>",
6
6
  "dependencies": {
7
7
  "@emotion/is-prop-valid": "^1.2.1",
@@ -1,193 +0,0 @@
1
- import { setupRtl } from '@codecademy/gamut-tests';
2
- import userEvent from '@testing-library/user-event';
3
- import React from 'react';
4
- import { GlobalHeaderDynamicDataContext, GlobalHeaderItemClickContext } from '../../../GlobalHeader/context';
5
- import { CATALOG_NAV_SECTIONS } from '../AppHeaderCatalogDropdown/consts';
6
- import { MarketingBanner } from '../AppHeaderCatalogDropdown/MarketingBanner';
7
- import { AppHeaderSection } from '.';
8
- import { jsx as _jsx } from "react/jsx-runtime";
9
- const mockOnClick = jest.fn();
10
- const mockKeyDownEvents = jest.fn();
11
- const mockItem = {
12
- text: 'catalog',
13
- id: 'the-catalog',
14
- type: 'catalog-dropdown',
15
- trackingTarget: 'catalog-dropdown'
16
- };
17
- const defaultProps = {
18
- item: mockItem,
19
- isOpen: true,
20
- keyDownEvents: mockKeyDownEvents
21
- };
22
- const mockContextValue = {
23
- globalHeaderItemClick: mockOnClick
24
- };
25
- const mockDataContextValue = {
26
- globalHeaderDynamicData: {
27
- catalogDropdown: {
28
- banner: {
29
- href: 'https://example.com',
30
- text: 'Test banner text'
31
- },
32
- skillPaths: {
33
- totalSkillPathCount: 0,
34
- promotedSkillPaths: []
35
- },
36
- careerPaths: {
37
- totalCareerPathCount: 0,
38
- promotedCareerPaths: []
39
- },
40
- certificationPaths: {
41
- totalCertificationPathCount: 0
42
- }
43
- }
44
- }
45
- };
46
- describe('AppHeaderSection', () => {
47
- const createWrapper = (dataContextValue = mockDataContextValue) => ({
48
- children
49
- }) => /*#__PURE__*/_jsx(GlobalHeaderDynamicDataContext.Provider, {
50
- value: dataContextValue,
51
- children: /*#__PURE__*/_jsx(GlobalHeaderItemClickContext.Provider, {
52
- value: mockContextValue,
53
- children: children
54
- })
55
- });
56
- const renderView = setupRtl(AppHeaderSection, defaultProps).options({
57
- wrapper: createWrapper()
58
- });
59
- it('renders the component ', () => {
60
- const {
61
- view
62
- } = renderView({
63
- navSections: CATALOG_NAV_SECTIONS,
64
- handleClose: jest.fn(),
65
- type: 'catalog-dropdown'
66
- });
67
- expect(view.getByRole('heading', {
68
- name: 'Course topics'
69
- })).toBeInTheDocument();
70
- });
71
- describe('marketing banner', () => {
72
- const createRenderView = bannerData => setupRtl(AppHeaderSection, defaultProps).options({
73
- wrapper: createWrapper({
74
- globalHeaderDynamicData: {
75
- catalogDropdown: {
76
- banner: bannerData,
77
- skillPaths: {
78
- totalSkillPathCount: 0,
79
- promotedSkillPaths: []
80
- },
81
- careerPaths: {
82
- totalCareerPathCount: 0,
83
- promotedCareerPaths: []
84
- },
85
- certificationPaths: {
86
- totalCertificationPathCount: 0
87
- }
88
- }
89
- }
90
- })
91
- });
92
- it('renders the marketing banner when the banner text and href are present', () => {
93
- const {
94
- view
95
- } = renderView({
96
- navSections: CATALOG_NAV_SECTIONS,
97
- handleClose: jest.fn(),
98
- type: 'catalog-dropdown',
99
- MarketingBanner
100
- });
101
- expect(view.getByText('Test banner text')).toBeInTheDocument();
102
- });
103
- it('does not render the marketing banner when the banner href is empty', () => {
104
- const {
105
- view
106
- } = createRenderView({
107
- href: '',
108
- text: 'Test banner text'
109
- })({
110
- navSections: CATALOG_NAV_SECTIONS,
111
- handleClose: jest.fn(),
112
- type: 'catalog-dropdown',
113
- MarketingBanner
114
- });
115
- expect(view.queryByText('Test banner text')).not.toBeInTheDocument();
116
- expect(view.queryByTestId('marketing-banner')).not.toBeInTheDocument();
117
- });
118
- it('does not render the marketing banner when the banner text is empty', () => {
119
- const {
120
- view
121
- } = createRenderView({
122
- href: 'https://example.com',
123
- text: ''
124
- })({
125
- navSections: CATALOG_NAV_SECTIONS,
126
- handleClose: jest.fn(),
127
- type: 'catalog-dropdown',
128
- MarketingBanner
129
- });
130
- expect(view.queryByTestId('marketing-banner')).not.toBeInTheDocument();
131
- });
132
- it('does not render the marketing banner when the banner text is empty', () => {
133
- const {
134
- view
135
- } = createRenderView({
136
- href: 'https://example.com',
137
- text: ''
138
- })({
139
- navSections: CATALOG_NAV_SECTIONS,
140
- handleClose: jest.fn(),
141
- type: 'catalog-dropdown',
142
- MarketingBanner
143
- });
144
- expect(view.queryByText('Test banner text')).not.toBeInTheDocument();
145
- });
146
- });
147
- describe('tab navigation', () => {
148
- it('starts with the first tab active by default', () => {
149
- const {
150
- view
151
- } = renderView({
152
- navSections: CATALOG_NAV_SECTIONS,
153
- handleClose: jest.fn(),
154
- type: 'catalog-dropdown'
155
- });
156
- const courseTopicsButton = view.getByTestId('nav-section-course-topics');
157
- expect(courseTopicsButton).toHaveAttribute('aria-expanded', 'true');
158
- });
159
- it('switches active tab when clicking on different sections', async () => {
160
- const {
161
- view
162
- } = renderView({
163
- navSections: CATALOG_NAV_SECTIONS,
164
- handleClose: jest.fn(),
165
- type: 'catalog-dropdown'
166
- });
167
- const careerPathsButton = view.getByTestId('nav-section-career-paths');
168
- await userEvent.click(careerPathsButton);
169
- expect(careerPathsButton).toHaveAttribute('aria-expanded', 'true');
170
- const courseTopicsButton = view.getByTestId('nav-section-course-topics');
171
- expect(courseTopicsButton).toHaveAttribute('aria-expanded', 'false');
172
- });
173
- it('calls tracking with correct parameters when clicking nav section', async () => {
174
- const {
175
- view
176
- } = renderView({
177
- navSections: CATALOG_NAV_SECTIONS,
178
- handleClose: jest.fn(),
179
- type: 'catalog-dropdown'
180
- });
181
- const careerPathsButton = view.getByTestId('nav-section-career-paths');
182
- await userEvent.click(careerPathsButton);
183
- expect(mockOnClick).toHaveBeenCalledTimes(1);
184
- expect(mockOnClick).toHaveBeenCalledWith(expect.anything(),
185
- // event object
186
- expect.objectContaining({
187
- id: 'career-paths',
188
- type: 'catalog-dropdown'
189
- }) // tracking parameters
190
- );
191
- });
192
- });
193
- });
@@ -1,21 +0,0 @@
1
- import { GamutIconProps } from '@codecademy/gamut-icons';
2
- import React, { PropsWithChildren } from 'react';
3
- import { AppHeaderCatalogDropdownItem, AppHeaderResourcesDropdownItem } from '../../shared';
4
- type NavSectionProps = PropsWithChildren & {
5
- item: AppHeaderCatalogDropdownItem | AppHeaderResourcesDropdownItem;
6
- isActiveTab: boolean;
7
- setActiveTab: (tab: number) => void;
8
- icon?: React.ComponentType<GamutIconProps>;
9
- text: string;
10
- index: number;
11
- tabIndex?: number;
12
- };
13
- export declare const NavigationButton: import("@emotion/styled").StyledComponent<{
14
- theme?: import("@emotion/react").Theme;
15
- as?: React.ElementType;
16
- } & {
17
- isActive: boolean;
18
- index: number;
19
- }, React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, {}>;
20
- declare const NavSection: ({ isActiveTab, setActiveTab, icon: Icon, text, index, children, item, tabIndex, }: NavSectionProps) => import("react/jsx-runtime").JSX.Element;
21
- export default NavSection;