{"version":3,"file":"button.COPmNatg.js","sources":["../../../../../packages/web-components/src/lib/components/button/button.ts"],"sourcesContent":["import { PropertyValueMap, html, nothing } from 'lit';\nimport { ifDefined } from 'lit/directives/if-defined.js';\nimport { property, query, queryAssignedElements } from 'lit/decorators.js';\nimport 'element-internals-polyfill';\nimport { pdsCustomElement as customElement } from '../../decorators/pds-custom-element';\nimport { PdsElement } from '../PdsElement';\nimport styles from './button.scss?inline';\nimport '@principal/design-system-icons-web/loader';\nimport { getAriaDescribedByElementIds } from '../../../utils/get-aria-described-by-element-ids';\n\nexport type ButtonVariant =\n | 'default'\n | 'default-inverted'\n | 'primary'\n | 'primary-inverted'\n | 'icon'\n | 'icon-inverted'\n | 'loading';\n\nconst size = ['default', 'sm'] as const;\nexport type ButtonSize = (typeof size)[number];\n\n/**\n * @summary A styled button element\n *\n * @slot default Optional: The contents of the button, should only contain text\n * @slot icon-right Optional: Holds an icon to the right of the link, restricted to pds-icon\n * @slot icon-left Optional: Holds an icon to the left of the link, restricted to pds-icon\n *\n * @fires pds-button-click A custom event dispatched on click\n */\n@customElement('pds-button', {\n category: 'component',\n type: 'component',\n state: 'stable',\n styles,\n})\nexport class PdsButton extends PdsElement {\n /**\n * @internal\n */\n static formAssociated = true;\n\n /**\n * @internal\n */\n internals: Omit<\n ElementInternals,\n 'ariaBrailleLabel' | 'ariaBrailleRoleDescription'\n >;\n\n constructor() {\n super();\n this.internals = this.attachInternals();\n }\n\n /**\n * - **default** renders the button used for the most common calls to action that don't require as much visual attention.\n * - **default-inverted** renders a default button for use on darker backgrounds.\n * - **primary** renders the button used for the most important calls to action.\n * - **primary-inverted** renders a primary button for use on darker backgrounds.\n * - **icon** renders the button used for icon.\n * - **icon-inverted** renders the button for icons used on darker backgrounds.\n * - **loading** renders a disabled button with a spinning loading icon.\n */\n @property()\n variant: ButtonVariant = 'default';\n\n /**\n * Small button\n */\n @property()\n size: ButtonSize = 'default';\n\n /**\n * Disabled button\n */\n @property({ type: Boolean })\n disabled: boolean = false;\n\n /**\n * Disabled button description\n */\n @property()\n disabledContext: string;\n\n /**\n * FullWidth button\n */\n @property({ type: Boolean })\n fullWidth: boolean = false;\n\n /**\n * - **submit** specifies submit type button for form-data submit, default\n * - **reset** specifies reset type button for form-data reset\n * - **button** specifies clickable button type\n */\n @property({\n reflect: true,\n })\n type: 'submit' | 'reset' | 'button' = 'submit';\n\n // TODOv4: remove deprecated emphasis variants that are not a11y-compliant\n /**\n * Render the button as a link variant\n *\n * - **default** renders link for basic action\n * - **DEPRECATED**\n * - **emphasis** provide more affordance\n * - **emphasis-inverted** provide more affordance on a darker background\n * - **icon-left** link with icon left\n * - **icon-right** link with icon right\n */\n @property()\n link:\n | 'default'\n | 'inverted'\n | 'emphasis'\n | 'emphasis-inverted'\n | 'icon-left'\n | 'icon-right'\n | '' = '';\n\n /**\n * Remove padding from link button. Default is false.\n */\n @property({ type: Boolean })\n removeLinkPadding: boolean = false;\n\n /**\n * Screen reader label for button\n */\n @property({ type: String, reflect: true })\n ariaLabel: string;\n\n /**\n * A space-separated list of element IDs that the button controls\n */\n @property({ type: String })\n ariaControls: string;\n\n // TODOv4: Remove this property in favor of ariaDescribedbyIds\n /**\n * A space-separated list of element IDs that provide additional information to the user about the link\n * There needs to be an element with the same ID as the value of this property in the light DOM or else the aria-describedby attribute will not be set.\n * **DEPRECATED**\n */\n @property()\n ariaDescribedby: string;\n\n /**\n *\n * A space-separated list of element IDs that provide additional information to the user about the link\n * There needs to be an associated element with the same ID in the light DOM or else the aria-describedby attribute will not be set.\n */\n @property()\n ariaDescribedbyIds: string;\n\n /**\n * This is a faux focus effect to show the effect of being focused without\n * actually being focused.\n */\n @property({ type: Boolean })\n isActive: boolean = false;\n\n /**\n * @internal\n */\n @query('button')\n button: HTMLButtonElement;\n\n /**\n * This grabs the iconLeft slot\n * @internal\n */\n @queryAssignedElements({ slot: 'icon-left' })\n iconLeftList: HTMLElement[];\n\n /**\n * This grabs the iconRight slot\n * @internal\n */\n @queryAssignedElements({ slot: 'icon-right' })\n iconRightList: HTMLElement[];\n\n // We could re-render here to make sure slot changes from\n // icon-left to icon-right are handled on the fly, but that's\n // likely not going to be a need for users. If it ever does\n // become an issue, then we can address it here by re-rendering\n // with something like requestUpdate().\n handleSlotChange(e: Event) {\n this.handleSlotValidation(e);\n // TODOv4: If we are removing NSKv1 support, move verifyAria out of the setTimeout and delete the setTimeout function.\n // This isn't needed once we drop NSK v1 support. NSK v2 handles this properly.\n setTimeout(async () => {\n this.verifyAria();\n }, 100);\n this.addSizeToButtonIcon();\n }\n\n /**\n * This grabs the content from the default slot\n * @internal\n */\n @queryAssignedElements({ slot: undefined })\n defaultSlotElements: HTMLElement[];\n\n addSizeToButtonIcon() {\n const icons = [\n ...this.iconLeftList,\n ...this.iconRightList,\n ...this.defaultSlotElements,\n ];\n if (icons && icons.length) {\n icons.forEach((icon) => {\n if (icon.tagName.toLowerCase().includes('pds-icon')) {\n if (this.variant === 'icon' || this.variant === 'icon-inverted') {\n if (size.includes(this.size)) {\n icon.setAttribute('size', this.size === 'sm' ? 'default' : 'lg');\n } else {\n icon.setAttribute('size', 'lg');\n }\n } else if (size.includes(this.size)) {\n icon.setAttribute('size', this.size);\n } else {\n icon.setAttribute('size', 'default');\n }\n }\n });\n }\n }\n\n private handleClick(e: MouseEvent) {\n let eventMessage;\n\n if (this.textContent?.trim() === '') {\n eventMessage = this.ariaLabel;\n } else {\n eventMessage = this.textContent;\n }\n\n if (this.variant === 'loading' || this.disabled) {\n e.preventDefault();\n return;\n }\n\n const customEvent = new CustomEvent('pds-button-click', {\n bubbles: true,\n composed: true,\n cancelable: true,\n detail: {\n summary: eventMessage,\n },\n });\n\n this.dispatchEvent(customEvent);\n\n if (customEvent.defaultPrevented) {\n e.preventDefault();\n } else {\n this.submitOrResetAssociatedForm();\n }\n }\n\n private handleKeydown(e: KeyboardEvent) {\n if (this.variant === 'loading' || this.disabled) {\n e.preventDefault();\n return;\n }\n\n if (e.key === 'Enter' || e.key === ' ') {\n this.submitOrResetAssociatedForm();\n }\n }\n\n private submitOrResetAssociatedForm() {\n // Needed for submit and reset buttons\n if (this.type === 'submit') {\n // Older versions of Safari don't support the requestSubmit method\n if (this.internals.form?.requestSubmit) {\n this.internals.form.requestSubmit();\n } else {\n this.internals.form?.submit();\n }\n }\n\n if (this.type === 'reset') {\n this.internals.form?.reset();\n }\n }\n\n /**\n * @internal\n *\n * @returns boolean indicating whether or not button has valid screen readable text associated\n */\n verifyAria() {\n const hasLabel = !!this.ariaLabel || !!this.textContent?.trim();\n\n if (!hasLabel) {\n console.error(\n 'Button text is required as an ariaLabel property or as a slot in <%s /> but is undefined on: %o',\n this.tagName.toLowerCase(),\n this,\n );\n }\n\n return hasLabel;\n }\n\n /**\n * @internal\n */\n get classNames() {\n return {\n [this.variant]: !!this.variant,\n sm: this.size === 'sm',\n 'is-active': this.isActive,\n link: !!this.link,\n [`link-${this.link}`]: !!this.link,\n 'remove-link-padding': this.removeLinkPadding,\n fullWidth: this.fullWidth === true,\n };\n }\n\n protected override async firstUpdated() {\n super.firstUpdated();\n this.handleSlotValidation('icon-left');\n this.handleSlotValidation('icon-right');\n this.handleSlotValidation();\n await this.updateComplete;\n // TODOv4: If we are removing NSKv1 support, move verifyAria out of the setTimeout and delete the setTimeout function.\n // This isn't needed once we drop NSK v1 support. NSK v2 handles this properly.\n setTimeout(async () => {\n this.verifyAria();\n }, 1000);\n this.addSizeToButtonIcon();\n }\n\n async updated(\n changedProperties: PropertyValueMap | Map,\n ) {\n await this.updateComplete;\n this.addSizeToButtonIcon();\n\n if (changedProperties.has('disabled') && this.disabled) {\n this.isActive = false;\n }\n\n // TODOv4: Remove the logic for ariaDescribedby once we remove the property\n if (\n changedProperties.has('ariaDescribedbyIds') ||\n changedProperties.has('ariaDescribedby') ||\n changedProperties.has('disabledContext')\n ) {\n const ariaDescribedbyIds = getAriaDescribedByElementIds(this);\n this.button.setAttribute('aria-describedby', ariaDescribedbyIds);\n }\n }\n\n render() {\n // TODOv4: remove this deprecation warning\n if (this.link === 'emphasis' || this.link === 'emphasis-inverted') {\n const newVariant =\n this.link === 'emphasis-inverted' ? 'inverted' : 'default';\n console.warn(\n `The ${this.link} link button variant is deprecated and will be removed in the next major version of PDS. Please use the ${newVariant} variant instead.`,\n );\n }\n\n return html`${this.disabledContext &&\n html`