@hubspot/cms-component-library 0.1.0 → 0.2.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/components/componentLibrary/Accordion/AccordionContent/ContentFields.tsx +5 -3
- package/components/componentLibrary/Accordion/AccordionItem/StyleFields.tsx +5 -3
- package/components/componentLibrary/Accordion/AccordionItem/index.module.scss +2 -2
- package/components/componentLibrary/Accordion/AccordionItem/index.tsx +3 -3
- package/components/componentLibrary/Accordion/AccordionTitle/ContentFields.tsx +5 -3
- package/components/componentLibrary/Accordion/AccordionTitle/index.module.scss +2 -2
- package/components/componentLibrary/Accordion/stories/Accordion.stories.tsx +80 -1
- package/components/componentLibrary/Accordion/stories/AccordionDecorator.tsx +14 -14
- package/components/componentLibrary/Button/ContentFields.tsx +5 -3
- package/components/componentLibrary/Button/StyleFields.tsx +5 -3
- package/components/componentLibrary/Button/index.module.scss +22 -14
- package/components/componentLibrary/Button/index.tsx +6 -6
- package/components/componentLibrary/Button/stories/Button.AsButton.stories.tsx +30 -1
- package/components/componentLibrary/Button/stories/Button.AsLink.stories.tsx +38 -1
- package/components/componentLibrary/Button/stories/ButtonDecorator.tsx +1 -1
- package/components/componentLibrary/Card/StyleFields.tsx +5 -3
- package/components/componentLibrary/Card/stories/Card.stories.tsx +46 -1
- package/components/componentLibrary/Card/stories/CardDecorator.tsx +1 -1
- package/components/componentLibrary/Divider/ContentFields.tsx +5 -3
- package/components/componentLibrary/Divider/StyleFields.tsx +5 -3
- package/components/componentLibrary/Divider/index.module.scss +6 -6
- package/components/componentLibrary/Divider/index.tsx +7 -3
- package/components/componentLibrary/Divider/stories/Divider.stories.tsx +44 -50
- package/components/componentLibrary/Divider/stories/{DividerDecorator.module.css → DividerDecorator.module.scss} +5 -4
- package/components/componentLibrary/Divider/stories/DividerDecorator.tsx +1 -1
- package/components/componentLibrary/Divider/types.ts +3 -1
- package/components/componentLibrary/Drawer/hooks/index.tsx +13 -0
- package/components/componentLibrary/Drawer/index.module.scss +94 -0
- package/components/componentLibrary/Drawer/index.tsx +131 -0
- package/components/componentLibrary/Drawer/llm.txt +416 -0
- package/components/componentLibrary/Drawer/stories/Drawer.stories.tsx +512 -0
- package/components/componentLibrary/Drawer/stories/DrawerDecorator.module.scss +8 -0
- package/components/componentLibrary/Drawer/stories/DrawerDecorator.tsx +18 -0
- package/components/componentLibrary/Drawer/types.ts +25 -0
- package/components/componentLibrary/Flex/stories/FlexDecorator.tsx +1 -1
- package/components/componentLibrary/Flex/types.ts +3 -1
- package/components/componentLibrary/Grid/stories/Grid.stories.tsx +454 -152
- package/components/componentLibrary/Grid/stories/GridDecorator.tsx +2 -2
- package/components/componentLibrary/Heading/ContentFields.tsx +5 -3
- package/components/componentLibrary/Heading/StyleFields.tsx +11 -9
- package/components/componentLibrary/Heading/index.tsx +3 -3
- package/components/componentLibrary/Heading/llm.txt +8 -8
- package/components/componentLibrary/Heading/stories/Heading.stories.tsx +3 -3
- package/components/componentLibrary/Heading/stories/HeadingDecorator.tsx +1 -1
- package/components/componentLibrary/Heading/types.ts +4 -4
- package/components/componentLibrary/Icon/ContentFields.tsx +5 -3
- package/components/componentLibrary/Icon/stories/Icon.stories.tsx +1 -1
- package/components/componentLibrary/Icon/stories/IconDecorator.tsx +1 -1
- package/components/componentLibrary/Image/ContentFields.tsx +5 -3
- package/components/componentLibrary/Image/index.tsx +4 -4
- package/components/componentLibrary/Image/llm.txt +17 -17
- package/components/componentLibrary/Image/stories/Image.stories.tsx +61 -18
- package/components/componentLibrary/Image/stories/ImageDecorator.tsx +1 -1
- package/components/componentLibrary/Image/types.ts +2 -2
- package/components/componentLibrary/LanguageSwitcher/ContentFields.tsx +18 -0
- package/components/componentLibrary/LanguageSwitcher/LanguageOptions.module.scss +37 -0
- package/components/componentLibrary/LanguageSwitcher/LanguageOptions.tsx +65 -0
- package/components/componentLibrary/LanguageSwitcher/StyleFields.tsx +48 -0
- package/components/componentLibrary/LanguageSwitcher/_dummyData.tsx +247 -0
- package/components/componentLibrary/LanguageSwitcher/assets/Globe.tsx +16 -0
- package/components/componentLibrary/LanguageSwitcher/index.module.scss +58 -0
- package/components/componentLibrary/LanguageSwitcher/index.tsx +125 -0
- package/components/componentLibrary/LanguageSwitcher/llm.txt +380 -0
- package/components/componentLibrary/LanguageSwitcher/stories/LanguageSwitcher.stories.tsx +349 -0
- package/components/componentLibrary/LanguageSwitcher/stories/LanguageSwitcherDecorator.module.scss +5 -0
- package/components/componentLibrary/LanguageSwitcher/stories/LanguageSwitcherDecorator.tsx +8 -0
- package/components/componentLibrary/LanguageSwitcher/types.ts +48 -0
- package/components/componentLibrary/LanguageSwitcher/utils.tsx +38 -0
- package/components/componentLibrary/Link/ContentFields.tsx +5 -3
- package/components/componentLibrary/Link/StyleFields.tsx +5 -3
- package/components/componentLibrary/Link/index.module.scss +10 -0
- package/components/componentLibrary/Link/index.tsx +24 -14
- package/components/componentLibrary/Link/stories/Link.stories.tsx +35 -5
- package/components/componentLibrary/Link/stories/LinkDecorator.tsx +11 -1
- package/components/componentLibrary/Link/types.ts +22 -13
- package/components/componentLibrary/List/ContentFields.tsx +5 -3
- package/components/componentLibrary/List/ListItem/ContentFields.tsx +6 -17
- package/components/componentLibrary/List/ListItem/index.module.scss +1 -13
- package/components/componentLibrary/List/ListItem/index.tsx +3 -30
- package/components/componentLibrary/List/ListItem/types.ts +1 -16
- package/components/componentLibrary/List/StyleFields.tsx +15 -18
- package/components/componentLibrary/List/index.module.scss +3 -0
- package/components/componentLibrary/List/index.tsx +5 -2
- package/components/componentLibrary/List/llm.txt +73 -103
- package/components/componentLibrary/List/stories/List.stories.tsx +56 -80
- package/components/componentLibrary/List/stories/ListDecorator.tsx +3 -6
- package/components/componentLibrary/List/types.ts +1 -3
- package/components/componentLibrary/Logo/_dummyLogoData.ts +12 -0
- package/components/componentLibrary/Logo/assets/hubspot-logo.png +0 -0
- package/components/componentLibrary/Logo/index.module.scss +22 -0
- package/components/componentLibrary/Logo/index.tsx +73 -0
- package/components/componentLibrary/Logo/llm.txt +262 -0
- package/components/componentLibrary/Logo/stories/Logo.stories.tsx +88 -0
- package/components/componentLibrary/Logo/stories/LogoDecorator.module.scss +10 -0
- package/components/componentLibrary/Logo/stories/LogoDecorator.tsx +8 -0
- package/components/componentLibrary/Logo/types.tsx +16 -0
- package/components/componentLibrary/Menu/ContentFields.tsx +16 -0
- package/components/componentLibrary/Menu/MenuItem/Chevron/index.module.scss +6 -0
- package/components/componentLibrary/Menu/MenuItem/Chevron/index.tsx +17 -0
- package/components/componentLibrary/Menu/MenuItem/index.module.scss +7 -0
- package/components/componentLibrary/Menu/MenuItem/index.tsx +266 -0
- package/components/componentLibrary/Menu/MenuItem/types.ts +17 -0
- package/components/componentLibrary/Menu/NavigationMenu/ContentFields.tsx +20 -0
- package/components/componentLibrary/Menu/NavigationMenu/index.tsx +18 -0
- package/components/componentLibrary/Menu/NavigationMenu/islands/NavigationMenuIsland.tsx +95 -0
- package/components/componentLibrary/Menu/NavigationMenu/islands/index.module.scss +100 -0
- package/components/componentLibrary/Menu/NavigationMenu/islands/types.ts +19 -0
- package/components/componentLibrary/Menu/NavigationMenu/llm.txt +197 -0
- package/components/componentLibrary/Menu/NavigationMenu/stories/NavigationMenu.stories.tsx +286 -0
- package/components/componentLibrary/Menu/NavigationMenu/stories/NavigationMenuDecorator.module.scss +15 -0
- package/components/componentLibrary/Menu/NavigationMenu/stories/NavigationMenuDecorator.tsx +12 -0
- package/components/componentLibrary/Menu/NavigationMenu/types.ts +3 -0
- package/components/componentLibrary/Menu/VerticalMenu/ContentFields.tsx +20 -0
- package/components/componentLibrary/Menu/VerticalMenu/index.tsx +18 -0
- package/components/componentLibrary/Menu/VerticalMenu/islands/index.module.scss +53 -0
- package/components/componentLibrary/Menu/VerticalMenu/islands/verticalMenuIsland.tsx +78 -0
- package/components/componentLibrary/Menu/VerticalMenu/llm.txt +177 -0
- package/components/componentLibrary/Menu/VerticalMenu/stories/VerticalMenu.stories.tsx +242 -0
- package/components/componentLibrary/Menu/VerticalMenu/stories/VerticalMenuDecorator.module.scss +19 -0
- package/components/componentLibrary/Menu/VerticalMenu/stories/VerticalMenuDecorator.tsx +12 -0
- package/components/componentLibrary/Menu/VerticalMenu/types.ts +21 -0
- package/components/componentLibrary/Menu/_dummyMenuData.js +1346 -0
- package/components/componentLibrary/Menu/types.ts +56 -0
- package/components/componentLibrary/Menu/utils/transformMenuData.ts +11 -0
- package/components/componentLibrary/_patterns/README.md +15 -17
- package/components/componentLibrary/_patterns/checklist-and-examples.md +17 -17
- package/components/componentLibrary/_patterns/component-structure.md +21 -23
- package/components/componentLibrary/_patterns/css-patterns.md +170 -18
- package/components/componentLibrary/_patterns/field-patterns.md +97 -27
- package/components/componentLibrary/_patterns/function-declaration-patterns.md +281 -0
- package/components/componentLibrary/_patterns/llm-txt.template.md +4 -2
- package/components/componentLibrary/_patterns/prop-naming-patterns.md +208 -0
- package/components/componentLibrary/_patterns/storybook-patterns.md +25 -8
- package/components/componentLibrary/_patterns/typescript-patterns.md +6 -3
- package/package.json +4 -2
- /package/components/componentLibrary/Button/stories/{ButtonDecorator.module.css → ButtonDecorator.module.scss} +0 -0
- /package/components/componentLibrary/Card/stories/{CardDecorator.module.css → CardDecorator.module.scss} +0 -0
- /package/components/componentLibrary/Flex/stories/{FlexDecorator.module.css → FlexDecorator.module.scss} +0 -0
- /package/components/componentLibrary/Grid/stories/{GridDecorator.module.css → GridDecorator.module.scss} +0 -0
- /package/components/componentLibrary/Heading/stories/{HeadingDecorator.module.css → HeadingDecorator.module.scss} +0 -0
- /package/components/componentLibrary/Icon/stories/{IconDecorator.module.css → IconDecorator.module.scss} +0 -0
- /package/components/componentLibrary/Image/stories/{ImageDecorator.module.css → ImageDecorator.module.scss} +0 -0
- /package/components/componentLibrary/Image/stories/assets/{catSmile.jpg → cat-smile.jpg} +0 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { FlexJustifyContent } from '../../../Flex/types.js';
|
|
2
|
+
import { CSSVariables } from '../../../utils/types.js';
|
|
3
|
+
import { SiteMenu } from '@hubspot/cms-components';
|
|
4
|
+
|
|
5
|
+
export type NavigationMenuProps = {
|
|
6
|
+
navAriaLabel?: string;
|
|
7
|
+
justifyMenu?: FlexJustifyContent;
|
|
8
|
+
className?: string;
|
|
9
|
+
style?: CSSVariables;
|
|
10
|
+
linkColor?: string;
|
|
11
|
+
linkColorHover?: string;
|
|
12
|
+
backgroundColor?: string;
|
|
13
|
+
backgroundColorHover?: string;
|
|
14
|
+
submenuLinkColor?: string;
|
|
15
|
+
submenuLinkColorHover?: string;
|
|
16
|
+
submenuBackgroundColor?: string;
|
|
17
|
+
submenuBackgroundColorHover?: string;
|
|
18
|
+
menuData: SiteMenu;
|
|
19
|
+
};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# NavigationMenu Component
|
|
2
|
+
|
|
3
|
+
A horizontal navigation menu component that renders a top-level menu bar with dropdown submenus, keyboard navigation support, and flexible styling options.
|
|
4
|
+
|
|
5
|
+
## Import path
|
|
6
|
+
```tsx
|
|
7
|
+
import NavigationMenu from '@hubspot/cms-component-library/NavigationMenu';
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Purpose
|
|
11
|
+
|
|
12
|
+
The NavigationMenu component provides a horizontal navigation menu for HubSpot CMS projects. It automatically integrates with HubSpot's site menu system, supporting multi-level dropdown menus with keyboard navigation, customizable colors for links and backgrounds, and flexible horizontal alignment options.
|
|
13
|
+
|
|
14
|
+
## Component Structure
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
NavigationMenu/
|
|
18
|
+
├── index.tsx # Main component wrapper (Island integration)
|
|
19
|
+
├── ContentFields.tsx # HubSpot field definitions for menu selection
|
|
20
|
+
├── islands/
|
|
21
|
+
│ ├── NavigationMenuIsland.tsx # Island component with rendering logic
|
|
22
|
+
│ ├── types.ts # TypeScript type definitions
|
|
23
|
+
│ └── index.module.scss # CSS module with design tokens
|
|
24
|
+
└── stories/
|
|
25
|
+
├── NavigationMenu.stories.tsx # Storybook examples
|
|
26
|
+
└── NavigationMenuDecorator.tsx # Storybook decorator
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Components
|
|
30
|
+
|
|
31
|
+
### NavigationMenu (Main Component)
|
|
32
|
+
|
|
33
|
+
**Purpose:** Renders a horizontal navigation menu from HubSpot site menu data with dropdown submenus.
|
|
34
|
+
|
|
35
|
+
**Props:**
|
|
36
|
+
```tsx
|
|
37
|
+
{
|
|
38
|
+
menuData: SiteMenu; // Menu data from useMenu hook (required)
|
|
39
|
+
justifyMenu?: FlexJustifyContent; // Horizontal alignment (default: 'flex-start')
|
|
40
|
+
navAriaLabel?: string; // ARIA label for navigation (default: 'Navigation')
|
|
41
|
+
linkColor?: string; // Top-level link text color
|
|
42
|
+
linkColorHover?: string; // Top-level link hover text color
|
|
43
|
+
backgroundColor?: string; // Top-level link background color
|
|
44
|
+
backgroundColorHover?: string; // Top-level link hover background color
|
|
45
|
+
submenuLinkColor?: string; // Submenu link text color
|
|
46
|
+
submenuLinkColorHover?: string; // Submenu link hover text color
|
|
47
|
+
submenuBackgroundColor?: string; // Submenu link background color
|
|
48
|
+
submenuBackgroundColorHover?: string; // Submenu link hover background color
|
|
49
|
+
className?: string; // Additional CSS classes
|
|
50
|
+
style?: React.CSSProperties; // Inline styles with CSS variables
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Consuming the Component with useMenu Hook
|
|
55
|
+
|
|
56
|
+
The NavigationMenu component requires menu data from HubSpot's `useMenu` hook. This hook fetches the site menu structure based on a menu ID defined in your module fields.
|
|
57
|
+
|
|
58
|
+
### Basic Module Implementation
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
import { ModuleMeta } from '../types/modules.js';
|
|
62
|
+
import NavigationMenu from '@hubspot/cms-component-library/NavigationMenu';
|
|
63
|
+
import { useMenu, fieldPath } from '@hubspot/cms-components';
|
|
64
|
+
|
|
65
|
+
export const Component = () => {
|
|
66
|
+
// Fetch menu data using the useMenu hook
|
|
67
|
+
// fieldPath('menu') references the menu field defined in fields.tsx
|
|
68
|
+
const menuData = useMenu(fieldPath('menu'));
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<NavigationMenu
|
|
72
|
+
menuData={menuData}
|
|
73
|
+
justifyMenu="flex-start"
|
|
74
|
+
navAriaLabel="Main navigation"
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export { fields } from './fields.js';
|
|
80
|
+
|
|
81
|
+
export const meta: ModuleMeta = {
|
|
82
|
+
label: 'Main Navigation Menu',
|
|
83
|
+
content_types: [],
|
|
84
|
+
icon: '',
|
|
85
|
+
categories: ['navigation'],
|
|
86
|
+
};
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Module Fields Definition
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
import { ModuleFields } from '@hubspot/cms-components/fields';
|
|
93
|
+
import NavigationMenu from '@hubspot/cms-component-library/NavigationMenu';
|
|
94
|
+
|
|
95
|
+
export const fields = (
|
|
96
|
+
<ModuleFields>
|
|
97
|
+
<NavigationMenu.ContentFields />
|
|
98
|
+
</ModuleFields>
|
|
99
|
+
);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**What this does:**
|
|
103
|
+
1. `NavigationMenu.ContentFields` creates a HubSpot MenuField in your module
|
|
104
|
+
2. This field allows content editors to select which site menu to display
|
|
105
|
+
3. The `useMenu(fieldPath('menu'))` hook fetches the selected menu's data
|
|
106
|
+
4. The menu data is passed to the NavigationMenu component via the `menuData` prop
|
|
107
|
+
|
|
108
|
+
## Usage Examples
|
|
109
|
+
|
|
110
|
+
### Basic Navigation Menu
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
import NavigationMenu from '@hubspot/cms-component-library/NavigationMenu';
|
|
114
|
+
import { useMenu, fieldPath } from '@hubspot/cms-components';
|
|
115
|
+
|
|
116
|
+
export const Component = () => {
|
|
117
|
+
const menuData = useMenu(fieldPath('menu'));
|
|
118
|
+
|
|
119
|
+
return <NavigationMenu menuData={menuData} />;
|
|
120
|
+
};
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
## HubSpot CMS Integration
|
|
125
|
+
|
|
126
|
+
### Field Definitions
|
|
127
|
+
|
|
128
|
+
The NavigationMenu component provides field definitions for menu selection in HubSpot CMS modules.
|
|
129
|
+
|
|
130
|
+
#### ContentFields
|
|
131
|
+
|
|
132
|
+
Configurable props for customizing field labels, names, and defaults:
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
<NavigationMenu.ContentFields
|
|
136
|
+
menuLabel="Menu"
|
|
137
|
+
menuName="menu"
|
|
138
|
+
menuDefault="default"
|
|
139
|
+
/>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Fields:**
|
|
143
|
+
- `menu`: MenuField for selecting which site menu to display (default: 'default')
|
|
144
|
+
|
|
145
|
+
## Styling
|
|
146
|
+
|
|
147
|
+
### CSS Variables
|
|
148
|
+
|
|
149
|
+
The NavigationMenu component uses CSS variables for theming and customization:
|
|
150
|
+
|
|
151
|
+
**Base Menu Styles:**
|
|
152
|
+
- `--hscl-navigationMenu-gap`: Space between top-level menu items
|
|
153
|
+
- `--hscl-navigationMenu-color`: Link text color (default: currentColor)
|
|
154
|
+
- `--hscl-navigationMenu-backgroundColor`: Link background color (default: transparent)
|
|
155
|
+
- `--hscl-navigationMenu-color-hover`: Link hover text color
|
|
156
|
+
- `--hscl-navigationMenu-backgroundColor-hover`: Link hover background color
|
|
157
|
+
|
|
158
|
+
**Submenu-Specific Styles:**
|
|
159
|
+
- `--hscl-navigationMenu-submenu-color`: Submenu link text color
|
|
160
|
+
- `--hscl-navigationMenu-submenu-backgroundColor`: Submenu link background color
|
|
161
|
+
- `--hscl-navigationMenu-submenu-color-hover`: Submenu link hover text color
|
|
162
|
+
- `--hscl-navigationMenu-submenu-backgroundColor-hover`: Submenu link hover background color
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
## Accessibility
|
|
166
|
+
|
|
167
|
+
The NavigationMenu component follows accessibility best practices:
|
|
168
|
+
|
|
169
|
+
- **Semantic HTML**: Renders as a `<nav>` element with an unordered list (`<ul>`)
|
|
170
|
+
- **ARIA Labels**: Customizable `aria-label` via the `navAriaLabel` prop
|
|
171
|
+
- **ARIA Roles**: List items have `role="menuitem"` for proper screen reader support
|
|
172
|
+
- **Keyboard Navigation**: Full keyboard support with arrow keys
|
|
173
|
+
- `ArrowDown`: Opens submenu or moves to next item
|
|
174
|
+
- `ArrowUp`: Closes submenu or moves to previous item
|
|
175
|
+
- `ArrowRight`: Moves to next top-level item or opens nested submenu
|
|
176
|
+
- `ArrowLeft`: Moves to previous top-level item or returns to parent
|
|
177
|
+
- `Enter`/`Space`: Activates link
|
|
178
|
+
- `Escape`: Closes submenu
|
|
179
|
+
- `Home`/`End`: Jump to first/last item
|
|
180
|
+
- **Focus Management**: Keyboard focus properly managed across menu levels
|
|
181
|
+
- **External Links**: Automatically adds `target="_blank"` and `rel="noopener noreferrer"` for external links
|
|
182
|
+
|
|
183
|
+
## Best Practices
|
|
184
|
+
|
|
185
|
+
- **Always use useMenu hook**: The NavigationMenu component requires menu data from the `useMenu` hook—never pass hardcoded menu structures
|
|
186
|
+
- **Consistent alignment**: Use the same `justifyMenu` value across navigation instances for visual consistency
|
|
187
|
+
- **ARIA labels**: Provide descriptive `navAriaLabel` values (e.g., "Main navigation", "Footer navigation")
|
|
188
|
+
- **Color contrast**: Ensure sufficient contrast between link colors and backgrounds (WCAG AA minimum)
|
|
189
|
+
- **Hover states**: Always define hover colors for better visual feedback
|
|
190
|
+
- **Submenu styling**: Use distinct colors for dropdown menus to differentiate them from top-level items
|
|
191
|
+
- **Override CSS variables**: Customize appearance using CSS variables rather than overriding class styles
|
|
192
|
+
- **Null checking**: Always check if menuData exists before rendering, as useMenu can return null
|
|
193
|
+
|
|
194
|
+
## Related Components
|
|
195
|
+
|
|
196
|
+
- **VerticalMenu**: Vertical navigation menu component for sidebar navigation
|
|
197
|
+
- **MenuItem**: Used internally to render individual menu items with support for nested children and keyboard navigation
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import NavigationMenu from '../index.js';
|
|
3
|
+
import { NavigationMenuProps } from '../islands/types.js';
|
|
4
|
+
import { withNavigationMenuStyles } from './NavigationMenuDecorator.js';
|
|
5
|
+
import { SBContainer, SBFocusWrapper } from '@sb-utils';
|
|
6
|
+
import { dummyMenuData } from '../../_dummyMenuData.js';
|
|
7
|
+
|
|
8
|
+
const meta: Meta<NavigationMenuProps> = {
|
|
9
|
+
title: 'Component Library/Menu/Navigation Menu',
|
|
10
|
+
component: NavigationMenu,
|
|
11
|
+
parameters: {
|
|
12
|
+
layout: 'fullwidth',
|
|
13
|
+
docs: {
|
|
14
|
+
description: {
|
|
15
|
+
component:
|
|
16
|
+
'The NavigationMenu component creates a horizontal navigation menu with dropdown submenus. It supports keyboard navigation and is typically used in website headers.',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
decorators: [withNavigationMenuStyles],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default meta;
|
|
24
|
+
type Story = StoryObj<typeof meta>;
|
|
25
|
+
|
|
26
|
+
export const Default: Story = {
|
|
27
|
+
render: () => (
|
|
28
|
+
<SBContainer
|
|
29
|
+
addBackground
|
|
30
|
+
style={{ overflow: 'visible', minHeight: '300px' }}
|
|
31
|
+
>
|
|
32
|
+
<NavigationMenu navAriaLabel="Main Navigation" menuData={dummyMenuData} />
|
|
33
|
+
</SBContainer>
|
|
34
|
+
),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const JustifyContent: Story = {
|
|
38
|
+
name: 'Justify Content Options',
|
|
39
|
+
render: () => (
|
|
40
|
+
<SBContainer flex direction="column" gap="extralarge">
|
|
41
|
+
<SBContainer
|
|
42
|
+
addBackground
|
|
43
|
+
style={{ overflow: 'visible', minHeight: '250px' }}
|
|
44
|
+
>
|
|
45
|
+
<h4>Left Aligned (Default)</h4>
|
|
46
|
+
<NavigationMenu justifyMenu="flex-start" menuData={dummyMenuData} />
|
|
47
|
+
</SBContainer>
|
|
48
|
+
|
|
49
|
+
<SBContainer
|
|
50
|
+
addBackground
|
|
51
|
+
style={{ overflow: 'visible', minHeight: '250px' }}
|
|
52
|
+
>
|
|
53
|
+
<h4>Center Aligned</h4>
|
|
54
|
+
<NavigationMenu justifyMenu="center" menuData={dummyMenuData} />
|
|
55
|
+
</SBContainer>
|
|
56
|
+
|
|
57
|
+
<SBContainer
|
|
58
|
+
addBackground
|
|
59
|
+
style={{ overflow: 'visible', minHeight: '250px' }}
|
|
60
|
+
>
|
|
61
|
+
<h4>Right Aligned</h4>
|
|
62
|
+
<NavigationMenu justifyMenu="flex-end" menuData={dummyMenuData} />
|
|
63
|
+
</SBContainer>
|
|
64
|
+
|
|
65
|
+
<SBContainer
|
|
66
|
+
addBackground
|
|
67
|
+
style={{ overflow: 'visible', minHeight: '250px' }}
|
|
68
|
+
>
|
|
69
|
+
<h4>Space Between</h4>
|
|
70
|
+
<NavigationMenu justifyMenu="space-between" menuData={dummyMenuData} />
|
|
71
|
+
</SBContainer>
|
|
72
|
+
|
|
73
|
+
<SBContainer
|
|
74
|
+
addBackground
|
|
75
|
+
style={{ overflow: 'visible', minHeight: '250px' }}
|
|
76
|
+
>
|
|
77
|
+
<h4>Space Around</h4>
|
|
78
|
+
<NavigationMenu justifyMenu="space-around" menuData={dummyMenuData} />
|
|
79
|
+
</SBContainer>
|
|
80
|
+
</SBContainer>
|
|
81
|
+
),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const ColorCustomization: Story = {
|
|
85
|
+
name: 'Color Customization',
|
|
86
|
+
render: () => (
|
|
87
|
+
<SBContainer flex direction="column" gap="extralarge">
|
|
88
|
+
<SBContainer
|
|
89
|
+
addBackground
|
|
90
|
+
style={{ overflow: 'visible', minHeight: '250px' }}
|
|
91
|
+
>
|
|
92
|
+
<h4>Default Colors</h4>
|
|
93
|
+
<NavigationMenu menuData={dummyMenuData} />
|
|
94
|
+
</SBContainer>
|
|
95
|
+
|
|
96
|
+
<SBContainer
|
|
97
|
+
addBackground
|
|
98
|
+
style={{ overflow: 'visible', minHeight: '250px' }}
|
|
99
|
+
>
|
|
100
|
+
<h4>Custom Link Colors</h4>
|
|
101
|
+
<NavigationMenu
|
|
102
|
+
linkColor="#2d3e50"
|
|
103
|
+
linkColorHover="#ff7a59"
|
|
104
|
+
menuData={dummyMenuData}
|
|
105
|
+
/>
|
|
106
|
+
</SBContainer>
|
|
107
|
+
|
|
108
|
+
<SBContainer
|
|
109
|
+
addBackground
|
|
110
|
+
style={{ overflow: 'visible', minHeight: '250px' }}
|
|
111
|
+
>
|
|
112
|
+
<h4>With Background Colors</h4>
|
|
113
|
+
<NavigationMenu
|
|
114
|
+
linkColor="#ffffff"
|
|
115
|
+
linkColorHover="#ffffff"
|
|
116
|
+
backgroundColor="#0066cc"
|
|
117
|
+
backgroundColorHover="#0052a3"
|
|
118
|
+
menuData={dummyMenuData}
|
|
119
|
+
/>
|
|
120
|
+
</SBContainer>
|
|
121
|
+
|
|
122
|
+
<SBContainer
|
|
123
|
+
addBackground
|
|
124
|
+
style={{ overflow: 'visible', minHeight: '250px' }}
|
|
125
|
+
>
|
|
126
|
+
<h4>Custom Submenu Colors</h4>
|
|
127
|
+
<NavigationMenu
|
|
128
|
+
linkColor="#1a1a1a"
|
|
129
|
+
linkColorHover="#e91e63"
|
|
130
|
+
backgroundColor="#fff3e0"
|
|
131
|
+
backgroundColorHover="#ffe0b2"
|
|
132
|
+
submenuLinkColor="#4a148c"
|
|
133
|
+
submenuLinkColorHover="#00e676"
|
|
134
|
+
submenuBackgroundColor="#f3e5f5"
|
|
135
|
+
submenuBackgroundColorHover="#e1bee7"
|
|
136
|
+
menuData={dummyMenuData}
|
|
137
|
+
/>
|
|
138
|
+
</SBContainer>
|
|
139
|
+
|
|
140
|
+
<SBContainer
|
|
141
|
+
style={{
|
|
142
|
+
backgroundColor: '#1a1a1a',
|
|
143
|
+
padding: '20px',
|
|
144
|
+
minHeight: '300px',
|
|
145
|
+
overflow: 'visible',
|
|
146
|
+
}}
|
|
147
|
+
>
|
|
148
|
+
<h4 style={{ color: '#ffffff', marginTop: 0 }}>Dark Theme</h4>
|
|
149
|
+
<NavigationMenu
|
|
150
|
+
linkColor="#e0e0e0"
|
|
151
|
+
linkColorHover="#ffffff"
|
|
152
|
+
backgroundColor="#2d2d2d"
|
|
153
|
+
backgroundColorHover="#3d3d3d"
|
|
154
|
+
submenuLinkColor="#e0e0e0"
|
|
155
|
+
submenuLinkColorHover="#ffffff"
|
|
156
|
+
submenuBackgroundColor="#2d2d2d"
|
|
157
|
+
submenuBackgroundColorHover="#3d3d3d"
|
|
158
|
+
menuData={dummyMenuData}
|
|
159
|
+
/>
|
|
160
|
+
</SBContainer>
|
|
161
|
+
</SBContainer>
|
|
162
|
+
),
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
export const FocusState: Story = {
|
|
166
|
+
name: 'Focus State',
|
|
167
|
+
render: () => (
|
|
168
|
+
<SBContainer flex direction="column" gap="large">
|
|
169
|
+
<SBContainer
|
|
170
|
+
addBackground
|
|
171
|
+
style={{ overflow: 'visible', minHeight: '250px' }}
|
|
172
|
+
>
|
|
173
|
+
<h4>Focus State</h4>
|
|
174
|
+
<p>
|
|
175
|
+
In production, this focus outline appears when navigating with the
|
|
176
|
+
keyboard (Tab key). This Storybook demo auto-applies the focus state
|
|
177
|
+
to show how it looks.
|
|
178
|
+
</p>
|
|
179
|
+
<SBFocusWrapper>
|
|
180
|
+
<NavigationMenu
|
|
181
|
+
navAriaLabel="Focused Navigation"
|
|
182
|
+
menuData={dummyMenuData}
|
|
183
|
+
/>
|
|
184
|
+
</SBFocusWrapper>
|
|
185
|
+
</SBContainer>
|
|
186
|
+
</SBContainer>
|
|
187
|
+
),
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export const AccessibilityFeatures: Story = {
|
|
191
|
+
name: 'Accessibility Features',
|
|
192
|
+
render: () => (
|
|
193
|
+
<SBContainer flex direction="column" gap="large">
|
|
194
|
+
<SBContainer
|
|
195
|
+
addBackground
|
|
196
|
+
style={{ overflow: 'visible', minHeight: '250px' }}
|
|
197
|
+
>
|
|
198
|
+
<h4>ARIA Label</h4>
|
|
199
|
+
<p>
|
|
200
|
+
The navigation element includes an aria-label for screen readers
|
|
201
|
+
(inspect the element to see it).
|
|
202
|
+
</p>
|
|
203
|
+
<NavigationMenu
|
|
204
|
+
navAriaLabel="Main Site Navigation"
|
|
205
|
+
menuData={dummyMenuData}
|
|
206
|
+
/>
|
|
207
|
+
</SBContainer>
|
|
208
|
+
|
|
209
|
+
<SBContainer
|
|
210
|
+
addBackground
|
|
211
|
+
style={{ overflow: 'visible', minHeight: '250px' }}
|
|
212
|
+
>
|
|
213
|
+
<h4>Custom ARIA Label</h4>
|
|
214
|
+
<p>
|
|
215
|
+
You can customize the ARIA label to describe the navigation purpose.
|
|
216
|
+
</p>
|
|
217
|
+
<NavigationMenu
|
|
218
|
+
navAriaLabel="Footer Navigation"
|
|
219
|
+
menuData={dummyMenuData}
|
|
220
|
+
/>
|
|
221
|
+
</SBContainer>
|
|
222
|
+
|
|
223
|
+
<SBContainer
|
|
224
|
+
addBackground
|
|
225
|
+
style={{ overflow: 'visible', minHeight: '250px' }}
|
|
226
|
+
>
|
|
227
|
+
<h4>Keyboard Accessible</h4>
|
|
228
|
+
<p>
|
|
229
|
+
All menu items and submenus are fully keyboard accessible using Tab,
|
|
230
|
+
Enter, and Arrow keys.
|
|
231
|
+
</p>
|
|
232
|
+
<NavigationMenu
|
|
233
|
+
navAriaLabel="Accessible Navigation Demo"
|
|
234
|
+
menuData={dummyMenuData}
|
|
235
|
+
/>
|
|
236
|
+
</SBContainer>
|
|
237
|
+
</SBContainer>
|
|
238
|
+
),
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
export const RealWorldExample: Story = {
|
|
242
|
+
name: 'In Context',
|
|
243
|
+
render: () => (
|
|
244
|
+
<SBContainer
|
|
245
|
+
style={{
|
|
246
|
+
backgroundColor: '#ffffff',
|
|
247
|
+
borderBottom: '1px solid #cbd6e2',
|
|
248
|
+
padding: '0',
|
|
249
|
+
minHeight: '350px',
|
|
250
|
+
overflow: 'visible',
|
|
251
|
+
}}
|
|
252
|
+
>
|
|
253
|
+
<SBContainer
|
|
254
|
+
flex
|
|
255
|
+
direction="row"
|
|
256
|
+
alignItems="center"
|
|
257
|
+
justifyContent="space-between"
|
|
258
|
+
style={{
|
|
259
|
+
maxWidth: '1200px',
|
|
260
|
+
margin: '0 auto',
|
|
261
|
+
padding: '0 50px',
|
|
262
|
+
}}
|
|
263
|
+
>
|
|
264
|
+
<SBContainer
|
|
265
|
+
style={{
|
|
266
|
+
fontSize: '24px',
|
|
267
|
+
fontWeight: 'bold',
|
|
268
|
+
color: '#33475b',
|
|
269
|
+
}}
|
|
270
|
+
>
|
|
271
|
+
Acme Corp
|
|
272
|
+
</SBContainer>
|
|
273
|
+
<NavigationMenu
|
|
274
|
+
justifyMenu="flex-end"
|
|
275
|
+
linkColor="#33475b"
|
|
276
|
+
linkColorHover="#0066cc"
|
|
277
|
+
submenuLinkColor="#516f90"
|
|
278
|
+
submenuLinkColorHover="#0066cc"
|
|
279
|
+
submenuBackgroundColor="#ffffff"
|
|
280
|
+
submenuBackgroundColorHover="#f5f8fa"
|
|
281
|
+
menuData={dummyMenuData}
|
|
282
|
+
/>
|
|
283
|
+
</SBContainer>
|
|
284
|
+
</SBContainer>
|
|
285
|
+
),
|
|
286
|
+
};
|
package/components/componentLibrary/Menu/NavigationMenu/stories/NavigationMenuDecorator.module.scss
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
.navigationMenuContainer {
|
|
2
|
+
--hscl-navigationMenu-gap: 0px;
|
|
3
|
+
--hscl-navigationMenu-color: #33475b;
|
|
4
|
+
--hscl-navigationMenu-color-hover: #0066cc;
|
|
5
|
+
--hscl-navigationMenu-backgroundColor: transparent;
|
|
6
|
+
--hscl-navigationMenu-backgroundColor-hover: #f5f8fa;
|
|
7
|
+
--hscl-navigationMenu-submenu-color: #33475b;
|
|
8
|
+
--hscl-navigationMenu-submenu-color-hover: #0066cc;
|
|
9
|
+
--hscl-navigationMenu-submenu-backgroundColor: #ffffff;
|
|
10
|
+
--hscl-navigationMenu-submenu-backgroundColor-hover: #f5f8fa;
|
|
11
|
+
|
|
12
|
+
nav li {
|
|
13
|
+
margin-bottom: 0;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Decorator } from '@storybook/react';
|
|
2
|
+
import styles from './NavigationMenuDecorator.module.scss';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shared decorator for NavigationMenu stories that applies default CSS variables
|
|
6
|
+
* and scoped styles to prevent pollution across other Storybook stories.
|
|
7
|
+
*/
|
|
8
|
+
export const withNavigationMenuStyles: Decorator = Story => (
|
|
9
|
+
<div className={styles.navigationMenuContainer}>
|
|
10
|
+
<Story />
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import BaseContentFields from '../ContentFields.js';
|
|
2
|
+
import { VerticalMenuContentFieldProps } from './types.js';
|
|
3
|
+
|
|
4
|
+
const ContentFields = ({
|
|
5
|
+
menuLabel,
|
|
6
|
+
menuName,
|
|
7
|
+
menuDefault,
|
|
8
|
+
}: VerticalMenuContentFieldProps) => {
|
|
9
|
+
return (
|
|
10
|
+
<>
|
|
11
|
+
<BaseContentFields
|
|
12
|
+
menuLabel={menuLabel}
|
|
13
|
+
menuName={menuName}
|
|
14
|
+
menuDefault={menuDefault}
|
|
15
|
+
/>
|
|
16
|
+
</>
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default ContentFields;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// @ts-expect-error -- ?island not typed
|
|
2
|
+
import VerticalMenuIsland from './islands/verticalMenuIsland.js?island';
|
|
3
|
+
import { VerticalMenuProps } from './types.js';
|
|
4
|
+
import { Island } from '@hubspot/cms-components';
|
|
5
|
+
import ContentFields from './ContentFields.js';
|
|
6
|
+
|
|
7
|
+
const VerticalMenuComponent = (props: VerticalMenuProps) => {
|
|
8
|
+
return <Island module={VerticalMenuIsland} {...props} />;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type VerticalMenuComponentType = typeof VerticalMenuComponent & {
|
|
12
|
+
ContentFields: typeof ContentFields;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const VerticalMenu = VerticalMenuComponent as VerticalMenuComponentType;
|
|
16
|
+
VerticalMenu.ContentFields = ContentFields;
|
|
17
|
+
|
|
18
|
+
export default VerticalMenu;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
.verticalMenu {
|
|
2
|
+
width: var(--hscl-verticalMenu-width, 100%);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.menuList {
|
|
6
|
+
list-style: none;
|
|
7
|
+
display: flex;
|
|
8
|
+
flex-direction: column;
|
|
9
|
+
gap: var(--hscl-verticalMenu-gap, 0);
|
|
10
|
+
width: 100%;
|
|
11
|
+
margin: 0;
|
|
12
|
+
padding: 0;
|
|
13
|
+
|
|
14
|
+
// base styles for all list items
|
|
15
|
+
li {
|
|
16
|
+
position: relative;
|
|
17
|
+
|
|
18
|
+
// base styles for all links and spans
|
|
19
|
+
a, span {
|
|
20
|
+
display: block;
|
|
21
|
+
text-decoration: none;
|
|
22
|
+
padding-inline: var(--hscl-verticalMenu-paddingInline, 10px);
|
|
23
|
+
padding-block: var(--hscl-verticalMenu-paddingBlock, 8px);
|
|
24
|
+
color: var(--hscl-verticalMenu-color, currentColor);
|
|
25
|
+
background-color: var(--hscl-verticalMenu-backgroundColor, transparent);
|
|
26
|
+
|
|
27
|
+
&:hover {
|
|
28
|
+
color: var(--hscl-verticalMenu-color-hover, var(--hscl-verticalMenu-color, currentColor));
|
|
29
|
+
background-color: var(--hscl-verticalMenu-backgroundColor-hover, var(--hscl-verticalMenu-backgroundColor, transparent));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// base styles for all submenu lists
|
|
35
|
+
ul {
|
|
36
|
+
list-style: none;
|
|
37
|
+
margin: 0;
|
|
38
|
+
padding: 0;
|
|
39
|
+
|
|
40
|
+
li {
|
|
41
|
+
a, span {
|
|
42
|
+
color: var(--hscl-verticalMenu-submenu-color, var(--hscl-verticalMenu-color, currentColor));
|
|
43
|
+
background-color: var(--hscl-verticalMenu-submenu-backgroundColor, var(--hscl-verticalMenu-backgroundColor, transparent));
|
|
44
|
+
padding-inline-start: calc(var(--hscl-verticalMenu-paddingInline, 10px) + (var(--hscl-menuItem-level, 1) - 1) * var(--hscl-verticalMenu-paddingInlineStart, 0px));
|
|
45
|
+
|
|
46
|
+
&:hover {
|
|
47
|
+
color: var(--hscl-verticalMenu-submenu-color-hover, var(--hscl-verticalMenu-submenu-color, var(--hscl-verticalMenu-color-hover, var(--hscl-verticalMenu-color, currentColor))));
|
|
48
|
+
background-color: var(--hscl-verticalMenu-submenu-backgroundColor-hover, var(--hscl-verticalMenu-submenu-backgroundColor, var(--hscl-verticalMenu-backgroundColor-hover, var(--hscl-verticalMenu-backgroundColor, transparent))));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|