Skip to content

fix(components): LinkToExternal retake #2867

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
May 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tall-rings-act.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hashicorp/design-system-components': patch
---

Introduce the `hds-resolve-link-to-component` utility to correctly resolve the LinkTo component when `@isRouteExternal` is set on `HdsBreadcrumbItem` or `HdsInteractive`. Consumers are now required to install `ember-engines` when `@isRouteExternal` is `true`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for @alex-ju: does this works with the automated script for release notes generation?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The script currently only picks up changelog entries written in this format:

`ComponentName` - description for the change

8 changes: 8 additions & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,14 @@
"typescript-eslint": "^8.29.0",
"webpack": "^5.97.1"
},
"peerDependencies": {
"ember-engines": ">= 0.11.0"
},
"peerDependenciesMeta": {
"ember-engines": {
"optional": true
}
},
"ember": {
"edition": "octane"
},
Expand Down
4 changes: 2 additions & 2 deletions packages/components/src/components/hds/breadcrumb/item.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
</div>
{{else}}
{{#if @isRouteExternal}}
<LinkToExternal
<this.linkToExternal
class="hds-breadcrumb__link"
@current-when={{@current-when}}
@models={{hds-link-to-models @model @models}}
Expand All @@ -28,7 +28,7 @@
</div>
{{/if}}
<span class="hds-breadcrumb__text">{{@text}}</span>
</LinkToExternal>
</this.linkToExternal>
{{else}}
<LinkTo
class="hds-breadcrumb__link"
Expand Down
25 changes: 24 additions & 1 deletion packages/components/src/components/hds/breadcrumb/item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@
*/

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { htmlSafe } from '@ember/template';
import { assert } from '@ember/debug';

import { hdsResolveLinkToExternal } from '../../../utils/hds-resolve-link-to-external.ts';

import type Owner from '@ember/owner';
import type { LinkTo } from '@ember/routing';
import type { SafeString } from '@ember/template';
import type { HdsIconSignature } from '../icon';
import type { HdsIconSignature } from '../icon/index';

export interface HdsBreadcrumbItemSignature {
Args: {
Expand All @@ -27,6 +33,23 @@ export interface HdsBreadcrumbItemSignature {
}

export default class HdsBreadcrumbItem extends Component<HdsBreadcrumbItemSignature> {
@tracked linkToExternal: LinkTo | null = null;

constructor(owner: Owner, args: HdsBreadcrumbItemSignature['Args']) {
super(owner, args);

// we want to avoid resolving the component if it's not needed
if (args.isRouteExternal) {
void this.resolveLinkToExternal();
}
}

async resolveLinkToExternal() {
this.linkToExternal = await hdsResolveLinkToExternal(
this.args.isRouteExternal
);
}

/**
* @param maxWidth
* @type {string}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
{{! NOTICE: we can't support the direct use of the "href" HTML attribute via ...attributes in the <a> elements, because we need to rely on the "@href" Ember argument to differentiate between different types of generated output }}
{{~#if @route~}}
{{~#if this.isRouteExternal~}}
<LinkToExternal
<this.linkToExternal
@current-when={{@current-when}}
@models={{hds-link-to-models @model @models}}
@query={{hds-link-to-query @query}}
@replace={{@replace}}
@route={{@route}}
...attributes
>{{yield}}</LinkToExternal>
>{{yield}}</this.linkToExternal>
{{~else~}}
<LinkTo
@current-when={{@current-when}}
Expand Down
22 changes: 22 additions & 0 deletions packages/components/src/components/hds/interactive/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@
*/

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

import { hdsResolveLinkToExternal } from '../../../utils/hds-resolve-link-to-external.ts';

import type Owner from '@ember/owner';
import type { LinkTo } from '@ember/routing';

export interface HdsInteractiveSignature {
Args: {
href?: string;
Expand All @@ -27,6 +33,22 @@ export interface HdsInteractiveSignature {
}

export default class HdsInteractive extends Component<HdsInteractiveSignature> {
@tracked linkToExternal: LinkTo | null = null;

constructor(owner: Owner, args: HdsInteractiveSignature['Args']) {
super(owner, args);

// we want to avoid resolving the component if it's not needed
if (args.isRouteExternal) {
void this.resolveLinkToExternal();
}
}

async resolveLinkToExternal() {
this.linkToExternal = await hdsResolveLinkToExternal(
this.args.isRouteExternal
);
}
/**
* Determines if a @href value is "external" (it adds target="_blank" rel="noopener noreferrer")
*
Expand Down
31 changes: 31 additions & 0 deletions packages/components/src/utils/hds-resolve-link-to-external.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { LinkTo } from '@ember/routing';
import { assert } from '@ember/debug';

/**
* Resolves the correct component to use for the `LinkTo`.
*
* @param isRouteExternal - If true, will return the `LinkToExternal` component. If `ember-engines` is not installed, an assertion will be thrown.
* @returns A promise resolving to the correct component to use for the `LinkTo`.
*/
export async function hdsResolveLinkToExternal(
isRouteExternal?: boolean
): Promise<typeof LinkTo> {
if (isRouteExternal) {
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const mod = await import(
// @ts-expect-error: we list this as optional peer dependency
'ember-engines/components/link-to-external-component'
);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
return mod.default as typeof LinkTo;
} catch {
assert(
`@isRouteExternal is only available when using the "ember-engines" addon. Please install it to use this feature.`,
false
);
}
}

return LinkTo;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import '@glint/environment-ember-loose';
import '@glint/environment-ember-template-imports';

import { LinkTo } from '@ember/routing';
import { Portal, PortalTarget } from 'ember-stargate';

import type HdsComponentsRegistry from '../src/template-registry';
Expand All @@ -25,7 +24,6 @@ declare module '@glint/environment-ember-loose/registry' {
RenderModifiersRegistry,
EmbroiderUtilRegistry /*, other addon registries */ {
// local entries
LinkToExternal: typeof LinkTo;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[praise] Great that you remembered to remove it :)

Portal: typeof Portal;
PortalTarget: typeof PortalTarget;
'sort-by': HelperLike<{
Expand Down
Loading