diff --git a/apiExamples/dynamichostname.html b/apiExamples/dynamichostname.html new file mode 100644 index 0000000..bd10566 --- /dev/null +++ b/apiExamples/dynamichostname.html @@ -0,0 +1,53 @@ +

+ Expect this + + Alaska Airlines + + link to be redefined based on the hostname of the current page. +

+

+ Expect this + + Alaska Airlines + + link to be maintain the hostname of the original link. +

+

+ Expect this + + Alaska Airlines + + link to be maintain the hostname of the original link. +

+

+ Expect this + + Alaska Airlines + + link to be redefined based on the hostname of the current page. +

+

+ Expect this + + Alaska Airlines + + link to be redefined based on the hostname of the current page. +

+

+ Expect this + + Alaska Airlines + + link to be remain relative. +

diff --git a/docs/api.md b/docs/api.md index dfc262c..2acf9bd 100644 --- a/docs/api.md +++ b/docs/api.md @@ -10,6 +10,7 @@ | `fluid` | `fluid` | | `Boolean` | | If true and `type="cta"`, the hyperlink will have a fluid-width UI. | | `href` | `href` | | `String` | | Defines the URL of the linked page. | | `ondark` | `ondark` | | `Boolean` | false | If true, the hyperlink will be styled for use on a dark background. | +| `origin` | `origin` | | `String` | | Defines hostname for the origin of the URL. Options are `hostname` or `dynamic`. Default is `dynamic`. Using `hostname` will force the URL to be rewritten to use the hostname of the `href` attribute. Using `dynamic` will rewrite the URL to use the hostname of the current page. | | `referrerpolicy` | `referrerpolicy` | | `Boolean` | | If true, sets `strict-origin-when-cross-origin` to control the referrer information sent with requests. | | `rel` | `rel` | | `String` | | Defines the relationship between the current document and the linked document. | | `relative` | `relative` | | `Boolean` | false | If true, the auto URL re-write feature will be disabled. | diff --git a/docs/partials/api.md b/docs/partials/api.md index c37ae1c..cf6c2a7 100644 --- a/docs/partials/api.md +++ b/docs/partials/api.md @@ -25,6 +25,23 @@ +## Dynamic hostname support + +The `auro-hyperlink` component defaults to modifying the origin of any provided URL to match the hostname of the current page unless explicitly configured. Using `origin="hostname"` will retain the hostname of the URL entered into the `href` attribute. Using `target="_blank"`, the URL on the anchor tag will maintain the hostname of the URL in the `href` attribute. Using `origin="dynamic"` in combination with `target="_blank"` will allow the targeted link to match the hostname of the current page. + +
+ + +
+ + + See code + + + + + + ## External Links Hyperlinks when used with the `target="_blank"` attribute are domain aware and return either an internal domain new-window icon versus an icon that communicates users will be taken to a new domain. diff --git a/docs/partials/index.md b/docs/partials/index.md index ced20c5..c99bd27 100644 --- a/docs/partials/index.md +++ b/docs/partials/index.md @@ -31,6 +31,23 @@ If the `href` attribute is not added, the hyperlink element will render back sim +## Dynamic hostname support + +The `auro-hyperlink` component defaults to modifying the origin of any provided URL to match the hostname of the current page unless explicitly configured. Using `origin="hostname"` will retain the hostname of the URL entered into the `href` attribute. Using `target="_blank"`, the URL on the anchor tag will maintain the hostname of the URL in the `href` attribute. Using `origin="dynamic"` in combination with `target="_blank"` will allow the targeted link to match the hostname of the current page. + +
+ + +
+ + + See code + + + + + + ## Navigation pattern example Auro's hyperlink element can be used in many creative ways in combination with other elements for easy-to-manage UI solutions. This example uses the `auro-hyperlink` with the `auro-icon` elements with a little CSS to create a pretty popular nav style UI. diff --git a/src/auro-hyperlink.js b/src/auro-hyperlink.js index 95a670e..a12e5ba 100644 --- a/src/auro-hyperlink.js +++ b/src/auro-hyperlink.js @@ -36,6 +36,7 @@ import tokensCss from "./tokens-css.js"; * @attr {String} href - Defines the URL of the linked page. * @attr {String} target - Defines where to open the linked document. * @attr {String} type - Defines the type of hyperlink; accepts `nav` or `cta`. + * @attr {String} origin - Defines hostname for the origin of the URL. Options are `hostname` or `dynamic`. Default is `dynamic`. Using `hostname` will force the URL to be rewritten to use the hostname of the `href` attribute. Using `dynamic` will rewrite the URL to use the hostname of the current page. * @csspart link - Allows styling to be applied to the `a` element. * @csspart targetIcon - Allows styling to be applied to the icon that appears next to the hyperlink. */ diff --git a/src/component-base.mjs b/src/component-base.mjs index 1a1c8af..4e31413 100644 --- a/src/component-base.mjs +++ b/src/component-base.mjs @@ -62,6 +62,7 @@ export default class ComponentBase extends LitElement { static get properties() { return { href: { type: String }, + origin: { type: String }, rel: { type: String }, role: { type: String }, target: { type: String }, @@ -147,19 +148,67 @@ export default class ComponentBase extends LitElement { * @returns {string|undefined} The safe URL or `undefined`. */ safeUrl(href, relative) { + if (!href) { return undefined; } - const url = new URL(href, 'https://www.alaskaair.com'); + /** + * Determines the current hostname based on the environment (browser or server-side). + * + * @param {string} [href] - The URL to extract the hostname from when running in a server-side environment. + * @returns {string} The current hostname. + */ + let currentHostname; + if (typeof window !== 'undefined') { + currentHostname = window.location.origin; + } else { + // Extract the hostname from the provided href for SSR + try { + currentHostname = new URL(href).origin; + } catch (e) { + // If href is not a valid URL, provide a default fallback + currentHostname = 'http://localhost:8000'; + } + } + + /** + * Processes the given href and currentHostname to generate a URL object. + * + * @param {string} href - The href attribute of the anchor element. + * @param {string} currentHostname - The current hostname to be used as a base. + * @returns {URL|undefined} The generated URL object, or undefined if an error occurs. + * @private + */ + let url; + + // let's refactor this so that it's using a enum versus a boolean value + try { + url = new URL(href, currentHostname); + if (this.origin !== `hostname` || this.origin === `dynamic`) { + const currentUrl = new URL(currentHostname); + url.hostname = currentUrl.hostname; + url.port = currentUrl.port; // Preserve the port number + } + if (this.target === `_blank` && this.origin === undefined) { + const hrefURL = new URL(href); + url.hostname = hrefURL.hostname; + url.port = ''; // Remove the port number + } + } catch (e) { + return undefined; + } + + // return href if protocol is supported switch (url.protocol) { case 'tel:': case 'sms:': case 'mailto:': return href; - // Specifically want to render NO shadowDOM for the following refs + // Return undefined for unsupported protocols + // renders NO shadow DOM in the UI case 'javascript:': case 'data:': case 'vbscript:': @@ -215,22 +264,26 @@ export default class ComponentBase extends LitElement { * @returns {HTMLElement|undefined} The HTML element containing the icon, or undefined if no icon is generated. */ targetIcon(target) { - /** - * Checks if a URL's domain is from the 'alaskaair.com' domain or its subdomains. + * Checks if a given URL belongs to an internal domain. + * * @param {string} url - The URL to check. - * @returns {boolean} Returns true if the URL's domain is 'alaskaair.com' or one of its subdomains, otherwise false. + * @returns {boolean} - Returns true if the URL is an internal domain, otherwise false. */ - const isAlaskaAirDomain = (url) => { + const isInternalDomain = (url) => { const urlObject = new URL(url); - return urlObject.hostname.endsWith('.alaskaair.com'); + const hostname = urlObject.hostname; + const isInternal = hostname.includes('.alaskaair.com') || + hostname.includes('.hawaiianairlines.com') || + hostname.includes('localhost'); + return isInternal; }; - - // If target is '_blank' and the URL's domain is 'alaskaair.com' or one of its subdomains, return icon for new window - if (target === '_blank' && isAlaskaAirDomain(this.safeUri)) { + // If target is '_blank' and the URL's domain is internal or one of its subdomains, + // return icon for new window + if (target === '_blank' && isInternalDomain(this.safeUri)) { return this.generateIconHtml(newWindow.svg); - } else if (target === '_blank' && !isAlaskaAirDomain(this.safeUri) && this.includesDomain) { - // If target is '_blank' and the URL does not belong to 'alaskaair.com' or its subdomains but contains a domain, return icon for external link + } else if (target === '_blank' && !isInternalDomain(this.safeUri) && this.includesDomain) { + // If target is '_blank' and the URL is not internal, return icon for external link return this.generateIconHtml(externalLink.svg); } diff --git a/test/auro-hyperlink.test.js b/test/auro-hyperlink.test.js index 99fedbf..aae77e2 100644 --- a/test/auro-hyperlink.test.js +++ b/test/auro-hyperlink.test.js @@ -5,9 +5,10 @@ describe('auro-hyperlink', () => { it('auro-hyperlink is accessible', async () => { const el = await fixture(html` - Alaska air + Alaska air `); + // console.log(el.shadowRoot); await expect(el).to.be.accessible(); }); @@ -37,6 +38,42 @@ describe('auro-hyperlink', () => { expect(anchor).not.to.have.attribute('href', 'https://www.alaskaair.com/auro'); }); + it('inserted URL is updated to be dynamic based on environment', async () => { + const el = await fixture(html` + It's Auro! + `); + + const anchor = el.shadowRoot.querySelector('a'); + expect(anchor).to.have.attribute('href').that.includes('localhost'); + }); + + it('inserted URL is expecte to maintain initial URL hostname', async () => { + const el = await fixture(html` + It's Auro! + `); + + const anchor = el.shadowRoot.querySelector('a'); + expect(anchor).to.have.attribute('href').that.includes('alaskaair'); + }); + + it('shadow DOM href is persisted from original href with target=_blank', async () => { + const el = await fixture(html` + It's Auro! + `); + + const anchor = el.shadowRoot.querySelector('a'); + expect(anchor).to.have.attribute('href').that.includes('alaskaair'); + }); + + it('shadow DOM anchor is dynamic even with target=_blank applied', async () => { + const el = await fixture(html` + It's Auro! + `); + + const anchor = el.shadowRoot.querySelector('a'); + expect(anchor).to.have.attribute('href').that.includes('localhost'); + }); + // eval that JS in the href attr is ignored it('auro-hyperlink is javascript', async () => { const el = await fixture(html` @@ -148,11 +185,15 @@ describe('safeUrl function', () => { }); it('returns href when protocol is https:', async () => { - const result = component.safeUrl('https://www.example.com', false); + component.origin = 'hostname'; // Set the origin value + + const result = component.safeUrl('http://www.example.com', false); expect(result).to.equal('https://www.example.com/'); }); it('returns href with https protocol when relative is false', async () => { + component.origin = 'hostname'; // Set the origin value + const result = component.safeUrl('http://www.example.com', false); expect(result).to.equal('https://www.example.com/'); });