@hubspot/cms-component-library 0.3.5 → 0.3.7

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.
@@ -439,7 +439,7 @@ The Accordion component follows accessibility best practices:
439
439
  - **Gap selection**: Use any valid CSS length value (e.g., '8px', '16px', '24px', '48px') for spacing between items
440
440
  - **Dynamic rendering**: Always provide unique `key` props when mapping arrays to AccordionItems
441
441
  - **Rich content**: AccordionContent supports any HTML content including lists, paragraphs, images, and nested components
442
- - **Prefer library components**: When adding content inside AccordionContent, prefer using library components (e.g., `List`, `Heading`) over raw HTML elements for consistent theming and styling
442
+ - **Prefer library components**: When adding content inside AccordionContent, prefer using library components (e.g., `List`, `Text` with `textFeatureSet="heading"` for headings) over raw HTML elements for consistent theming and styling
443
443
  - **CSS Variables**: Override design tokens using CSS variables rather than hardcoding values
444
444
  - **Single responsibility**: Keep each accordion item focused on one topic for better UX
445
445
 
@@ -182,4 +182,4 @@ The Card component follows accessibility best practices:
182
182
 
183
183
  - **Flex**: Use for arranging cards in flexible layouts
184
184
  - **Grid**: Use for creating responsive card grids
