diff --git a/.changeset/label-link-variant.md b/.changeset/label-link-variant.md new file mode 100644 index 0000000000..4cd9dabadf --- /dev/null +++ b/.changeset/label-link-variant.md @@ -0,0 +1,7 @@ +--- +"@patternfly/elements": minor +--- + +`pf-label`: added link variant. Set the `href` attribute to render the +label text as a clickable link. On hover, the label border thickens +and changes color to indicate interactivity. diff --git a/elements/pf-label/demo/link.html b/elements/pf-label/demo/link.html new file mode 100644 index 0000000000..4eb4c4f070 --- /dev/null +++ b/elements/pf-label/demo/link.html @@ -0,0 +1,14 @@ +
+ Blue + Green + Orange + Red Hat + Purple + Cyan + Gold + Grey +
+ + diff --git a/elements/pf-label/pf-label.css b/elements/pf-label/pf-label.css index f5489dc070..000d70205a 100644 --- a/elements/pf-label/pf-label.css +++ b/elements/pf-label/pf-label.css @@ -2,6 +2,10 @@ position: relative; white-space: nowrap; border: 0; + + --_label-link-hover-border-color: + var(--pf-c-label__content--link--hover--before--BorderColor, + var(--pf-global--BorderColor--200, #8a8d90)); } pf-icon, ::slotted(pf-icon) { @@ -79,6 +83,16 @@ pf-icon, ::slotted(pf-icon) { --pf-global--icon--FontSize--sm: 12px; } +.outline { + /** outline label background color */ + --pf-c-label--BackgroundColor: var(--pf-c-label--m-outline--BackgroundColor, #ffffff); + --pf-c-label__content--before--BorderColor: var(--pf-global--palette--black-300, #d2d2d2); + + --_label-link-hover-border-color: + var(--pf-c-label--m-outline__content--link--hover--before--BorderColor, + var(--pf-global--BorderColor--100, #d2d2d2)); +} + .blue { /** blue label content text color */ --pf-c-label__content--Color: var(--pf-c-label--m-blue__content--Color, var(--pf-global--info-color--200, #002952)); @@ -86,11 +100,23 @@ pf-icon, ::slotted(pf-icon) { --pf-c-label--BackgroundColor: var(--pf-c-label--m-blue--BackgroundColor, var(--pf-global--palette--blue-50, #e7f1fa)); /** blue label content border color */ --pf-c-label__content--before--BorderColor: var(--pf-c-label--m-blue__content--before--BorderColor, var(--pf-global--palette--blue-100, #bee1f4)); + + --_label-link-hover-border-color: + var(--pf-c-label__content--link--hover--before--BorderColor, + var(--pf-c-label--m-blue__content--link--hover--before--BorderColor, + var(--pf-global--primary-color--100, #06c))); + + --_label-icon-color: var(--pf-c-label__icon--Color, var(--pf-c-label--m-blue__icon--Color, var(--pf-global--primary-color--100, #06c))); } .blue.outline { /** outline blue label content text color */ --pf-c-label__content--Color: var(--pf-c-label--m-outline__content--Color, var(--pf-c-label--m-outline--m-blue__content--Color, var(--pf-global--primary-color--100, #06c))); + + --_label-link-hover-border-color: + var(--pf-c-label--m-outline__content--link--hover--before--BorderColor, + var(--pf-c-label--m-outline--m-blue__content--link--hover--before--BorderColor, + var(--pf-global--BorderColor--100, #d2d2d2))); } .cyan { @@ -100,11 +126,23 @@ pf-icon, ::slotted(pf-icon) { --pf-c-label--BackgroundColor: var(--pf-c-label--m-cyan--BackgroundColor, var(--pf-global--palette--cyan-50, #f2f9f9)); /** cyan label content border color */ --pf-c-label__content--before--BorderColor: var(--pf-c-label--m-cyan__content--before--BorderColor, var(--pf-global--palette--cyan-100, #a2d9d9)); + + --_label-link-hover-border-color: + var(--pf-c-label__content--link--hover--before--BorderColor, + var(--pf-c-label--m-cyan__content--link--hover--before--BorderColor, + var(--pf-global--default-color--200, #009596))); + + --_label-icon-color: var(--pf-c-label__icon--Color, var(--pf-c-label--m-cyan__icon--Color, var(--pf-global--default-color--200, #009596))); } .cyan.outline { /** outline cyan label content text color */ - --pf-c-label__content--Color: var(--pf-c-label--m-outline__content--Color, var(--pf-c-label--m-outline--m-cyan__content--Color, var(--pf-global--palette--cyan-400, #005f60))) + --pf-c-label__content--Color: var(--pf-c-label--m-outline__content--Color, var(--pf-c-label--m-outline--m-cyan__content--Color, var(--pf-global--palette--cyan-400, #005f60))); + + --_label-link-hover-border-color: + var(--pf-c-label--m-outline__content--link--hover--before--BorderColor, + var(--pf-c-label--m-outline--m-cyan__content--link--hover--before--BorderColor, + var(--pf-global--BorderColor--100, #d2d2d2))); } .green { @@ -114,11 +152,23 @@ pf-icon, ::slotted(pf-icon) { --pf-c-label--BackgroundColor: var(--pf-c-label--m-green--BackgroundColor, var(--pf-global--palette--green-50, #f3faf2)); /** green label content border color */ --pf-c-label__content--before--BorderColor: var(--pf-c-label--m-green__content--before--BorderColor, var(--pf-global--palette--green-100, #bde5b8)); + + --_label-link-hover-border-color: + var(--pf-c-label__content--link--hover--before--BorderColor, + var(--pf-c-label--m-green__content--link--hover--before--BorderColor, + var(--pf-global--success-color--100, #3e8635))); + + --_label-icon-color: var(--pf-c-label__icon--Color, var(--pf-c-label--m-green__icon--Color, var(--pf-global--success-color--100, #3e8635))); } -.green.outline{ +.green.outline { /** outline green label content text color */ - --pf-c-label__content--Color: var(--pf-c-label--m-outline__content--Color, var(--pf-c-label--m-outline--m-green__content--Color, var(--pf-global--success-color--100, #3e8635))) + --pf-c-label__content--Color: var(--pf-c-label--m-outline__content--Color, var(--pf-c-label--m-outline--m-green__content--Color, var(--pf-global--success-color--100, #3e8635))); + + --_label-link-hover-border-color: + var(--pf-c-label--m-outline__content--link--hover--before--BorderColor, + var(--pf-c-label--m-outline--m-green__content--link--hover--before--BorderColor, + var(--pf-global--BorderColor--100, #d2d2d2))); } .orange { @@ -128,11 +178,23 @@ pf-icon, ::slotted(pf-icon) { --pf-c-label--BackgroundColor: var(--pf-c-label--m-orange--BackgroundColor, var(--pf-global--palette--orange-50, #fff6ec)); /** orange label content border color */ --pf-c-label__content--before--BorderColor: var(--pf-c-label--m-orange__content--before--BorderColor, var(--pf-global--palette--orange-100, #f4b678)); + + --_label-link-hover-border-color: + var(--pf-c-label__content--link--hover--before--BorderColor, + var(--pf-c-label--m-orange__content--link--hover--before--BorderColor, + var(--pf-global--palette--orange-300, #ec7a08))); + + --_label-icon-color: var(--pf-c-label__icon--Color, var(--pf-c-label--m-orange__icon--Color, var(--pf-global--palette--orange-300, #ec7a08))); } .orange.outline { /** outline orange label content text color */ - --pf-c-label__content--Color: var(--pf-c-label--m-outline__content--Color, var(--pf-c-label--m-outline--m-orange__content--Color, var(--pf-global--palette--orange-500, #8f4700))) + --pf-c-label__content--Color: var(--pf-c-label--m-outline__content--Color, var(--pf-c-label--m-outline--m-orange__content--Color, var(--pf-global--palette--orange-500, #8f4700))); + + --_label-link-hover-border-color: + var(--pf-c-label--m-outline__content--link--hover--before--BorderColor, + var(--pf-c-label--m-outline--m-orange__content--link--hover--before--BorderColor, + var(--pf-global--BorderColor--100, #d2d2d2))); } .purple { @@ -142,11 +204,23 @@ pf-icon, ::slotted(pf-icon) { --pf-c-label--BackgroundColor: var(--pf-c-label--m-purple--BackgroundColor, var(--pf-global--palette--purple-50, #f2f0fc)); /** purple label content border color */ --pf-c-label__content--before--BorderColor: var(--pf-c-label--m-purple__content--before--BorderColor, var(--pf-global--palette--purple-100, #cbc1ff)); + + --_label-link-hover-border-color: + var(--pf-c-label__content--link--hover--before--BorderColor, + var(--pf-c-label--m-purple__content--link--hover--before--BorderColor, + var(--pf-global--palette--purple-500, #6753ac))); + + --_label-icon-color: var(--pf-c-label__icon--Color, var(--pf-c-label--m-purple__icon--Color, var(--pf-global--palette--purple-500, #6753ac))); } .purple.outline { /** outline purple label content text color */ - --pf-c-label__content--Color: var(--pf-c-label--m-outline__content--Color, var(--pf-c-label--m-outline--m-purple__content--Color, var(--pf-global--palette--purple-500, #6753ac))) + --pf-c-label__content--Color: var(--pf-c-label--m-outline__content--Color, var(--pf-c-label--m-outline--m-purple__content--Color, var(--pf-global--palette--purple-500, #6753ac))); + + --_label-link-hover-border-color: + var(--pf-c-label--m-outline__content--link--hover--before--BorderColor, + var(--pf-c-label--m-outline--m-purple__content--link--hover--before--BorderColor, + var(--pf-global--BorderColor--100, #d2d2d2))); } .red { @@ -156,11 +230,23 @@ pf-icon, ::slotted(pf-icon) { --pf-c-label--BackgroundColor: var(--pf-c-label--m-red--BackgroundColor, var(--pf-global--palette--red-50, #faeae8)); /** red label content border color */ --pf-c-label__content--before--BorderColor: var(--pf-c-label--m-red__content--before--BorderColor, var(--pf-global--palette--red-100, #c9190b)); + + --_label-link-hover-border-color: + var(--pf-c-label__content--link--hover--before--BorderColor, + var(--pf-c-label--m-red__content--link--hover--before--BorderColor, + var(--pf-global--danger-color--100, #c9190b))); + + --_label-icon-color: var(--pf-c-label__icon--Color, var(--pf-c-label--m-red__icon--Color, var(--pf-global--danger-color--100, #c9190b))); } .red.outline { /** outline red label content text color */ - --pf-c-label__content--Color: var(--pf-c-label--m-outline__content--Color, var(--pf-c-label--m-outline--m-red__content--Color, var(--pf-global--danger-color--100, #c9190b))) + --pf-c-label__content--Color: var(--pf-c-label--m-outline__content--Color, var(--pf-c-label--m-outline--m-red__content--Color, var(--pf-global--danger-color--100, #c9190b))); + + --_label-link-hover-border-color: + var(--pf-c-label--m-outline__content--link--hover--before--BorderColor, + var(--pf-c-label--m-outline--m-red__content--link--hover--before--BorderColor, + var(--pf-global--BorderColor--100, #d2d2d2))); } .gold { @@ -170,57 +256,30 @@ pf-icon, ::slotted(pf-icon) { --pf-c-label--BackgroundColor: var(--pf-c-label--m-gold--BackgroundColor, var(--pf-global--palette--gold-50, #fdf7e7)); /** gold label content border color */ --pf-c-label__content--before--BorderColor: var(--pf-c-label--m-gold__content--before--BorderColor, var(--pf-global--palette--gold-100, #f9e0a2)); + + --_label-link-hover-border-color: + var(--pf-c-label__content--link--hover--before--BorderColor, + var(--pf-c-label--m-gold__content--link--hover--before--BorderColor, + var(--pf-global--palette--gold-300, #f4c145))); + + --_label-icon-color: var(--pf-c-label__icon--Color, var(--pf-c-label--m-gold__icon--Color, var(--pf-global--palette--gold-400, #f0ab00))); } .gold.outline { /** outline gold label content text color */ - --pf-c-label__content--Color: var(--pf-c-label--m-outline__content--Color, var(--pf-c-label--m-outline--m-gold__content--Color, var(--pf-global--palette--gold-600, #795600))) -} + --pf-c-label__content--Color: var(--pf-c-label--m-outline__content--Color, var(--pf-c-label--m-outline--m-gold__content--Color, var(--pf-global--palette--gold-600, #795600))); -.outline { - /** outline label background color */ - --pf-c-label--BackgroundColor: var(--pf-c-label--m-outline--BackgroundColor, #ffffff); - --pf-c-label__content--before--BorderColor: var(--pf-global--palette--black-300, #d2d2d2); + --_label-link-hover-border-color: + var(--pf-c-label--m-outline__content--link--hover--before--BorderColor, + var(--pf-c-label--m-outline--m-gold__content--link--hover--before--BorderColor, + var(--pf-global--BorderColor--100, #d2d2d2))); } .hasIcon [part=icon] { left: var(--pf-c-label--PaddingLeft, var(--pf-global--spacer--md, 1rem)); margin-inline-end: var(--pf-c-label__icon--MarginRight, var(--pf-global--spacer--xs, 0.25rem)); -} - -.blue .hasIcon [part=icon] { - /** blue label icon color */ - color: var(--pf-c-label__icon--Color, var(--pf-c-label--m-blue__icon--Color, var(--pf-global--primary-color--100, #06c))); -} - -.cyan .hasIcon [part=icon] { - /** cyan label icon color */ - color: var(--pf-c-label__icon--Color, var(--pf-c-label--m-cyan__icon--Color, var(--pf-global--default-color--200, #009596))); -} - -.green .hasIcon [part=icon] { - /** green label icon color */ - color: var(--pf-c-label__icon--Color, var(--pf-c-label--m-green__icon--Color, var(--pf-global--success-color--100, #3e8635))); -} - -.orange .hasIcon [part=icon] { - /** orange label icon color */ - color: var(--pf-c-label__icon--Color, var(--pf-c-label--m-orange__icon--Color, var(--pf-global--palette--orange-300, #ec7a08))); -} - -.purple .hasIcon [part=icon] { - /** purple label icon color */ - color: var(--pf-c-label__icon--Color, var(--pf-c-label--m-purple__icon--Color, var(--pf-global--palette--purple-500, #6753ac))); -} - -.red .hasIcon [part=icon] { - /** red label icon color */ - color: var(--pf-c-label__icon--Color, var(--pf-c-label--m-red__icon--Color, var(--pf-global--danger-color--100, #c9190b))); -} - -.gold .hasIcon [part=icon] { - /** gold label icon color */ - color: var(--pf-c-label__icon--Color, var(--pf-c-label--m-gold__icon--Color, var(--pf-global--palette--gold-400, #f0ab00))); + /** label icon color */ + color: var(--_label-icon-color); } pf-button { @@ -240,6 +299,25 @@ pf-button { margin-left: var(--pf-c-label__c-button--MarginLeft, 0.25rem); } +#link { + color: inherit; + text-decoration: none; + cursor: pointer; +} + +:host(:hover) #link ~ *, +:host(:focus-within) #link ~ *, +#link:hover, +#link:focus { + cursor: pointer; +} + +:host(:hover) #container:has(#link)::before, +:host(:focus-within) #container:has(#link)::before { + border-width: var(--pf-c-label__content--link--hover--before--BorderWidth, 2px); + border-color: var(--_label-link-hover-border-color); +} + svg { vertical-align:-0.125em; fill: currentColor; diff --git a/elements/pf-label/pf-label.ts b/elements/pf-label/pf-label.ts index eea234bd62..52462efa95 100644 --- a/elements/pf-label/pf-label.ts +++ b/elements/pf-label/pf-label.ts @@ -2,6 +2,7 @@ import { LitElement, html, isServer, type TemplateResult } from 'lit'; import { customElement } from 'lit/decorators/custom-element.js'; import { property } from 'lit/decorators/property.js'; import { classMap } from 'lit/directives/class-map.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; import { SlotController } from '@patternfly/pfe-core/controllers/slot-controller.js'; @@ -68,6 +69,9 @@ export class PfLabel extends LitElement { /** Text label for a removable label's close button */ @property({ attribute: 'close-button-label' }) closeButtonLabel?: string; + /** When set, the label becomes a link. The label text renders inside an anchor element. */ + @property({ reflect: true }) href?: string; + /** Represents the state of the anonymous and icon slots */ #slots = new SlotController(this, null, 'icon'); @@ -79,9 +83,10 @@ export class PfLabel extends LitElement { } override render(): TemplateResult<1> { - const { compact, truncated } = this; + const { compact, truncated, href } = this; const { variant, color, icon } = this; const hasIcon = !!icon || this.#slots.hasSlotted('icon'); + const isLink = !!href; return html` + ${isLink ? html` + + ` : html` + `} Default Compact `; +const exampleWithHref = html` + Link Label +`; + describe('', function() { before(function() { @@ -140,4 +144,29 @@ describe('', function() { expect(containerStyles.getPropertyValue('padding-bottom')).to.equal('0px'); expect(containerStyles.getPropertyValue('padding-left')).to.equal('8px'); // 0.5rem = 8px @ 16px browser default }); + + describe('with href attribute', function() { + let element: PfLabel; + beforeEach(async function() { + element = await createFixture(exampleWithHref); + await element.updateComplete; + }); + + it('should render an anchor element', function() { + const link = element.shadowRoot!.querySelector('#link'); + expect(link).to.be.an.instanceOf(HTMLAnchorElement); + }); + + it('should set the href on the anchor', function() { + const link = element.shadowRoot!.querySelector('#link') as HTMLAnchorElement; + expect(link.href).to.equal('https://example.com/'); + }); + + it('should not render an anchor when href is not set', async function() { + const el = await createFixture(example); + await el.updateComplete; + const link = el.shadowRoot!.querySelector('#link'); + expect(link).to.be.null; + }); + }); });