@hashicorp/react-components 0.0.0-nightly.2c86693 → 0.0.0-nightly.2e0daaf

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": "@hashicorp/react-components",
3
- "version": "0.0.0-nightly.2c86693",
3
+ "version": "0.0.0-nightly.2e0daaf",
4
4
  "main": "src/index.ts",
5
5
  "scripts": {
6
6
  "lint": "next-hashicorp lint",
@@ -13,6 +13,7 @@ export interface ResourceCardProps {
13
13
  link: string
14
14
  productBadges?: CardProps['productBadges']
15
15
  thumbnail: ThumbnailProps
16
+ isExternal?: CardProps['isExternal']
16
17
  }
17
18
 
18
19
  export function ResourceCard({
@@ -22,6 +23,7 @@ export function ResourceCard({
22
23
  link,
23
24
  productBadges,
24
25
  thumbnail,
26
+ isExternal,
25
27
  }: ResourceCardProps) {
26
28
  const meta: string[] = date ? [date, category] : [category]
27
29
 
@@ -32,6 +34,7 @@ export function ResourceCard({
32
34
  meta={meta}
33
35
  productBadges={productBadges}
34
36
  thumbnail={thumbnail}
37
+ isExternal={isExternal}
35
38
  />
36
39
  )
37
40
  }
@@ -6,6 +6,7 @@ import * as NavigationMenu from '@radix-ui/react-navigation-menu'
6
6
  import s from './style.module.css'
7
7
 
8
8
  export interface LinkListProps {
9
+ locale?: string
9
10
  links: {
10
11
  title: string
11
12
  url: string
@@ -37,7 +38,21 @@ const GRADIENT_VALUE_MAP = new Map([
37
38
  ],
38
39
  ])
39
40
 
40
- export default function LinkList({ links, linkClickHandler }: LinkListProps) {
41
+ function getLocalizedHref(url: string, locale?: string): string {
42
+ const parsedHref = parseUrl(url).href
43
+
44
+ if (parsedHref.startsWith('/') && locale) {
45
+ return `/${locale}${parsedHref}`
46
+ }
47
+
48
+ return parsedHref
49
+ }
50
+
51
+ export default function LinkList({
52
+ locale,
53
+ links,
54
+ linkClickHandler,
55
+ }: LinkListProps) {
41
56
  return (
42
57
  <ul className={s.linkList}>
43
58
  {links.map((link) => {
@@ -53,8 +68,9 @@ export default function LinkList({ links, linkClickHandler }: LinkListProps) {
53
68
  <li key={link.title}>
54
69
  <NavigationMenu.Link asChild>
55
70
  <Link
71
+ hrefLang={locale ? locale : undefined}
56
72
  className={s.link}
57
- href={parseUrl(link.url).href}
73
+ href={getLocalizedHref(link.url, locale)}
58
74
  onClickCapture={() => {
59
75
  linkClickHandler && linkClickHandler(link.title, link.url)
60
76
  }}
@@ -12,12 +12,14 @@ interface StandardPanelProps {
12
12
  navData: NavItem[]
13
13
  promo?: PromoProps
14
14
  sectionName?: string
15
+ locale?: any
15
16
  }
16
17
 
17
18
  const StandardPanel = ({
18
19
  navData,
19
20
  promo,
20
21
  sectionName = '',
22
+ locale,
21
23
  }: StandardPanelProps) => {
22
24
  return (
23
25
  <NavPanel.Root>
@@ -26,6 +28,7 @@ const StandardPanel = ({
26
28
  return (
27
29
  <NavPanel.Column key={stableIdx}>
28
30
  <LinkList
31
+ locale={locale}
29
32
  links={navItems}
30
33
  linkClickHandler={(text, href) => {
31
34
  trackNavClickEvent(text, href, sectionName)
@@ -115,6 +115,7 @@ function SubnavInner({
115
115
  theme={theme}
116
116
  />
117
117
  </NavigationMenu.Link>
118
+
118
119
  {!hasOverflow && (
119
120
  <div className={s.subNavRight}>
120
121
  <MenuItemsDefault
@@ -44,7 +44,7 @@
44
44
 
45
45
  .subnavInner {
46
46
  display: grid;
47
- grid-template-columns: minmax(max-content, 1fr) min-content;
47
+ grid-template-columns: max-content min-content;
48
48
  align-items: center;
49
49
  justify-content: space-between;
50
50
  min-height: 62px;
@@ -1,4 +1,4 @@
1
- import { parseUrl } from '.'
1
+ import { parseUrl, hasHashiCorpDestination } from '.'
2
2
 
3
3
  describe('parseUrl', () => {
4
4
  it('detects hashicorp.com URLs as internal', () => {
@@ -7,6 +7,12 @@ describe('parseUrl', () => {
7
7
  expect(url.linkType).toBe('inbound')
8
8
  })
9
9
 
10
+ it('detects www.hashicorp.com URLs as internal', () => {
11
+ const url = parseUrl('https://hashicorp.com/foo')
12
+ expect(url.href).toBe('/foo')
13
+ expect(url.linkType).toBe('inbound')
14
+ })
15
+
10
16
  it('detects URLs not containing hashicorp.com as external', () => {
11
17
  const externalUrl = 'https://google.com/example'
12
18
  const url = parseUrl(externalUrl)
@@ -38,3 +44,26 @@ describe('parseUrl', () => {
38
44
  expect(url.href).toBe('https://developer.hashicorp.com/terraform')
39
45
  })
40
46
  })
47
+
48
+ describe('hasHashiCorpDestination', () => {
49
+ it('returns true for supported hashicorp domains', () => {
50
+ const isHashiCorpDestination = hasHashiCorpDestination(
51
+ 'https://portal.cloud.hashicorp.com/foo'
52
+ )
53
+ expect(isHashiCorpDestination).toBe(true)
54
+ })
55
+ it('returns false for external domains', () => {
56
+ const isHashiCorpDestination = hasHashiCorpDestination(
57
+ 'https://foo.com/bar'
58
+ )
59
+ expect(isHashiCorpDestination).toBe(false)
60
+ })
61
+ it('returns true for relative links', () => {
62
+ const isHashiCorpDestination = hasHashiCorpDestination('/bar')
63
+ expect(isHashiCorpDestination).toBe(true)
64
+ })
65
+ it('returns true for hashes', () => {
66
+ const isHashiCorpDestination = hasHashiCorpDestination('#someHash')
67
+ expect(isHashiCorpDestination).toBe(true)
68
+ })
69
+ })
@@ -1,10 +1,47 @@
1
+ export const hashicorpDestinations: string[] = [
2
+ 'app.terraform.io',
3
+ 'boundaryproject.io',
4
+ 'cloud.hashicorp.com',
5
+ 'consul.io',
6
+ 'developer.hashicorp.com',
7
+ 'hashicorp.com',
8
+ 'nomadproject.io',
9
+ 'packer.io',
10
+ 'portal.cloud.hashicorp.com',
11
+ 'registry.terraform.io',
12
+ 'terraform.io',
13
+ 'vagrantup.com',
14
+ 'vaultproject.io',
15
+ 'waypointproject.io',
16
+ ] as const
17
+
18
+ const containsHashiCorpDestination = (str: string): boolean =>
19
+ hashicorpDestinations.some(function (destination) {
20
+ return str.indexOf(destination) >= 0
21
+ })
22
+
23
+ /**
24
+ * This function determines if a `url` is a HashiCorp link (relative URLs, hashes,
25
+ * or a url with a HashiCorp domain) or a non-HashiCorp link.
26
+ *
27
+ * @param {string | undefined} url - The URL to be parsed. It may be a relative URL, an anchor, or a full external/internal URL.
28
+ *
29
+ * @returns {{ linkType: 'inbound' | 'outbound', href: string }}
30
+ * An object with two properties:
31
+ * - `linkType`: Indicates whether the URL is 'inbound' or 'outbound'. Can be used to conditionally
32
+ * render the value of the target attribute.
33
+ * - `href`: The parsed href (may include pathname, search, and hash for internal URLs).
34
+ *
35
+ * @throws {Error} Throws an error if the URL is undefined or invalid.
36
+ */
37
+
1
38
  export function parseUrl(url: string | undefined): {
2
- linkType: 'inbound' | 'outbound'
39
+ linkType: 'inbound' | 'outbound' | ''
3
40
  href: string
4
41
  } {
5
42
  if (!url) {
6
- console.log('url is required')
7
- return { linkType: 'outbound', href: '' }
43
+ console.log('No url to parse, returning empty string')
44
+ return { linkType: '', href: '' }
8
45
  }
9
46
 
10
47
  if (isRelative(url) || isAnchor(url)) {
@@ -18,17 +55,45 @@ export function parseUrl(url: string | undefined): {
18
55
  return { linkType: 'inbound', href }
19
56
  } catch (error) {
20
57
  console.log(`Invalid href: ${url}`)
21
- return { linkType: 'outbound', href: '' }
58
+ return { linkType: '', href: '' }
22
59
  }
23
60
  }
24
61
 
25
62
  return { linkType: 'outbound', href: url }
26
63
  }
27
64
 
28
- const isRelative = (url) => url.startsWith('/')
29
- const isAnchor = (url) => url.startsWith('#')
65
+ const isRelative = (url: string) => url.startsWith('/')
66
+ const isAnchor = (url: string) => url.startsWith('#')
67
+
68
+ /**
69
+ * Determines whether the provided url uses hashicorp.com or www.hashicorp.com as the hostname.
70
+ *
71
+ * @param {string} url - The URL to check.
72
+ * @returns {boolean} - Returns true if the link uses a HashiCorp domain, otherwise false.
73
+ */
30
74
 
31
- export const isInternal = (url) => {
75
+ export const isInternal = (url: string): boolean => {
32
76
  const hostname = new URL(url).hostname
33
- return hostname === 'hashicorp.com'
77
+ return hostname === 'hashicorp.com' || hostname === 'www.hashicorp.com'
78
+ }
79
+
80
+ /**
81
+ * Determines whether the provided url uses a HashiCorp hostname. This is used in several
82
+ * components to open a new tab for non-HashiCorp links.
83
+ *
84
+ * @param {string} url - The URL to check.
85
+ * @returns {boolean} - Returns true if the link uses a HashiCorp domain, otherwise false.
86
+ */
87
+
88
+ export const hasHashiCorpDestination = (url: string): boolean => {
89
+ // Fast return in case a relative URL or anchor is passed in
90
+ if (isRelative(url) || isAnchor(url)) return true
91
+ try {
92
+ const hostname = new URL(url).hostname
93
+ const isHashiCorpDestination = containsHashiCorpDestination(hostname)
94
+ return isHashiCorpDestination
95
+ } catch (e) {
96
+ console.error('Error: `url` is not a valid URL', e)
97
+ return false
98
+ }
34
99
  }