185
- - **Heading**: Commonly used for card titles
185
+ - **Text**: Use with `textFeatureSet="heading"` for editable card titles
@@ -10,7 +10,7 @@ const ContentFields = ({
10
10
  <FormField
11
11
  label={formIdLabel}
12
12
  name={formIdName}
13
- embedVersions={['v4']}
13
+ embedVersions={['v4', 'v3', 'v2']}
14
14
  default={formIdDefault}
15
15
  />
16
16
  );
@@ -1,11 +1,21 @@
1
1
  // @ts-expect-error -- ?island not typed
2
2
  import FormIsland from './islands/FormIsland.js?island';
3
+ // @ts-expect-error -- ?island not typed
4
+ import LegacyFormIsland from './islands/LegacyFormIsland.js?island';
3
5
  import { FormProps } from './types.js';
4
6
  import { Island } from '@hubspot/cms-components';
5
7
  import ContentFields from './ContentFields.js';
6
8
 
7
- const FormComponent = (props: FormProps) => {
8
- return <Island module={FormIsland} {...props} />;
9
+ const FormComponent = ({
10
+ formField,
11
+ formId,
12
+ formVersion,
13
+ ...rest
14
+ }: FormProps) => {
15
+ const resolvedFormId = formField != null ? formField.form_id : formId;
16
+ const FormModule = formVersion === 'v4' ? FormIsland : LegacyFormIsland;
17
+
18
+ return <Island module={FormModule} formId={resolvedFormId} {...rest} />;
9
19
  };
10
20
 
11
21
  type FormComponentType = typeof FormComponent & {
@@ -1,15 +1,15 @@
1
1
  import { useEffect } from 'react';
2
- import { FormProps, InternalFormProps } from '../types.js';
3
- import { getHubID } from '@hubspot/cms-components';
2
+ import { FormProps } from '../types.js';
3
+ import { getHubID, getHSEnv } from '@hubspot/cms-components';
4
4
 
5
5
  const getScriptSrc = (portalId: number, env: string) => {
6
6
  const host = env === 'qa' ? 'js.hsformsqa.net' : 'js.hsforms.net';
7
- return `https://${host}/forms/embed/${portalId}.js`;
7
+ return `https://${host}/forms/embed/developer/${portalId}.js`;
8
8
  };
9
9
 
10
- const FormIsland = ({ formId, env }: FormProps & InternalFormProps) => {
10
+ const FormIsland = ({ formId }: FormProps) => {
11
11
  const portalId = getHubID();
12
- const resolvedEnv = env === 'qa' ? env : 'prod';
12
+ const resolvedEnv = getHSEnv();
13
13
 
14
14
  useEffect(() => {
15
15
  if (!formId || !portalId) {
@@ -0,0 +1,77 @@
1
+ import { useEffect, useId } from 'react';
2
+ import { FormProps } from '../types.js';
3
+ import { getHubID, getHSEnv } from '@hubspot/cms-components';
4
+
5
+ declare global {
6
+ interface Window {
7
+ hbspt?: {
8
+ forms: {
9
+ create: (options: {
10
+ portalId: number;
11
+ formId: string;
12
+ env: 'qa' | 'prod';
13
+ target: string;
14
+ }) => void;
15
+ };
16
+ };
17
+ }
18
+ }
19
+
20
+ const getScriptSrc = (env: string) => {
21
+ const host = env === 'qa' ? 'js.hsformsqa.net' : 'js.hsforms.net';
22
+ return `//${host}/forms/embed/v2.js`;
23
+ };
24
+
25
+ const LegacyFormIsland = ({ formId }: FormProps) => {
26
+ const portalId = getHubID();
27
+ const resolvedEnv = getHSEnv();
28
+ const rawId = useId();
29
+ const containerId = `hs-legacy-form-${rawId.replace(/:/g, '')}`;
30
+
31
+ useEffect(() => {
32
+ if (!formId || !portalId) {
33
+ return;
34
+ }
35
+
36
+ const scriptSrc = getScriptSrc(resolvedEnv);
37
+
38
+ const createForm = () => {
39
+ window.hbspt?.forms.create({
40
+ portalId,
41
+ formId,
42
+ env: resolvedEnv,
43
+ target: `#${containerId}`,
44
+ });
45
+ };
46
+
47
+ const existingScript = document.querySelector(`script[src="${scriptSrc}"]`);
48
+
49
+ if (existingScript) {
50
+ if (window.hbspt) {
51
+ createForm();
52
+ } else {
53
+ existingScript.addEventListener('load', createForm);
54
+ return () => existingScript.removeEventListener('load', createForm);
55
+ }
56
+ return;
57
+ }
58
+
59
+ const script = document.createElement('script');
60
+ script.src = scriptSrc;
61
+ script.addEventListener('load', createForm);
62
+ document.head.appendChild(script);
63
+
64
+ return () => {
65
+ script.removeEventListener('load', createForm);
66
+ script.remove();
67
+ };
68
+ }, [formId, portalId, resolvedEnv, containerId]);
69
+
70
+ if (!formId || !portalId) {
71
+ return null;
72
+ }
73
+
74
+ return <div id={containerId} />;
75
+ };
76
+
77
+ export default LegacyFormIsland;
@@ -1,9 +1,17 @@
1
1
  import { FormFieldDefaults } from '@hubspot/cms-components/fields';
2
- export type InternalFormProps = {
3
- env?: 'qa' | 'prod';
4
- };
5
- export type FormProps = {
2
+
3
+ export type FormProps = FormPropsWithField | FormPropsWithoutField;
4
+
5
+ export type FormPropsWithoutField = {
6
+ formField?: never;
6
7
  formId: string;
8
+ formVersion: 'v4' | 'v3' | 'v2' | '';
9
+ };
10
+
11
+ export type FormPropsWithField = {
12
+ formField: typeof FormFieldDefaults;
13
+ formId?: never;
14
+ formVersion?: never;
7
15
  };
8
16
 
9
17
  export type ContentFieldsProps = {
@@ -109,13 +109,13 @@ Grid/
109
109
 
110
110
  **Purpose:** Wraps grid children to control their precise placement, spanning, and alignment within the Grid container. Can render as any HTML element or custom React component while maintaining grid positioning control.
111
111
 
112
- **Key Feature:** The `as` prop is polymorphic - it accepts both HTML element strings ('div', 'section', etc.) and React component references (Button, Heading, custom components). GridItem handles grid placement while passing through all other props to the underlying component.
112
+ **Key Feature:** The `as` prop is polymorphic - it accepts both HTML element strings ('div', 'section', etc.) and React component references (Button, Text, custom components). GridItem handles grid placement while passing through all other props to the underlying component.
113
113
 
114
114
  **Props:**
115
115
  ```tsx
116
116
  {
117
117
  as?: React.ElementType; // Any HTML element or React component (default: 'div')
118
- // Examples: 'div', 'article', Button, Heading, CustomComponent
118
+ // Examples: 'div', 'article', Button, Text, CustomComponent
119
119
  gridColumn?: string; // Grid column placement (e.g., '1 / 3', 'span 2', '2')
120
120
  gridColumnMd?: string; // Grid column placement at tablet breakpoint (768px+)
121
121
  gridColumnLg?: string; // Grid column placement at desktop breakpoint (1024px+)
@@ -148,7 +148,7 @@ GridItem's `as` prop provides flexibility in rendering while maintaining grid co
148
148
  2. **React Components**: Pass component references directly
149
149
  ```tsx
150
150
  <GridItem as={Button} buttonType="primary">Click me</GridItem>
151
- <GridItem as={Heading} headingLevel="h2">Title</GridItem>
151
+ <GridItem as={Text} fieldPath="title" />
152
152
  ```
153
153
 
154
154
  3. **Prop Pass-Through**: All props beyond GridItem's own props are forwarded to the underlying component
@@ -158,10 +158,8 @@ GridItem's `as` prop provides flexibility in rendering while maintaining grid co
158
158
  Link Button
159
159
  </GridItem>
160
160
 
161
- {/* Heading-specific props are passed through */}
162
- <GridItem as={Heading} headingLevel="h2" displayAs="h1">
163
- Styled Heading
164
- </GridItem>
161
+ {/* Text-specific props are passed through */}
162
+ <GridItem as={Text} fieldPath="sectionTitle" className="custom-text" />
165
163
  ```
166
164
 
167
165
  4. **Grid Positioning**: GridItem handles all grid-specific positioning regardless of the underlying component
@@ -199,7 +197,7 @@ import Grid, { GridItem } from '@hubspot/cms-component-library/Grid';
199
197
  ```tsx
200
198
  import Grid, { GridItem } from '@hubspot/cms-component-library/Grid';
201
199
  import Button from '@hubspot/cms-component-library/Button';
202
- import Heading from '@hubspot/cms-component-library/Heading';
200
+ import Text from '@hubspot/cms-component-library/Text';
203
201
 
204
202
  <Grid templateColumns="repeat(2, 1fr)" gap="16px">
205
203
  {/* GridItem rendering as Button */}
@@ -211,13 +209,9 @@ import Heading from '@hubspot/cms-component-library/Heading';
211
209
  Click Me
212
210
  </GridItem>
213
211
 
214
- {/* GridItem rendering as Heading */}
215
- <GridItem
216
- as={Heading}
217
- headingLevel="h2"
218
- displayAs="h3"
219
- >
220
- Section Title
212
+ {/* GridItem rendering as Text (heading feature set) */}
213
+ <GridItem>
214
+ <Text fieldPath="sectionTitle" />
221
215
  </GridItem>
222
216
 
223
217
  {/* Regular content */}
@@ -255,6 +249,9 @@ import Heading from '@hubspot/cms-component-library/Heading';
255
249
  ### Complex Layout Using Props (NO custom CSS)
256
250
 
257
251
  ```tsx
252
+ import Grid, { GridItem } from '@hubspot/cms-component-library/Grid';
253
+ import Text from '@hubspot/cms-component-library/Text';
254
+
258
255
  {/* Dashboard layout - all positioning via props */}
259
256
  <Grid
260
257
  templateColumns="1fr"
@@ -267,7 +264,7 @@ import Heading from '@hubspot/cms-component-library/Heading';
267
264
  gridColumn="1"
268
265
  gridColumnMd="1 / -1"
269
266
  >
270
- <Heading headingLevel="h1">Dashboard</Heading>
267
+ <Text fieldPath="dashboardTitle" />
271
268
  </GridItem>
272
269
 
273
270
  {/* Sidebar - hidden on mobile, shown on tablet+ */}
@@ -1,114 +1,23 @@
1
- import styles from './index.module.scss';
2
- import cx from '../utils/classname.js';
3
- import { getLanguageDisplayName } from './utils.js';
4
- import LanguageOptions from './LanguageOptions.js';
5
- import GlobeIcon from './assets/Globe.js';
1
+ // @ts-expect-error -- ?island not typed
2
+ import LanguageSwitcherIsland from './islands/LanguageSwitcherIsland.js?island';
6
3
  import type { LanguageSwitcherProps } from './types.js';
4
+ import { Island, useLanguageVariants } from '@hubspot/cms-components';
7
5
  import ContentFields from './ContentFields.js';
8
6
  import StyleFields from './StyleFields.js';
9
- import Button from '../Button/index.js';
10
- import { useLanguageVariants } from '@hubspot/cms-components';
11
- import { createTranslationsArrayAsObject } from './utils.js';
12
- import Drawer, { useDrawer } from '../Drawer/index.js';
13
- import { dummyTranslationsAsObject, dummyTranslations } from './_dummyData.js';
14
- import Divider from '../Divider/index.js';
15
- import type { CSSVariables } from '../utils/types.js';
16
7
 
17
8
  const LanguageSwitcherComponent = ({
18
- useDummyData = false, // !todo - remove this in PROD (NOTE: storybook still needs the _dummyData, so add the _dummyData file to storybook dir and refactor usage there)
19
- menuBackgroundColor,
20
- menuBackgroundColorHover,
21
- color,
22
- colorHover,
23
- languageSwitcherSelectText = 'Select a language',
24
- className = '',
25
- style = {},
9
+ translations: translationsProp,
10
+ ...rest
26
11
  }: LanguageSwitcherProps) => {
27
- const translations = useDummyData ? dummyTranslations : useLanguageVariants(); // !todo remove this in PROD
28
- const translationsArrayAsObject = useDummyData
29
- ? dummyTranslationsAsObject
30
- : createTranslationsArrayAsObject(translations); // !todo remove this in PROD
31
- const activeTranslation = translations?.find(item => item.isActive);
32
- const currentPageLanguage = activeTranslation?.languageCode ?? '';
33
- const currentPageLanguageDisplayName = getLanguageDisplayName({
34
- currentPageLanguage,
35
- translationsArrayAsObject,
36
- });
37
-
38
- const { isOpen, toggle, close } = useDrawer();
39
-
40
- const isInEditor = false; // !todo - update this to use useEditorVariableChecks() hook (is_in_editor property) once it's working
41
-
42
- const colorCss = color?.rgba;
43
- const menuBackgroundColorCss = menuBackgroundColor?.rgba;
44
-
45
- const cssVariables = {
46
- ...(colorCss && { '--hscl-languageSwitcher-color': colorCss }),
47
- ...(menuBackgroundColorCss && {
48
- '--hscl-languageSwitcher-backgroundColor': menuBackgroundColorCss,
49
- }),
50
- };
51
-
52
- const dividerCssVariables: CSSVariables = {
53
- '--hscl-divider-borderColor': colorCss,
54
- };
55
-
56
- const combinedClasses = cx(styles.languageSwitcher, className);
12
+ const languageVariants = useLanguageVariants();
13
+ const translations = translationsProp ?? languageVariants;
57
14
 
58
15
  return (
59
- <div style={{ ...cssVariables, ...style }} className={combinedClasses}>
60
- <Button
61
- buttonType="button"
62
- aria-expanded={isOpen}
63
- onClick={toggle}
64
- className={styles.button}
65
- style={{ color: colorCss }}
66
- >
67
- <GlobeIcon />
68
- {currentPageLanguageDisplayName}
69
- </Button>
70
- {!isInEditor && (
71
- <Drawer
72
- open={isOpen}
73
- direction="right"
74
- onOverlayClick={close}
75
- aria-label="Language selector"
76
- className={styles.drawer}
77
- style={{
78
- ...cssVariables,
79
- backgroundColor: menuBackgroundColorCss,
80
- }}
81
- >
82
- <div className={styles.sidePanelHeader}>
83
- <span className={styles.sidePanelTitle}>
84
- {languageSwitcherSelectText}
85
- </span>
86
- <Button
87
- buttonType="button"
88
- onClick={close}
89
- aria-label="Close language selector"
90
- className={styles.sidePanelCloseButton}
91
- >
92
-
93
- </Button>
94
- </div>
95
-
96
- <Divider spacing="0px" style={dividerCssVariables} />
97
-
98
- <div className={styles.sidePanelOptionsContainer}>
99
- <LanguageOptions
100
- {...{
101
- translations,
102
- menuBackgroundColor,
103
- menuBackgroundColorHover,
104
- color,
105
- colorHover,
106
- }}
107
- />
108
- </div>
109
- </Drawer>
110
- )}
111
- </div>
16
+ <Island
17
+ module={LanguageSwitcherIsland}
18
+ translations={translations}
19
+ {...rest}
20
+ />
112
21
  );
113
22
  };
114
23
 
@@ -0,0 +1,110 @@
1
+ import styles from '../index.module.scss';
2
+ import cx from '../../utils/classname.js';
3
+ import { getLanguageDisplayName } from '../utils.js';
4
+ import LanguageOptions from '../LanguageOptions.js';
5
+ import GlobeIcon from '../assets/Globe.js';
6
+ import type { LanguageSwitcherIslandProps } from '../types.js';
7
+ import Button from '../../Button/index.js';
8
+ import { createTranslationsArrayAsObject } from '../utils.js';
9
+ import Drawer, { useDrawer } from '../../Drawer/index.js';
10
+ import Divider from '../../Divider/index.js';
11
+ import type { CSSVariables } from '../../utils/types.js';
12
+
13
+ const LanguageSwitcherIsland = ({
14
+ translations,
15
+ menuBackgroundColor,
16
+ menuBackgroundColorHover,
17
+ color,
18
+ colorHover,
19
+ languageSwitcherSelectText = 'Select a language',
20
+ className = '',
21
+ style = {},
22
+ }: LanguageSwitcherIslandProps) => {
23
+ const translationsArrayAsObject = createTranslationsArrayAsObject(
24
+ translations
25
+ );
26
+ const activeTranslation = translations?.find(item => item.isActive);
27
+ const currentPageLanguage = activeTranslation?.languageCode ?? '';
28
+ const currentPageLanguageDisplayName = getLanguageDisplayName({
29
+ currentPageLanguage,
30
+ translationsArrayAsObject,
31
+ });
32
+
33
+ const { isOpen, toggle, close } = useDrawer();
34
+
35
+ const isInEditor = false;
36
+
37
+ const colorCss = color?.rgba;
38
+ const menuBackgroundColorCss = menuBackgroundColor?.rgba;
39
+
40
+ const cssVariables = {
41
+ ...(colorCss && { '--hscl-languageSwitcher-color': colorCss }),
42
+ ...(menuBackgroundColorCss && {
43
+ '--hscl-languageSwitcher-backgroundColor': menuBackgroundColorCss,
44
+ }),
45
+ };
46
+
47
+ const dividerCssVariables: CSSVariables = {
48
+ '--hscl-divider-borderColor': colorCss,
49
+ };
50
+
51
+ const combinedClasses = cx(styles.languageSwitcher, className);
52
+
53
+ return (
54
+ <div style={{ ...cssVariables, ...style }} className={combinedClasses}>
55
+ <Button
56
+ buttonType="button"
57
+ aria-expanded={isOpen}
58
+ onClick={toggle}
59
+ className={styles.button}
60
+ style={{ color: colorCss }}
61
+ >
62
+ <GlobeIcon />
63
+ {currentPageLanguageDisplayName}
64
+ </Button>
65
+ {!isInEditor && (
66
+ <Drawer
67
+ open={isOpen}
68
+ direction="right"
69
+ onOverlayClick={close}
70
+ aria-label="Language selector"
71
+ className={styles.drawer}
72
+ style={{
73
+ ...cssVariables,
74
+ backgroundColor: menuBackgroundColorCss,
75
+ }}
76
+ >
77
+ <div className={styles.sidePanelHeader}>
78
+ <span className={styles.sidePanelTitle}>
79
+ {languageSwitcherSelectText}
80
+ </span>
81
+ <Button
82
+ buttonType="button"
83
+ onClick={close}
84
+ aria-label="Close language selector"
85
+ className={styles.sidePanelCloseButton}
86
+ >
87
+
88
+ </Button>
89
+ </div>
90
+
91
+ <Divider spacing="0px" style={dividerCssVariables} />
92
+
93
+ <div className={styles.sidePanelOptionsContainer}>
94
+ <LanguageOptions
95
+ {...{
96
+ translations,
97
+ menuBackgroundColor,
98
+ menuBackgroundColorHover,
99
+ color,
100
+ colorHover,
101
+ }}
102
+ />
103
+ </div>
104
+ </Drawer>
105
+ )}
106
+ </div>
107
+ );
108
+ };
109
+
110
+ export default LanguageSwitcherIsland;
@@ -15,7 +15,7 @@ The LanguageSwitcher component provides an accessible interface for users to vie
15
15
 
16
16
  ```
17
17
  LanguageSwitcher/
18
- ├── index.tsx # Main component with render logic
18
+ ├── index.tsx # Main component (wrapper that delegates to Island)
19
19
  ├── types.ts # TypeScript type definitions
20
20
  ├── ContentFields.tsx # HubSpot field definitions for content
21
21
  ├── StyleFields.tsx # HubSpot field definitions for styling
@@ -26,6 +26,8 @@ LanguageSwitcher/
26
26
  ├── index.module.scss # CSS module with design tokens
27
27
  ├── assets/
28
28
  │ └── Globe.tsx # Globe SVG icon component
29
+ ├── islands/
30
+ │ └── LanguageSwitcherIsland.tsx # Client-side island with interactive UI
29
31
  └── stories/
30
32
  ├── LanguageSwitcher.stories.tsx # Component usage examples
31
33
  ├── LanguageSwitcherDecorator.tsx # Storybook decorator
@@ -41,82 +43,43 @@ LanguageSwitcher/
41
43
  **Props:**
42
44
  ```tsx
43
45
  {
46
+ translations?: LanguageVariant[]; // Language variants (if not provided, uses useLanguageVariants hook)
44
47
  className?: string; // Additional CSS classes
45
48
  style?: React.CSSProperties; // Inline styles
46
- textColor?: ColorFieldValue; // Text color for button and menu items
47
- textColorHover?: ColorFieldValue; // Text color for menu items on hover
49
+ color?: ColorFieldValue; // Text color for button and menu items
50
+ colorHover?: ColorFieldValue; // Text color for menu items on hover
48
51
  menuBackgroundColor?: ColorFieldValue; // Background color for the drawer
49
52
  menuBackgroundColorHover?: ColorFieldValue; // Background color for menu items on hover
50
53
  languageSwitcherSelectText?: string; // Header text in drawer (default: 'Select a language')
51
- useDummyData?: boolean; // Use dummy data for testing (development only)
52
54
  }
53
55
  ```
54
56
 
55
57
  ## Usage Examples
56
58
 
57
- ### Island Usage
59
+ ### Basic Usage
58
60
 
59
- The LanguageSwitcher component requires JavaScript for interactive state management and must be used within an Island component.
60
-
61
- #### Island Component
61
+ The LanguageSwitcher component automatically handles Island-ification internally. Simply import and use it with optional props:
62
62
 
63
63
  ```tsx
64
- // islands/LanguageSwitcherIsland.tsx
65
64
  import LanguageSwitcher from '@hubspot/cms-component-library/LanguageSwitcher';
66
65
 
67
- export default function LanguageSwitcherIsland({
68
- textColor,
69
- textColorHover,
70
- menuBackgroundColor,
71
- menuBackgroundColorHover,
72
- languageSwitcherSelectText,
73
- }) {
74
- return (
75
- <LanguageSwitcher
76
- textColor={textColor}
77
- textColorHover={textColorHover}
78
- menuBackgroundColor={menuBackgroundColor}
79
- menuBackgroundColorHover={menuBackgroundColorHover}
80
- languageSwitcherSelectText={languageSwitcherSelectText}
81
- />
82
- );
66
+ export default function MyComponent() {
67
+ return <LanguageSwitcher />;
83
68
  }
84
69
  ```
85
70
 
86
- #### Module File
87
-
88
- ```tsx
89
- import { Island } from '@hubspot/cms-components';
90
- import LanguageSwitcherIsland from './islands/LanguageSwitcherIsland?island';
91
-
92
- export const Component = ({ fieldValues }) => {
93
- return (
94
- <Island
95
- module={LanguageSwitcherIsland}
96
- textColor={fieldValues.textColor}
97
- textColorHover={fieldValues.textColorHover}
98
- menuBackgroundColor={fieldValues.menuBackgroundColor}
99
- menuBackgroundColorHover={fieldValues.menuBackgroundColorHover}
100
- languageSwitcherSelectText={fieldValues.languageSwitcherSelectText}
101
- />
102
- );
103
- };
104
- ```
71
+ When used without a `translations` prop, it automatically calls `useLanguageVariants()` to fetch language data.
105
72
 
106
- ### Basic Language Switcher
73
+ ### With Custom Header Text
107
74
 
108
75
  ```tsx
109
- import LanguageSwitcher from '@hubspot/cms-component-library/LanguageSwitcher';
110
-
111
- export default function LanguageSwitcherIsland() {
112
- return <LanguageSwitcher />;
113
- }
76
+ <LanguageSwitcher languageSwitcherSelectText="Choose your language" />
114
77
  ```
115
78
 
116
- ### With Custom Header Text
79
+ ### With Custom Translations
117
80
 
118
81
  ```tsx
119
- <LanguageSwitcher languageSwitcherSelectText="Choose your language" />
82
+ <LanguageSwitcher translations={customLanguageVariants} />
120
83
  ```
121
84
 
122
85
  ### With Custom Colors
@@ -125,8 +88,8 @@ If passing in colors directly (without using a ColorField), like the below examp
125
88
 
126
89
  ```tsx
127
90
  <LanguageSwitcher
128
- textColor={{ rgba: 'rgba(51, 71, 91, 1)' }}
129
- textColorHover={{ rgba: 'rgba(0, 102, 204, 1)' }}
91
+ color={{ rgba: 'rgba(51, 71, 91, 1)' }}
92
+ colorHover={{ rgba: 'rgba(0, 102, 204, 1)' }}
130
93
  menuBackgroundColor={{ rgba: 'rgba(255, 255, 255, 1)' }}
131
94
  menuBackgroundColorHover={{ rgba: 'rgba(245, 245, 245, 1)' }}
132
95
  />
@@ -197,21 +160,19 @@ Configurable props for color customization:
197
160
 
198
161
  ### Module Usage Example
199
162
 
200
- If passing in colors using a ColorField, like the below example, pass in the entire field value. HubSpot will transform the field value from a default of { color: hex, opacity: number } to { rbga, rgb, opacity, color, hex, css }. The LanguageSwitcher component will pull the rgba value from this final output.
163
+ If passing in colors using a ColorField, like the below example, pass in the entire field value. HubSpot will transform the field value from a default of { color: hex, opacity: number } to { rgba, rgb, opacity, color, hex, css }. The LanguageSwitcher component will pull the rgba value from this final output.
201
164
 
202
165
  ```tsx
203
- import { Island } from '@hubspot/cms-components';
204
- import LanguageSwitcherIsland from './islands/LanguageSwitcherIsland?island';
166
+ import LanguageSwitcher from '@hubspot/cms-component-library/LanguageSwitcher';
205
167
 
206
168
  export default function HeaderModule({ fieldValues }) {
207
169
  return (
208
170
  <header>
209
171
  <nav>
210
172
  {/* Other navigation items */}
211
- <Island
212
- module={LanguageSwitcherIsland}
213
- textColor={fieldValues.textColor}
214
- textColorHover={fieldValues.textColorHover}
173
+ <LanguageSwitcher
174
+ color={fieldValues.color}
175
+ colorHover={fieldValues.colorHover}
215
176
  menuBackgroundColor={fieldValues.menuBackgroundColor}
216
177
  menuBackgroundColorHover={fieldValues.menuBackgroundColorHover}
217
178
  languageSwitcherSelectText={fieldValues.languageSwitcherSelectText}
@@ -280,14 +241,14 @@ The LanguageSwitcher component follows accessibility best practices:
280
241
 
281
242
  ## Best Practices
282
243
 
283
- - **Use Islands for interactivity**: The LanguageSwitcher component is interactive and requires JavaScript, so it must be used within an Island component in modules
244
+ - **Island-ification is handled internally**: The LanguageSwitcher component automatically handles Island-ification for interactive state, so you don't need to manually wrap it
284
245
  - **Position prominently**: Place in global navigation (header or footer) where users expect language controls
285
246
  - **Use native language names**: The component automatically displays language names in their native script (e.g., "Español" for Spanish, "Français" for French)
286
247
  - **Customize header text**: Set `languageSwitcherSelectText` to match your site's primary language for consistency
287
248
  - **Color coordination**: Match `textColor` and `menuBackgroundColor` to your site's theme
288
249
  - **Color setting**: If using ColorFields, pass in the entire field value, as HubSpot transforms the user's color choice into an object with an rgba key. Otherwise, ensure the color object has an rgba key and value. The component expects an object with an rgba key.
289
250
  - **Responsive design**: The drawer automatically adjusts to full width on mobile devices (≤400px)
290
- - **Test with real translations**: While `useDummyData` prop exists for development, always test with actual HubSpot language variants in production
251
+ - **Test with real translations**: Pass real HubSpot language variants via the `translations` prop, or rely on the default `useLanguageVariants()` hook for production
291
252
  - **Provide clear visual feedback**: Use hover colors that provide sufficient contrast for user interaction
292
253
  - **Consider placement**: Position near other utility navigation items (search, account, etc.)
293
254
  - **Dark mode support**: Provide appropriate color values for dark backgrounds when needed
@@ -297,8 +258,7 @@ The LanguageSwitcher component follows accessibility best practices:
297
258
  ### Header Navigation Integration
298
259
 
299
260
  ```tsx
300
- import { Island } from '@hubspot/cms-components';
301
- import LanguageSwitcherIsland from './islands/LanguageSwitcherIsland?island';
261
+ import LanguageSwitcher from '@hubspot/cms-component-library/LanguageSwitcher';
302
262
 
303
263
  export default function SiteHeader({ fieldValues }) {
304
264
  return (
@@ -315,9 +275,8 @@ export default function SiteHeader({ fieldValues }) {
315
275
  <a href="/about">About</a>
316
276
  <a href="/services">Services</a>
317
277
  <a href="/contact">Contact</a>
318
- <Island
319
- module={LanguageSwitcherIsland}
320
- textColor={fieldValues.textColor}
278
+ <LanguageSwitcher
279
+ color={fieldValues.color}
321
280
  menuBackgroundColor={fieldValues.menuBackgroundColor}
322
281
  languageSwitcherSelectText={fieldValues.languageSwitcherSelectText}
323
282
  />
@@ -330,13 +289,14 @@ export default function SiteHeader({ fieldValues }) {
330
289
  ### Footer Placement
331
290
 
332
291
  ```tsx
292
+ import LanguageSwitcher from '@hubspot/cms-component-library/LanguageSwitcher';
293
+
333
294
  export default function SiteFooter({ fieldValues }) {
334
295
  return (
335
296
  <footer style={{ padding: '48px 24px', textAlign: 'center' }}>
336
297
  <div style={{ marginBottom: '24px' }}>
337
- <Island
338
- module={LanguageSwitcherIsland}
339
- textColor={fieldValues.footerTextColor}
298
+ <LanguageSwitcher
299
+ color={fieldValues.footerTextColor}
340
300
  menuBackgroundColor={fieldValues.menuBackgroundColor}
341
301
  languageSwitcherSelectText={fieldValues.languageSwitcherSelectText}
342
302
  />
@@ -350,6 +310,8 @@ export default function SiteFooter({ fieldValues }) {
350
310
  ### Mobile-First Navigation
351
311
 
352
312
  ```tsx
313
+ import LanguageSwitcher from '@hubspot/cms-component-library/LanguageSwitcher';
314
+
353
315
  export default function MobileNav({ fieldValues }) {
354
316
  return (
355
317
  <nav style={{
@@ -362,9 +324,8 @@ export default function MobileNav({ fieldValues }) {
362
324
  <a href="/about">About</a>
363
325
  <a href="/contact">Contact</a>
364
326
  <div style={{ borderTop: '1px solid #eee', paddingTop: '16px' }}>
365
- <Island
366
- module={LanguageSwitcherIsland}
367
- textColor={fieldValues.textColor}
327
+ <LanguageSwitcher
328
+ color={fieldValues.color}
368
329
  menuBackgroundColor={fieldValues.menuBackgroundColor}
369
330
  />
370
331
  </div>
@@ -3,6 +3,7 @@ import LanguageSwitcher from '../index.js';
3
3
  import { LanguageSwitcherProps } from '../types.js';
4
4
  import { withLanguageSwitcherStyles } from './LanguageSwitcherDecorator.js';
5
5
  import { SBContainer, SBFocusWrapper } from '@sb-utils';
6
+ import { dummyTranslations } from '../_dummyData.js';
6
7
 
7
8
  const meta: Meta<LanguageSwitcherProps> = {
8
9
  title: 'Component Library/LanguageSwitcher',
@@ -21,14 +22,6 @@ const meta: Meta<LanguageSwitcherProps> = {
21
22
  },
22
23
  decorators: [withLanguageSwitcherStyles],
23
24
  argTypes: {
24
- useDummyData: {
25
- control: 'boolean',
26
- description: 'Use dummy translation data for testing',
27
- table: {
28
- category: 'Development',
29
- defaultValue: { summary: 'false' },
30
- },
31
- },
32
25
  languageSwitcherSelectText: {
33
26
  control: 'text',
34
27
  description: 'Text displayed in the drawer header',
@@ -37,14 +30,14 @@ const meta: Meta<LanguageSwitcherProps> = {
37
30
  defaultValue: { summary: 'Select a language' },
38
31
  },
39
32
  },
40
- textColor: {
33
+ color: {
41
34
  control: 'object',
42
35
  description: 'Text color for button and menu items',
43
36
  table: {
44
37
  category: 'Visual',
45
38
  },
46
39
  },
47
- textColorHover: {
40
+ colorHover: {
48
41
  control: 'object',
49
42
  description: 'Text color for menu items on hover',
50
43
  table: {
@@ -90,7 +83,7 @@ export const Default: Story = {
90
83
  return (
91
84
  <SBContainer>
92
85
  <LanguageSwitcher
93
- useDummyData={true}
86
+ translations={dummyTranslations}
94
87
  languageSwitcherSelectText="Select a language"
95
88
  />
96
89
  </SBContainer>
@@ -140,10 +133,10 @@ export const ColorThemes: Story = {
140
133
  <p>Custom color theming with purple tones.</p>
141
134
  <SBContainer>
142
135
  <LanguageSwitcher
143
- useDummyData={true}
136
+ translations={dummyTranslations}
144
137
  languageSwitcherSelectText="Wählen Sie eine Sprache"
145
- textColor={purpleTextColor}
146
- textColorHover={purpleTextColorHover}
138
+ color={purpleTextColor}
139
+ colorHover={purpleTextColorHover}
147
140
  menuBackgroundColor={purpleMenuBgColor}
148
141
  menuBackgroundColorHover={purpleMenuBgColorHover}
149
142
  />
@@ -162,10 +155,10 @@ export const ColorThemes: Story = {
162
155
  </SBContainer>
163
156
  <SBContainer>
164
157
  <LanguageSwitcher
165
- useDummyData={true}
158
+ translations={dummyTranslations}
166
159
  languageSwitcherSelectText="Select a language"
167
- textColor={darkTextColor}
168
- textColorHover={darkTextColorHover}
160
+ color={darkTextColor}
161
+ colorHover={darkTextColorHover}
169
162
  menuBackgroundColor={darkMenuBgColor}
170
163
  menuBackgroundColorHover={darkMenuBgColorHover}
171
164
  />
@@ -195,7 +188,7 @@ export const CustomText: Story = {
195
188
  <strong>🇬🇧 English:</strong>
196
189
  </p>
197
190
  <LanguageSwitcher
198
- useDummyData={true}
191
+ translations={dummyTranslations}
199
192
  languageSwitcherSelectText="Select a language"
200
193
  />
201
194
  </SBContainer>
@@ -205,7 +198,7 @@ export const CustomText: Story = {
205
198
  <strong>🇩🇪 German:</strong>
206
199
  </p>
207
200
  <LanguageSwitcher
208
- useDummyData={true}
201
+ translations={dummyTranslations}
209
202
  languageSwitcherSelectText="Sprache wählen"
210
203
  />
211
204
  </SBContainer>
@@ -215,7 +208,7 @@ export const CustomText: Story = {
215
208
  <strong>🇪🇸 Spanish:</strong>
216
209
  </p>
217
210
  <LanguageSwitcher
218
- useDummyData={true}
211
+ translations={dummyTranslations}
219
212
  languageSwitcherSelectText="Seleccionar idioma"
220
213
  />
221
214
  </SBContainer>
@@ -225,7 +218,7 @@ export const CustomText: Story = {
225
218
  <strong>🇫🇷 French:</strong>
226
219
  </p>
227
220
  <LanguageSwitcher
228
- useDummyData={true}
221
+ translations={dummyTranslations}
229
222
  languageSwitcherSelectText="Choisir une langue"
230
223
  />
231
224
  </SBContainer>
@@ -288,9 +281,9 @@ export const InContext: Story = {
288
281
 
289
282
  <div style={{ paddingTop: '6px' }}>
290
283
  <LanguageSwitcher
291
- useDummyData={true}
292
- textColor={{ rgba: 'rgb(55, 245, 255)' }}
293
- textColorHover={{ rgba: 'rgb(255, 255, 255)' }}
284
+ translations={dummyTranslations}
285
+ color={{ rgba: 'rgb(55, 245, 255)' }}
286
+ colorHover={{ rgba: 'rgb(255, 255, 255)' }}
294
287
  menuBackgroundColor={{ rgba: 'rgba(100, 181, 246, 1)' }}
295
288
  menuBackgroundColorHover={{ rgba: 'rgba(0, 160, 240, 1)' }}
296
289
  />
@@ -325,7 +318,7 @@ export const InteractionStates: Story = {
325
318
  <h4>Hover State</h4>
326
319
  <p>Hover over the language switcher button to see the hover effect</p>
327
320
  <LanguageSwitcher
328
- useDummyData={true}
321
+ translations={dummyTranslations}
329
322
  languageSwitcherSelectText="Select a language"
330
323
  />
331
324
  </SBContainer>
@@ -339,7 +332,7 @@ export const InteractionStates: Story = {
339
332
  </p>
340
333
  <SBFocusWrapper>
341
334
  <LanguageSwitcher
342
- useDummyData={true}
335
+ translations={dummyTranslations}
343
336
  languageSwitcherSelectText="Select a language"
344
337
  />
345
338
  </SBFocusWrapper>
@@ -7,8 +7,19 @@ import type { CSSVariables } from '../utils/types.js';
7
7
 
8
8
  type ColorFieldValue = typeof ColorFieldDefaults;
9
9
 
10
+ export type LanguageSwitcherIslandProps = {
11
+ translations: LanguageVariant[];
12
+ className?: string;
13
+ style?: CSSVariables;
14
+ color?: ColorFieldValue;
15
+ colorHover?: ColorFieldValue;
16
+ menuBackgroundColorHover?: ColorFieldValue;
17
+ menuBackgroundColor?: ColorFieldValue;
18
+ languageSwitcherSelectText?: string;
19
+ };
20
+
10
21
  export type LanguageSwitcherProps = {
11
- useDummyData?: boolean; // !todo - remove this in PROD
22
+ translations?: LanguageVariant[];
12
23
  className?: string;
13
24
  style?: CSSVariables;
14
25
  color?: ColorFieldValue;
@@ -83,7 +83,7 @@ When creating a new component in componentLibrary, ensure:
83
83
  - [ ] Semantic HTML elements used appropriately
84
84
  - [ ] ARIA roles added when needed (e.g., `role="separator"`)
85
85
  - [ ] Icon purpose properly set (SEMANTIC vs DECORATIVE)
86
- - [ ] Heading level vs display separated for proper semantics
86
+ - [ ] Use `Text` with `textFeatureSet="heading"` for editable heading fields rather than raw HTML heading elements
87
87
 
88
88
  ## Reference Examples
89
89
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/cms-component-library",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "description": "HubSpot CMS React component library for building CMS modules",
5
5
  "license": "Apache-2.0",
6
6
  "exports": {
@@ -21,7 +21,7 @@
21
21
  },
22
22
  "type": "module",
23
23
  "dependencies": {
24
- "@hubspot/cms-components": "1.2.17",
24
+ "@hubspot/cms-components": "1.2.19",
25
25
  "sass-embedded": "^1.97.3"
26
26
  },
27
27
  "peerDependencies": {