1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[CL-689][CL-799] Fix Icon button a11y errors (#15750)

* Throw error if appA11yTitle is null in icon button

* Add required label input

* Fix icon button errors in CL components and storeis

* fix popover aria-label errors

* remove commented code

* add labels to icon buttons in browser

* add labels to icon buttons in web

* add labels to icon buttons in license

* add labels to icon buttons in send

* add labels to icon buttons in angular

* fix missing pipe error

* fix sso icon button missed in error

* update labels in vault

* add section expand button label

* Adding labels to icon buttons

* Add lint rule to not allow icon buttons without label input

* rename util file

* trigger updates on title change

* update eslint rule name and folder

* add edit collection label to vault headers

* fix web header story label

* add show/hide summary labels

* update summary message

* fix breadcrumbs label message

* fix JSDoc to use correct input

* remove commented code

* use label as aria-label always. Remove init function

* add moreBreadcrumbs translation message to other apps

* add @bitwarden/team-ui-foundation as code owner for component eslint rules

* switch title to const variable

* add jsdoc comment on what the label input is used for

* [PM-22415] Tax ID notifications for Organizations and Providers (#15996)

* [NO LOGIC] Rename BillableEntity to BitwardenSubscriber

This helps us maintain paraody with server where we call this choice type ISubscriber. I chose BitwardenSubscriber to avoid overlap with RxJS

* [NO LOGIC] Move subscriber-billing.client to clients folder

* [NO LOGIC] Move organization warnings under organization folder

* Move getWarnings from OrganizationBillingApiService to new OrganizationBillingClient

I'd like us to move away from stashing so much in libs and utilizing the JsLibServicesModule when it's not necessary to do so. These are invocations used exclusively by the Web Vault and, until that changes, they should be treated as such

* Refactor OrganizationWarningsService

There was a case added to the Inactive Subscription warning for a free trial, but free trials do not represent inactive subscriptions so this was semantically incorrect. This creates another method that pulls the free trial warning and shows a dialog asking the user to subscribe if they're on one.

* Implement Tax ID Warnings throughout Admin Console and Provider Portal

* Fix linting error

* Jimmy's feedback

* remove duplicate messages keys

* revert changes to popover stories

* add back dupe myItems key for now as it was already here

* fix directive type errors

* remove variable left in error from merge conflict

* revert unintentional change to reports layout

* add back reports change

---------

Co-authored-by: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com>
This commit is contained in:
Bryan Cunningham
2025-08-19 15:15:41 -04:00
committed by GitHub
parent 321cd86a2c
commit 4449d8baf6
124 changed files with 414 additions and 285 deletions

1
.github/CODEOWNERS vendored
View File

@@ -131,6 +131,7 @@ apps/web/src/translation-constants.ts @bitwarden/team-platform-dev
.github/workflows/version-auto-bump.yml @bitwarden/team-platform-dev
# ESLint custom rules
libs/eslint @bitwarden/team-platform-dev
libs/eslint/components @bitwarden/team-ui-foundation
# Typescript tooling
tsconfig.base.json @bitwarden/team-platform-dev
nx.json @bitwarden/team-platform-dev

View File

@@ -5579,5 +5579,9 @@
},
"showLess": {
"message": "Show less"
},
"moreBreadcrumbs": {
"message": "More breadcrumbs",
"description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed."
}
}

View File

@@ -30,7 +30,7 @@
</bit-item-content>
<button
*ngIf="i < fieldsEditThreshold"
appA11yTitle="{{ 'remove' | i18n }}"
label="{{ 'remove' | i18n }}"
bitIconButton="bwi-minus-circle"
buttonType="danger"
size="small"

View File

@@ -26,7 +26,7 @@
</bit-item-content>
<button
*ngIf="i < fieldsEditThreshold"
appA11yTitle="{{ 'remove' | i18n }}"
label="{{ 'remove' | i18n }}"
bitIconButton="bwi-minus-circle"
buttonType="danger"
size="small"

View File

@@ -3,7 +3,7 @@
bitIconButton="bwi-popout"
size="small"
type="button"
appA11yTitle="{{ 'popOutNewWindow' | i18n }}"
label="{{ 'popOutNewWindow' | i18n }}"
[title]="'popOutNewWindow' | i18n"
(click)="expand()"
></button>

View File

@@ -19,8 +19,7 @@
bitIconButton="bwi-angle-left"
type="button"
*ngIf="showBackButton"
[title]="'back' | i18n"
[attr.aria-label]="'back' | i18n"
[label]="'back' | i18n"
[bitAction]="backAction"
></button>
<h1 *ngIf="pageTitle" bitTypography="h3" class="!tw-mb-0.5">

View File

@@ -67,14 +67,10 @@ class ExtensionPoppedContainerComponent {}
<button type="button" bitBadge variant="primary">Fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" aria-label="Copy item"></button>
<button type="button" bitIconButton="bwi-clone" label="Copy item"></button>
</bit-item-action>
<bit-item-action>
<button
type="button"
bitIconButton="bwi-ellipsis-v"
aria-label="More options"
></button>
<button type="button" bitIconButton="bwi-ellipsis-v" label="More options"></button>
</bit-item-action>
</ng-container>
</bit-item>
@@ -102,13 +98,7 @@ class MockAddButtonComponent {}
@Component({
selector: "mock-popout-button",
template: `
<button
bitIconButton="bwi-popout"
size="small"
type="button"
title="Pop out"
aria-label="Pop out"
></button>
<button bitIconButton="bwi-popout" size="small" type="button" label="Pop out"></button>
`,
imports: [IconButtonModule],
})
@@ -278,7 +268,13 @@ class MockSettingsPageComponent {}
<popup-footer slot="footer">
<button type="button" bitButton buttonType="primary">Save</button>
<button type="button" bitButton buttonType="secondary">Cancel</button>
<button slot="end" type="button" buttonType="danger" bitIconButton="bwi-trash"></button>
<button
slot="end"
type="button"
buttonType="danger"
bitIconButton="bwi-trash"
label="Delete"
></button>
</popup-footer>
</popup-page>
`,
@@ -671,17 +667,13 @@ export const WithVirtualScrollChild: Story = {
<button type="button" bitBadge variant="primary">Fill</button>
</bit-item-action>
<bit-item-action>
<button
type="button"
bitIconButton="bwi-clone"
aria-label="Copy item"
></button>
<button type="button" bitIconButton="bwi-clone" label="Copy item"></button>
</bit-item-action>
<bit-item-action>
<button
type="button"
bitIconButton="bwi-ellipsis-v"
aria-label="More options"
label="More options"
></button>
</bit-item-action>
</ng-container>

View File

@@ -26,7 +26,7 @@
slot="end"
bitIconButton="bwi-trash"
[bitAction]="deleteSend"
appA11yTitle="{{ 'delete' | i18n }}"
label="{{ 'delete' | i18n }}"
></button>
</popup-footer>
</popup-page>

View File

@@ -38,7 +38,7 @@
type="button"
buttonType="danger"
bitIconButton="bwi-trash"
[appA11yTitle]="'delete' | i18n"
[label]="'delete' | i18n"
></button>
</popup-footer>
</popup-page>

View File

@@ -7,7 +7,7 @@
size="small"
appCopyField="username"
[cipher]="cipher"
[appA11yTitle]="'copyUsername' | i18n"
[label]="'copyUsername' | i18n"
></button>
</bit-item-action>
<bit-item-action>
@@ -18,7 +18,7 @@
size="small"
appCopyField="password"
[cipher]="cipher"
[appA11yTitle]="'copyPassword' | i18n"
[label]="'copyPassword' | i18n"
></button>
</bit-item-action>
<bit-item-action>
@@ -28,7 +28,7 @@
size="small"
appCopyField="totp"
[cipher]="cipher"
[appA11yTitle]="'copyVerificationCode' | i18n"
[label]="'copyVerificationCode' | i18n"
></button>
</bit-item-action>
</ng-container>
@@ -40,7 +40,7 @@
type="button"
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="'copyFieldCipherName' | i18n: singleCopyableLogin.key : cipher.name"
[label]="'copyFieldCipherName' | i18n: singleCopyableLogin.key : cipher.name"
[appCopyField]="singleCopyableLogin.field"
[cipher]="cipher"
></button>
@@ -49,7 +49,7 @@
type="button"
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="
[label]="
hasLoginValues ? ('copyInfoTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n)
"
[disabled]="!hasLoginValues"
@@ -86,7 +86,7 @@
size="small"
appCopyField="cardNumber"
[cipher]="cipher"
[appA11yTitle]="'copyNumber' | i18n"
[label]="'copyNumber' | i18n"
></button>
</bit-item-action>
<bit-item-action>
@@ -96,7 +96,7 @@
size="small"
appCopyField="securityCode"
[cipher]="cipher"
[appA11yTitle]="'copySecurityCode' | i18n"
[label]="'copySecurityCode' | i18n"
></button>
</bit-item-action>
</ng-container>
@@ -107,7 +107,7 @@
type="button"
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="'copyFieldCipherName' | i18n: singleCopyableCard.key : cipher.name"
[label]="'copyFieldCipherName' | i18n: singleCopyableCard.key : cipher.name"
[appCopyField]="singleCopyableCard.field"
[cipher]="cipher"
showToast
@@ -117,7 +117,7 @@
type="button"
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="
[label]="
hasCardValues ? ('copyInfoTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n)
"
[disabled]="!hasCardValues"
@@ -142,7 +142,7 @@
type="button"
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="'copyFieldCipherName' | i18n: singleCopyableIdentity.key : cipher.name"
[label]="'copyFieldCipherName' | i18n: singleCopyableIdentity.key : cipher.name"
[appCopyField]="singleCopyableIdentity.field"
[cipher]="cipher"
showToast
@@ -152,7 +152,7 @@
type="button"
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="
[label]="
hasIdentityValues ? ('copyInfoTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n)
"
[disabled]="!hasIdentityValues"
@@ -180,9 +180,7 @@
type="button"
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="
hasSecureNoteValue ? ('copyNoteTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n)
"
[label]="hasSecureNoteValue ? ('copyNoteTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n)"
appCopyField="secureNote"
[cipher]="cipher"
></button>
@@ -193,9 +191,7 @@
type="button"
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="
hasSshKeyValues ? ('copyInfoTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n)
"
[label]="hasSshKeyValues ? ('copyInfoTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n)"
[disabled]="!hasSshKeyValues"
[bitMenuTriggerFor]="sshKeyOptions"
></button>

View File

@@ -3,8 +3,7 @@
type="button"
bitIconButton="bwi-ellipsis-v"
size="small"
[attr.aria-label]="'moreOptionsLabel' | i18n: cipher.name"
[title]="'moreOptionsTitle' | i18n: cipher.name"
[label]="'moreOptionsLabel' | i18n: cipher.name"
[disabled]="decryptionFailure"
[bitMenuTriggerFor]="moreOptions"
></button>

View File

@@ -8,7 +8,7 @@
bitIconButton="bwi-sliders"
[buttonType]="'muted'"
[bitDisclosureTriggerFor]="disclosureRef"
[appA11yTitle]="'filterVault' | i18n"
[label]="'filterVault' | i18n"
aria-describedby="filters-applied"
></button>
<p

View File

@@ -42,7 +42,7 @@
type="button"
size="small"
(click)="onRefresh.emit()"
[appA11yTitle]="'refresh' | i18n"
[label]="'refresh' | i18n"
></button>
<span bitTypography="body2" slot="end">
<span
@@ -141,8 +141,7 @@
bitIconButton="bwi-external-link"
size="small"
(click)="launchCipher(cipher)"
[attr.aria-label]="'launchWebsiteName' | i18n: cipher.name"
[title]="'launchWebsiteName' | i18n: cipher.name"
[label]="'launchWebsiteName' | i18n: cipher.name"
></button>
</bit-item-action>
<app-item-copy-actions [cipher]="cipher"></app-item-copy-actions>

View File

@@ -33,7 +33,7 @@
type="button"
buttonType="danger"
bitIconButton="bwi-trash"
[appA11yTitle]="(cipher.isDeleted ? 'deleteForever' : 'delete') | i18n"
[label]="(cipher.isDeleted ? 'deleteForever' : 'delete') | i18n"
></button>
</popup-footer>
</popup-page>

View File

@@ -25,7 +25,7 @@
slot="end"
type="button"
(click)="openAddEditFolderDialog(folder)"
[appA11yTitle]="'editFolderWithName' | i18n: folder.name"
[label]="'editFolderWithName' | i18n: folder.name"
bitIconButton="bwi-pencil-square"
class="tw-self-end"
data-testid="edit-folder-button"

View File

@@ -37,8 +37,7 @@
type="button"
bitIconButton="bwi-ellipsis-v"
size="small"
[attr.aria-label]="'moreOptionsLabel' | i18n: cipher.name"
[title]="'moreOptionsTitle' | i18n: cipher.name"
[label]="'moreOptionsLabel' | i18n: cipher.name"
[bitMenuTriggerFor]="moreOptions"
></button>
<bit-menu #moreOptions>

View File

@@ -4077,5 +4077,9 @@
},
"enableAutotypeDescription": {
"message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut."
},
"moreBreadcrumbs": {
"message": "More breadcrumbs",
"description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed."
}
}

View File

@@ -34,6 +34,7 @@
[bitMenuTriggerFor]="editCollectionMenu"
size="small"
type="button"
[label]="'editCollection' | i18n"
></button>
<bit-menu #editCollectionMenu>
<ng-container *ngIf="canEditCollection">

View File

@@ -80,7 +80,7 @@
bitIconButton="bwi-trash"
bitFormButton
[bitAction]="delete"
[appA11yTitle]="'delete' | i18n"
[label]="'delete' | i18n"
></button>
</ng-container>
</bit-dialog>

View File

@@ -46,7 +46,7 @@
type="button"
bitIconButton="bwi-ellipsis-v"
size="small"
appA11yTitle="{{ 'options' | i18n }}"
label="{{ 'options' | i18n }}"
></button>
<bit-menu #headerMenu>
@@ -82,7 +82,7 @@
type="button"
bitIconButton="bwi-ellipsis-v"
size="small"
appA11yTitle="{{ 'options' | i18n }}"
label="{{ 'options' | i18n }}"
></button>
<bit-menu #rowMenu>

View File

@@ -106,7 +106,7 @@
type="button"
bitIconButton="bwi-ellipsis-v"
size="small"
appA11yTitle="{{ 'options' | i18n }}"
label="{{ 'options' | i18n }}"
*ngIf="showUserManagementControls$ | async"
></button>
@@ -350,7 +350,7 @@
type="button"
bitIconButton="bwi-ellipsis-v"
size="small"
appA11yTitle="{{ 'options' | i18n }}"
label="{{ 'options' | i18n }}"
></button>
<bit-menu #rowMenu>

View File

@@ -51,7 +51,7 @@ const render: Story["render"] = (args) => ({
buttonType="danger"
size="default"
title="Delete"
aria-label="Delete"></button>
label="Delete"></button>
</ng-container>
</bit-dialog>
`,

View File

@@ -122,7 +122,7 @@
type="button"
bitIconButton="bwi-close"
buttonType="muted"
appA11yTitle="{{ 'remove' | i18n }} {{ item.labelName }}"
label="{{ 'remove' | i18n }} {{ item.labelName }}"
[disabled]="disabled"
(click)="selectionList.deselectItem(item.id); handleBlur()"
></button>

View File

@@ -143,7 +143,7 @@
buttonType="danger"
class="tw-ml-auto"
bitFormButton
[appA11yTitle]="'delete' | i18n"
[label]="'delete' | i18n"
[bitAction]="delete"
[disabled]="loading"
></button>

View File

@@ -62,7 +62,7 @@
buttonType="danger"
[bitAction]="delete"
*ngIf="editMode"
appA11yTitle="{{ 'delete' | i18n }}"
label="{{ 'delete' | i18n }}"
></button>
</ng-container>
</bit-dialog>

View File

@@ -89,7 +89,7 @@
<button
[bitMenuTriggerFor]="trustedContactOptions"
type="button"
appA11yTitle="{{ 'options' | i18n }}"
label="{{ 'options' | i18n }}"
bitIconButton="bwi-ellipsis-v"
buttonType="main"
></button>
@@ -212,7 +212,7 @@
<button
[bitMenuTriggerFor]="grantedContactOptions"
type="button"
appA11yTitle="{{ 'options' | i18n }}"
label="{{ 'options' | i18n }}"
bitIconButton="bwi-ellipsis-v"
buttonType="main"
></button>

View File

@@ -39,7 +39,7 @@
type="button"
buttonType="danger"
(click)="remove(i)"
appA11yTitle="{{ 'remove' | i18n }}"
label="{{ 'remove' | i18n }}"
></button>
</div>
</div>

View File

@@ -53,7 +53,7 @@
bitIconButton="bwi-ellipsis-v"
buttonType="main"
[bitMenuTriggerFor]="appListDropdown"
appA11yTitle="{{ 'options' | i18n }}"
label="{{ 'options' | i18n }}"
></button>
<bit-menu #appListDropdown>
@if (!isSelfHosted && !sponsoredFamily.validUntil) {

View File

@@ -33,7 +33,7 @@
showToast
[valueLabel]="'billingSyncKey' | i18n"
[appCopyClick]="clientSecret"
[appA11yTitle]="'copyValue' | i18n"
[label]="'copyValue' | i18n"
></button>
</bit-form-field>
<div class="tw-mt-2 tw-text-sm tw-text-muted" *ngIf="showLastSyncText">

View File

@@ -33,7 +33,7 @@
bitIconButton="bwi-trash"
bitFormButton
[bitAction]="deleteConnection"
appA11yTitle="{{ 'delete' | i18n }}"
label="{{ 'delete' | i18n }}"
></button>
</ng-container>
</bit-dialog>

View File

@@ -359,7 +359,7 @@
type="button"
[bitIconButton]="totalOpened ? 'bwi-angle-down' : 'bwi-angle-up'"
size="small"
aria-hidden="true"
[label]="totalOpened ? ('hidePricingSummary' | i18n) : ('showPricingSummary' | i18n)"
></button>
</p>
</div>

View File

@@ -8,7 +8,7 @@
type="button"
size="small"
class="tw-float-right"
appA11yTitle="{{ 'cancel' | i18n }}"
label="{{ 'cancel' | i18n }}"
(click)="cancel()"
></button>
<h2 bitTypography="h2">{{ "changeBillingPlan" | i18n }}</h2>

View File

@@ -12,7 +12,7 @@
bitIconButton="bwi-ellipsis-v"
buttonType="main"
[bitMenuTriggerFor]="appListDropdown"
appA11yTitle="{{ 'options' | i18n }}"
label="{{ 'options' | i18n }}"
></button>
<bit-menu #appListDropdown>
<button

View File

@@ -11,7 +11,9 @@
type="button"
[bitIconButton]="summaryData.totalOpened ? 'bwi-angle-down' : 'bwi-angle-up'"
size="small"
aria-hidden="true"
[label]="
summaryData.totalOpened ? ('hidePricingSummary' | i18n) : ('showPricingSummary' | i18n)
"
></button>
</p>
</div>

View File

@@ -2,7 +2,7 @@
<div class="tw-flex tw-flex-wrap tw-gap-4 tw-mt-4">
<div class="tw-w-full">
<a bitButton bitIconButton="bwi-angle-left" routerLink="./" *ngIf="!homepage">
<a bitButton routerLink="./" *ngIf="!homepage">
{{ "backToReports" | i18n }}
</a>
</div>

View File

@@ -48,7 +48,7 @@ class MockStateService {
@Component({
selector: "product-switcher",
template: `<button type="button" bitIconButton="bwi-filter"></button>`,
template: `<button type="button" bitIconButton="bwi-filter" label="Switch products"></button>`,
standalone: false,
})
class MockProductSwitcher {}

View File

@@ -3,7 +3,7 @@
bitIconButton="bwi bwi-fw bwi-filter"
[bitMenuTriggerFor]="content?.menu"
[buttonType]="buttonType"
[attr.aria-label]="'switchProducts' | i18n"
[label]="'switchProducts' | i18n"
*ngIf="products$ | async"
></button>
<product-switcher-content #content></product-switcher-content>

View File

@@ -32,7 +32,7 @@
type="button"
buttonType="danger"
(click)="remove(i)"
appA11yTitle="{{ 'remove' | i18n }}"
label="{{ 'remove' | i18n }}"
></button>
</div>
<button bitButton type="button" (click)="add()" buttonType="secondary" class="tw-mb-2">
@@ -62,6 +62,7 @@
bitIconButton="bwi-ellipsis-v"
[bitMenuTriggerFor]="appListDropdown"
class="tw-border-0 tw-bg-transparent tw-p-0"
[label]="'editDomain' | i18n"
></button>
<bit-menu #appListDropdown>
<a href="#" bitMenuItem appStopClick (click)="toggleExcluded(d)" *ngIf="!d.excluded">

View File

@@ -156,7 +156,7 @@
type="button"
[bitMenuTriggerFor]="sendOptions"
bitIconButton="bwi-ellipsis-v"
appA11yTitle="{{ 'options' | i18n }}"
label="{{ 'options' | i18n }}"
></button>
<bit-menu #sendOptions>
<button type="button" bitMenuItem (click)="copy(s)">

View File

@@ -71,7 +71,7 @@
bitIconButton="bwi-trash"
type="button"
buttonType="danger"
[appA11yTitle]="'delete' | i18n"
[label]="'delete' | i18n"
[bitAction]="delete"
[disabled]="!canDelete"
data-testid="delete-cipher-btn"

View File

@@ -82,7 +82,7 @@
size="small"
bitIconButton="bwi-ellipsis-v"
type="button"
appA11yTitle="{{ 'options' | i18n }}"
label="{{ 'options' | i18n }}"
appStopProp
></button>
<bit-menu #corruptedCipherOptions>
@@ -101,7 +101,7 @@
bitIconButton="bwi-ellipsis-v"
type="button"
appStopProp
appA11yTitle="{{ 'options' | i18n }}"
label="{{ 'options' | i18n }}"
></button>
<bit-menu #cipherOptions>
<ng-container *ngIf="isNotDeletedLoginCipher">

View File

@@ -65,7 +65,7 @@
size="small"
bitIconButton="bwi-ellipsis-v"
type="button"
appA11yTitle="{{ 'options' | i18n }}"
label="{{ 'options' | i18n }}"
appStopProp
></button>
}

View File

@@ -56,7 +56,7 @@
bitIconButton="bwi-ellipsis-v"
size="small"
type="button"
appA11yTitle="{{ 'options' | i18n }}"
label="{{ 'options' | i18n }}"
></button>
<bit-menu #headerMenu>
<button *ngIf="bulkMoveAllowed" type="button" bitMenuItem (click)="bulkMoveToFolder()">

View File

@@ -29,7 +29,7 @@
[bitMenuTriggerFor]="editCollectionMenu"
size="small"
type="button"
aria-haspopup="true"
[label]="'editCollection' | i18n"
></button>
<bit-menu #editCollectionMenu>
<button

View File

@@ -2825,6 +2825,12 @@
}
}
},
"showPricingSummary": {
"message": "Show pricing summary"
},
"hidePricingSummary": {
"message": "Hide pricing summary"
},
"summary": {
"message": "Summary"
},
@@ -11023,6 +11029,10 @@
"missingTaxIdWarning": {
"message": "Action required: You're missing a Tax ID number in payment details. If a Tax ID is not added, your invoices may include additional tax."
},
"moreBreadcrumbs": {
"message": "More breadcrumbs",
"description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed."
},
"addTaxId": {
"message": "Add a Tax ID"
},

View File

@@ -25,7 +25,7 @@
bitIconButton="bwi-ellipsis-v"
size="small"
type="button"
appA11yTitle="{{ 'options' | i18n }}"
label="{{ 'options' | i18n }}"
></button>
<bit-menu #headerMenu>
<button
@@ -71,7 +71,7 @@
bitIconButton="bwi-ellipsis-v"
size="small"
type="button"
appA11yTitle="{{ 'options' | i18n }}"
label="{{ 'options' | i18n }}"
></button>
<bit-menu #rowMenu>
<button

View File

@@ -36,7 +36,7 @@
type="button"
bitSuffix
bitIconButton="bwi-clone"
appA11yTitle="{{ 'copyDnsTxtRecord' | i18n }}"
label="{{ 'copyDnsTxtRecord' | i18n }}"
(click)="copyDnsTxt()"
></button>
</bit-form-field>
@@ -67,8 +67,7 @@
bitIconButton="bwi-trash"
buttonType="danger"
size="default"
title="{{ 'delete' | i18n }}"
aria-label="Delete"
label="{{ 'delete' | i18n }}"
[bitAction]="deleteDomain"
type="submit"
bitFormButton

View File

@@ -29,7 +29,7 @@
bitSuffix
bitIconButton="bwi-clone"
[bitAction]="copyScimUrl"
[appA11yTitle]="'copyScimUrl' | i18n"
[label]="'copyScimUrl' | i18n"
></button>
</bit-form-field>
@@ -46,7 +46,7 @@
bitSuffix
[bitIconButton]="showScimKey ? 'bwi-eye-slash' : 'bwi-eye'"
[bitAction]="toggleScimKey"
[appA11yTitle]="'toggleVisibility' | i18n"
[label]="'toggleVisibility' | i18n"
></button>
<button
type="button"
@@ -54,14 +54,14 @@
bitIconButton="bwi-generate"
[bitAction]="rotateScimKey"
bitFormButton
[appA11yTitle]="'rotateScimKey' | i18n"
[label]="'rotateScimKey' | i18n"
></button>
<button
type="button"
bitSuffix
bitIconButton="bwi-clone"
[bitAction]="copyScimKey"
[appA11yTitle]="'copyScimKey' | i18n"
[label]="'copyScimKey' | i18n"
></button>
<bit-hint>{{ "scimApiKeyHelperText" | i18n }}</bit-hint>
</bit-form-field>

View File

@@ -58,7 +58,7 @@
bitIconButton="bwi-trash"
buttonType="danger"
bitFormButton
[appA11yTitle]="'delete' | i18n"
[label]="'delete' | i18n"
[bitAction]="delete"
[disabled]="loading"
></button>

View File

@@ -79,7 +79,7 @@
type="button"
bitIconButton="bwi-ellipsis-v"
size="small"
appA11yTitle="{{ 'options' | i18n }}"
label="{{ 'options' | i18n }}"
></button>
<bit-menu #headerMenu>
<button
@@ -177,7 +177,7 @@
type="button"
bitIconButton="bwi-ellipsis-v"
size="small"
appA11yTitle="{{ 'options' | i18n }}"
label="{{ 'options' | i18n }}"
></button>
<bit-menu #rowMenu>
<button

View File

@@ -181,7 +181,7 @@
bitSuffix
type="button"
[appCopyClick]="callbackPath"
[appA11yTitle]="'copyValue' | i18n"
[label]="'copyValue' | i18n"
></button>
</bit-form-field>
@@ -193,7 +193,7 @@
bitSuffix
type="button"
[appCopyClick]="signedOutCallbackPath"
[appA11yTitle]="'copyValue' | i18n"
[label]="'copyValue' | i18n"
></button>
</bit-form-field>
@@ -336,7 +336,7 @@
bitSuffix
type="button"
[appCopyClick]="spEntityId"
[appA11yTitle]="'copyValue' | i18n"
[label]="'copyValue' | i18n"
></button>
</bit-form-field>
@@ -348,7 +348,7 @@
bitSuffix
type="button"
[appCopyClick]="spEntityIdStatic"
[appA11yTitle]="'copyValue' | i18n"
[label]="'copyValue' | i18n"
></button>
</bit-form-field>
@@ -360,14 +360,14 @@
bitSuffix
type="button"
[appLaunchClick]="spMetadataUrl"
[appA11yTitle]="'launch' | i18n"
[label]="'launch' | i18n"
></button>
<button
bitIconButton="bwi-clone"
bitSuffix
type="button"
[appCopyClick]="spMetadataUrl"
[appA11yTitle]="'copyValue' | i18n"
[label]="'copyValue' | i18n"
></button>
</bit-form-field>
@@ -379,7 +379,7 @@
bitSuffix
type="button"
[appCopyClick]="spAcsUrl"
[appA11yTitle]="'copyValue' | i18n"
[label]="'copyValue' | i18n"
></button>
</bit-form-field>

View File

@@ -86,7 +86,7 @@
type="button"
bitIconButton="bwi-ellipsis-v"
size="small"
appA11yTitle="{{ 'options' | i18n }}"
label="{{ 'options' | i18n }}"
></button>
<bit-menu #rowMenu>
<button

View File

@@ -84,7 +84,7 @@
type="button"
bitIconButton="bwi-ellipsis-v"
size="small"
appA11yTitle="{{ 'options' | i18n }}"
label="{{ 'options' | i18n }}"
></button>
<bit-menu #rowMenu>

View File

@@ -7,6 +7,7 @@
(click)="toggle()"
[attr.aria-expanded]="open"
[attr.aria-controls]="contentId"
[label]="'toggleVisibility' | i18n"
></button>
</header>
<div *ngIf="open" [attr.id]="contentId" class="tw-mt-4">

View File

@@ -95,7 +95,7 @@
buttonType="danger"
bitIconButton="bwi-trash"
bitFormButton
[appA11yTitle]="'delete' | i18n"
[label]="'delete' | i18n"
[bitAction]="delete"
></button>
</ng-container>

View File

@@ -40,8 +40,7 @@
bitIconButton="bwi-ellipsis-v"
buttonType="main"
[bitMenuTriggerFor]="tableMenu"
[title]="'options' | i18n"
[attr.aria-label]="'options' | i18n"
[label]="'options' | i18n"
></button>
</th>
</tr>
@@ -65,8 +64,7 @@
type="button"
bitIconButton="bwi-ellipsis-v"
buttonType="main"
[title]="'options' | i18n"
[attr.aria-label]="'options' | i18n"
[label]="'options' | i18n"
[bitMenuTriggerFor]="tokenMenu"
></button>
</td>

View File

@@ -11,12 +11,19 @@
type="button"
bitIconButton="bwi-clone"
[bitAction]="copyIdentityUrl"
[label]="'copyCustomField' | i18n: identityUrl"
></button>
</bit-form-field>
<bit-form-field class="tw-w-2/5 tw-min-w-80">
<bit-label>{{ "apiUrl" | i18n }}</bit-label>
<input bitInput type="text" [(ngModel)]="apiUrl" [disabled]="true" />
<button bitSuffix type="button" bitIconButton="bwi-clone" [bitAction]="copyApiUrl"></button>
<button
bitSuffix
type="button"
bitIconButton="bwi-clone"
[bitAction]="copyApiUrl"
[label]="'copyCustomField' | i18n: apiUrl"
></button>
</bit-form-field>
</div>
<bit-form-field class="tw-w-2/5 tw-min-w-80">
@@ -27,6 +34,7 @@
type="button"
bitIconButton="bwi-clone"
[bitAction]="copyOrganizationId"
[label]="'copyCustomField' | i18n: organizationId"
></button>
</bit-form-field>
</div>

View File

@@ -39,8 +39,7 @@
type="button"
bitIconButton="bwi-ellipsis-v"
buttonType="main"
[title]="'options' | i18n"
[attr.aria-label]="'options' | i18n"
[label]="'options' | i18n"
[bitMenuTriggerFor]="tableMenu"
></button>
</th>
@@ -72,8 +71,7 @@
type="button"
bitIconButton="bwi-ellipsis-v"
buttonType="main"
[title]="'options' | i18n"
[attr.aria-label]="'options' | i18n"
[label]="'options' | i18n"
[bitMenuTriggerFor]="serviceAccountMenu"
></button>
</td>

View File

@@ -67,8 +67,7 @@
buttonType="main"
size="default"
[disabled]="disabled"
[attr.title]="'remove' | i18n"
[attr.aria-label]="'remove' | i18n"
[label]="'remove' | i18n"
(click)="selectionList.deselectItem(item.id); handleBlur()"
></button>
</td>

View File

@@ -43,8 +43,7 @@
bitIconButton="bwi-ellipsis-v"
buttonType="main"
[bitMenuTriggerFor]="tableMenu"
[title]="'options' | i18n"
[attr.aria-label]="'options' | i18n"
[label]="'options' | i18n"
*ngIf="showMenus"
></button>
</th>
@@ -77,8 +76,7 @@
bitIconButton="bwi-clone"
buttonType="main"
size="small"
[title]="'copyUuid' | i18n"
[attr.aria-label]="'copyUuid' | i18n"
[label]="'copyUuid' | i18n"
(click)="copyProjectUuidToClipboard(project.id)"
></button>
</div>
@@ -94,8 +92,7 @@
bitIconButton="bwi-ellipsis-v"
buttonType="main"
[bitMenuTriggerFor]="projectMenu"
[title]="'options' | i18n"
[attr.aria-label]="'options' | i18n"
[label]="'options' | i18n"
*ngIf="showMenus"
></button>
</td>

View File

@@ -45,8 +45,7 @@
type="button"
bitIconButton="bwi-ellipsis-v"
buttonType="main"
[title]="'options' | i18n"
[attr.aria-label]="'options' | i18n"
[label]="'options' | i18n"
[bitMenuTriggerFor]="tableMenu"
></button>
</th>
@@ -78,8 +77,7 @@
bitIconButton="bwi-clone"
buttonType="main"
size="small"
[title]="'copyUuid' | i18n"
[attr.aria-label]="'copyUuid' | i18n"
[label]="'copyUuid' | i18n"
(click)="copySecretUuidEvent.emit(secret.id)"
></button>
</div>
@@ -108,8 +106,7 @@
type="button"
bitIconButton="bwi-ellipsis-v"
buttonType="main"
[title]="'options' | i18n"
[attr.aria-label]="'options' | i18n"
[label]="'options' | i18n"
[bitMenuTriggerFor]="secretMenu"
></button>
</td>

View File

@@ -12,6 +12,7 @@ import angularRxjs from "eslint-plugin-rxjs-angular";
import storybook from "eslint-plugin-storybook";
import platformPlugins from "./libs/eslint/platform/index.mjs";
import componentPlugins from "./libs/eslint/components/index.mjs";
export default tseslint.config(
...storybook.configs["flat/recommended"],
@@ -174,6 +175,7 @@ export default tseslint.config(
plugins: {
"@angular-eslint/template": angular.templatePlugin,
tailwindcss: eslintPluginTailwindCSS,
"@bitwarden/components": componentPlugins,
},
rules: {
"@angular-eslint/template/button-has-type": "error",
@@ -188,6 +190,10 @@ export default tseslint.config(
"tailwindcss/enforces-negative-arbitrary-values": "error",
"tailwindcss/enforces-shorthand": "error",
"tailwindcss/no-contradicting-classname": "error",
"@bitwarden/components/require-label-on-biticonbutton": [
"error",
{ ignoreIfHas: ["bitPasswordInputToggle"] },
],
},
},

View File

@@ -18,9 +18,8 @@
size="small"
*ngIf="!persistent"
(click)="handleDismiss()"
[attr.title]="'close' | i18n"
[attr.aria-label]="'close' | i18n"
class="-tw-me-2"
[label]="'close' | i18n"
></button>
</div>

View File

@@ -45,7 +45,7 @@
type="button"
bitIconButton="bwi-generate"
bitSuffix
[appA11yTitle]="'generatePassword' | i18n"
[label]="'generatePassword' | i18n"
(click)="generatePassword()"
></button>
<button
@@ -53,7 +53,7 @@
type="button"
bitSuffix
bitIconButton="bwi-clone"
appA11yTitle="{{ 'copyPassword' | i18n }}"
label="{{ 'copyPassword' | i18n }}"
(click)="copy()"
></button>
<button

View File

@@ -1,4 +1,6 @@
import { Directive, effect, ElementRef, input, Renderer2 } from "@angular/core";
import { Directive, effect, ElementRef, input } from "@angular/core";
import { setA11yTitleAndAriaLabel } from "./set-a11y-title-and-aria-label";
@Directive({
selector: "[appA11yTitle]",
@@ -6,19 +8,16 @@ import { Directive, effect, ElementRef, input, Renderer2 } from "@angular/core";
export class A11yTitleDirective {
title = input.required<string>({ alias: "appA11yTitle" });
constructor(
private el: ElementRef,
private renderer: Renderer2,
) {
constructor(private el: ElementRef) {
const originalTitle = this.el.nativeElement.getAttribute("title");
const originalAriaLabel = this.el.nativeElement.getAttribute("aria-label");
effect(() => {
if (originalTitle === null) {
this.renderer.setAttribute(this.el.nativeElement, "title", this.title());
}
if (originalAriaLabel === null) {
this.renderer.setAttribute(this.el.nativeElement, "aria-label", this.title());
}
setA11yTitleAndAriaLabel({
element: this.el.nativeElement,
title: originalTitle ?? this.title(),
label: originalAriaLabel ?? this.title(),
});
});
}
}

View File

@@ -0,0 +1,16 @@
export function setA11yTitleAndAriaLabel({
element,
title,
label,
}: {
element: HTMLElement;
title?: string;
label?: string;
}): void {
if (title) {
element.setAttribute("title", title);
}
if (label) {
element.setAttribute("aria-label", label);
}
}

View File

@@ -125,7 +125,13 @@ handler.
```html
<button type="button" bitFormButton bitButton [bitAction]="handler">Do action</button>
<button type="button" bitFormButton bitIconButton="bwi-star" [bitAction]="handler"></button>
<button
type="button"
bitFormButton
bitIconButton="bwi-star"
label="Your label here"
[bitAction]="handler"
></button>
```
## `[bitSubmit]` Disabled Form Submit

View File

@@ -7,6 +7,7 @@ import { delay, of } from "rxjs";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { A11yTitleDirective } from "../a11y";
import { ButtonModule } from "../button";
import { FormFieldModule } from "../form-field";
import { IconButtonModule } from "../icon-button";
@@ -28,20 +29,21 @@ const template = `
<bit-form-field>
<bit-label>Email</bit-label>
<input bitInput formControlName="email" />
<button type="button" bitSuffix bitIconButton="bwi-refresh" bitFormButton [bitAction]="refresh"></button>
<button type="button" label="Refresh" bitSuffix bitIconButton="bwi-refresh" bitFormButton [bitAction]="refresh"></button>
</bit-form-field>
<button class="tw-me-2" type="submit" buttonType="primary" bitButton bitFormButton>Submit</button>
<button class="tw-me-2" type="button" buttonType="secondary" bitButton bitFormButton>Cancel</button>
<button class="tw-me-2" type="button" buttonType="danger" bitButton bitFormButton [bitAction]="delete">Delete</button>
<button class="tw-me-2" type="button" buttonType="secondary" bitButton bitFormButton [disabled]="true">Disabled</button>
<button class="tw-me-2" type="button" buttonType="muted" bitIconButton="bwi-star" bitFormButton [bitAction]="delete">Delete</button>
<button class="tw-me-2" type="button" buttonType="muted" bitIconButton="bwi-star" label="Delete" bitFormButton [bitAction]="delete">Delete</button>
</form>`;
@Component({
selector: "app-promise-example",
template,
imports: [
A11yTitleDirective,
AsyncActionsModule,
ButtonModule,
FormFieldModule,
@@ -86,6 +88,7 @@ class PromiseExampleComponent {
selector: "app-observable-example",
template,
imports: [
A11yTitleDirective,
AsyncActionsModule,
ButtonModule,
FormFieldModule,

View File

@@ -63,7 +63,7 @@ from how click handlers are usually defined with the output syntax `(click)="han
```html
<button bitButton [bitAction]="handler">Do action</button>
<button bitIconButton="bwi-trash" [bitAction]="handler"></button>`;
<button bitIconButton="bwi-trash" label="Your label here" [bitAction]="handler"></button>`;
```
## Stories

View File

@@ -16,7 +16,7 @@ const template = /*html*/ `
<button type="button" bitButton buttonType="primary" [bitAction]="action" class="tw-me-2">
Perform action {{ statusEmoji }}
</button>
<button type="button" bitIconButton="bwi-trash" buttonType="danger" [bitAction]="action"></button>`;
<button type="button" label="Delete" bitIconButton="bwi-trash" buttonType="danger" [bitAction]="action"></button>`;
@Component({
template,

View File

@@ -19,8 +19,7 @@
buttonType="main"
size="small"
(click)="onClose.emit()"
[attr.title]="'close' | i18n"
[attr.aria-label]="'close' | i18n"
[label]="'close' | i18n"
></button>
}
</div>

View File

@@ -35,6 +35,7 @@
bitIconButton="bwi-ellipsis-h"
[bitMenuTriggerFor]="overflowMenu"
size="small"
[label]="'moreBreadcrumbs' | i18n"
></button>
<bit-menu #overflowMenu>
@for (breadcrumb of overflow; track breadcrumb) {

View File

@@ -2,6 +2,8 @@ import { CommonModule } from "@angular/common";
import { Component, ContentChildren, QueryList, input } from "@angular/core";
import { RouterModule } from "@angular/router";
import { I18nPipe } from "@bitwarden/ui-common";
import { IconButtonModule } from "../icon-button";
import { LinkModule } from "../link";
import { MenuModule } from "../menu";
@@ -16,7 +18,7 @@ import { BreadcrumbComponent } from "./breadcrumb.component";
@Component({
selector: "bit-breadcrumbs",
templateUrl: "./breadcrumbs.component.html",
imports: [CommonModule, LinkModule, RouterModule, IconButtonModule, MenuModule],
imports: [I18nPipe, CommonModule, LinkModule, RouterModule, IconButtonModule, MenuModule],
})
export class BreadcrumbsComponent {
readonly show = input(3);

View File

@@ -2,9 +2,12 @@ import { Component, importProvidersFrom } from "@angular/core";
import { RouterModule } from "@angular/router";
import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { IconButtonModule } from "../icon-button";
import { LinkModule } from "../link";
import { MenuModule } from "../menu";
import { I18nMockService } from "../utils";
import { BreadcrumbComponent } from "./breadcrumb.component";
import { BreadcrumbsComponent } from "./breadcrumbs.component";
@@ -26,6 +29,16 @@ export default {
decorators: [
moduleMetadata({
imports: [LinkModule, MenuModule, IconButtonModule, RouterModule, BreadcrumbComponent],
providers: [
{
provide: I18nService,
useFactory: () => {
return new I18nMockService({
moreBreadcrumbs: "More breadcrumbs",
});
},
},
],
}),
applicationConfig({
providers: [

View File

@@ -37,8 +37,7 @@
buttonType="main"
size="default"
bitDialogClose
[attr.title]="'close' | i18n"
[attr.aria-label]="'close' | i18n"
[label]="'close' | i18n"
></button>
}
</header>

View File

@@ -101,8 +101,7 @@ export const Default: Story = {
bitIconButton="bwi-trash"
buttonType="danger"
size="default"
title="Delete"
aria-label="Delete"></button>
label="Delete"></button>
</ng-container>
</bit-dialog>
`,
@@ -219,7 +218,7 @@ export const WithCards: Story = {
<h2 bitTypography="h6">
Foo
</h2>
<button type="button" bitIconButton="bwi-star" size="small" slot="end"></button>
<button type="button" label="Favorite" bitIconButton="bwi-star" size="small" slot="end"></button>
</bit-section-header>
<bit-card>
<bit-form-field>
@@ -239,7 +238,7 @@ export const WithCards: Story = {
<h2 bitTypography="h6">
Bar
</h2>
<button type="button" bitIconButton="bwi-star" size="small" slot="end"></button>
<button label="Favorite" type="button" bitIconButton="bwi-star" size="small" slot="end"></button>
</bit-section-header>
<bit-card>
<bit-form-field>
@@ -265,8 +264,7 @@ export const WithCards: Story = {
bitIconButton="bwi-trash"
buttonType="danger"
size="default"
title="Delete"
aria-label="Delete"></button>
label="Delete"></button>
</ng-container>
</bit-dialog>
</form>

View File

@@ -28,6 +28,7 @@ let nextId = 0;
* bitIconButton="bwi-sliders"
* [buttonType]="'muted'"
* [bitDisclosureTriggerFor]="disclosureRef"
* [label]="'Settings' | i18n"
* ></button>
* <bit-disclosure #disclosureRef open>click button to hide this content</bit-disclosure>
* ```

View File

@@ -27,7 +27,7 @@ export const DisclosureWithIconButton: Story = {
render: (args) => ({
props: args,
template: /*html*/ `
<button type="button" bitIconButton="bwi-sliders" [buttonType]="'muted'" [bitDisclosureTriggerFor]="disclosureRef">
<button type="button" label="Settings" bitIconButton="bwi-sliders" [buttonType]="'muted'" [bitDisclosureTriggerFor]="disclosureRef">
</button>
<bit-disclosure #disclosureRef class="tw-text-main tw-block" open>click button to hide this content</bit-disclosure>
`,

View File

@@ -5,11 +5,5 @@
{{ title() }}
</h2>
</div>
<button
bitIconButton="bwi-close"
type="button"
bitDrawerClose
[attr.title]="'close' | i18n"
[attr.aria-label]="'close' | i18n"
></button>
<button bitIconButton="bwi-close" type="button" bitDrawerClose [label]="'close' | i18n"></button>
</header>

View File

@@ -239,8 +239,8 @@ export const Readonly: Story = {
<bit-form-field>
<bit-label>Input</bit-label>
<input bitInput type="password" value="Foobar" [readonly]="true" />
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
<button type="button" bitSuffix bitIconButton="bwi-clone" [appA11yTitle]="'Clone Input'"></button>
<button type="button" label="Toggle password" bitIconButton bitSuffix bitPasswordInputToggle></button>
<button type="button" bitSuffix bitIconButton="bwi-clone" [label]="'Clone Input'"></button>
</bit-form-field>
<bit-form-field>
@@ -261,7 +261,7 @@ export const Readonly: Story = {
<bit-label>Input</bit-label>
<input bitInput type="password" value="Foobar" readonly />
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
<button type="button" bitSuffix bitIconButton="bwi-clone" [appA11yTitle]="'Clone Input'"></button>
<button type="button" bitSuffix bitIconButton="bwi-clone" [label]="'Clone Input'"></button>
</bit-form-field>
<bit-form-field>
@@ -309,11 +309,11 @@ export const ButtonInputGroup: Story = {
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a>
</bit-label>
<button type="button" bitPrefix bitIconButton="bwi-star" [appA11yTitle]="'Favorite Label'"></button>
<button type="button" bitPrefix bitIconButton="bwi-star" [label]="'Favorite Label'"></button>
<input bitInput placeholder="Placeholder" />
<button type="button" bitSuffix bitIconButton="bwi-eye" [appA11yTitle]="'Hide Label'"></button>
<button type="button" bitSuffix bitIconButton="bwi-clone" [appA11yTitle]="'Clone Label'"></button>
<button type="button" bitSuffix bitIconButton="bwi-ellipsis-v" [appA11yTitle]="'Menu Label'"></button>
<button type="button" bitSuffix bitIconButton="bwi-eye" [label]="'Hide Label'"></button>
<button type="button" bitSuffix bitIconButton="bwi-clone" [label]="'Clone Label'"></button>
<button type="button" bitSuffix bitIconButton="bwi-ellipsis-v" [label]="'Menu Label'"></button>
</bit-form-field>
`,
}),
@@ -326,11 +326,11 @@ export const DisabledButtonInputGroup: Story = {
template: /*html*/ `
<bit-form-field>
<bit-label>Label</bit-label>
<button type="button" bitPrefix bitIconButton="bwi-star" disabled [appA11yTitle]="'Favorite Label'"></button>
<button type="button" bitPrefix bitIconButton="bwi-star" disabled [label]="'Favorite Label'"></button>
<input bitInput placeholder="Placeholder" disabled />
<button type="button" bitSuffix bitIconButton="bwi-eye" disabled [appA11yTitle]="'Hide Label'"></button>
<button type="button" bitSuffix bitIconButton="bwi-clone" disabled [appA11yTitle]="'Clone Label'"></button>
<button type="button" bitSuffix bitIconButton="bwi-ellipsis-v" disabled [appA11yTitle]="'Menu Label'"></button>
<button type="button" bitSuffix bitIconButton="bwi-eye" disabled [label]="'Hide Label'"></button>
<button type="button" bitSuffix bitIconButton="bwi-clone" disabled [label]="'Clone Label'"></button>
<button type="button" bitSuffix bitIconButton="bwi-ellipsis-v" disabled [label]="'Menu Label'"></button>
</bit-form-field>
`,
@@ -345,9 +345,9 @@ export const PartiallyDisabledButtonInputGroup: Story = {
<bit-form-field>
<bit-label>Label</bit-label>
<input bitInput placeholder="Placeholder" disabled />
<button type="button" bitSuffix bitIconButton="bwi-eye" [appA11yTitle]="'Hide Label'"></button>
<button type="button" bitSuffix bitIconButton="bwi-clone" [appA11yTitle]="'Clone Label'"></button>
<button type="button" bitSuffix bitIconButton="bwi-ellipsis-v" disabled [appA11yTitle]="'Menu Label'"></button>
<button type="button" bitSuffix bitIconButton="bwi-eye" [label]="'Hide Label'"></button>
<button type="button" bitSuffix bitIconButton="bwi-clone" [label]="'Clone Label'"></button>
<button type="button" bitSuffix bitIconButton="bwi-ellipsis-v" disabled [label]="'Menu Label'"></button>
</bit-form-field>
`,
}),

View File

@@ -20,7 +20,13 @@ import { BitPasswordInputToggleDirective } from "./password-input-toggle.directi
<bit-form-field>
<bit-label>Password</bit-label>
<input bitInput type="password" />
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
<button
type="button"
label="Toggle password visibility"
bitIconButton
bitSuffix
bitPasswordInputToggle
></button>
</bit-form-field>
</form>
`,

View File

@@ -48,7 +48,7 @@ export const Default: Story = {
<bit-form-field>
<bit-label>Password</bit-label>
<input bitInput type="password" />
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
<button type="button" label="Toggle password visibility" bitIconButton bitSuffix bitPasswordInputToggle></button>
</bit-form-field>
</form>
`,
@@ -63,7 +63,7 @@ export const Binding: Story = {
<bit-form-field>
<bit-label>Password</bit-label>
<input bitInput type="password" />
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle [(toggled)]="toggled"></button>
<button type="button" label="Toggle password visibility" bitIconButton bitSuffix bitPasswordInputToggle [(toggled)]="toggled"></button>
</bit-form-field>
<label class="tw-text-main">

View File

@@ -1,8 +1,9 @@
import { NgClass } from "@angular/common";
import { Component, computed, ElementRef, HostBinding, input, model } from "@angular/core";
import { Component, computed, effect, ElementRef, HostBinding, input, model } from "@angular/core";
import { toObservable, toSignal } from "@angular/core/rxjs-interop";
import { debounce, interval } from "rxjs";
import { setA11yTitleAndAriaLabel } from "../a11y/set-a11y-title-and-aria-label";
import { ButtonLikeAbstraction } from "../shared/button-like.abstraction";
import { FocusableElement } from "../shared/focusable-element";
@@ -62,7 +63,7 @@ const sizes: Record<IconButtonSize, string[]> = {
small: ["tw-text-base", "tw-p-2", "tw-rounded"],
};
/**
* Icon buttons are used when no text accompanies the button. It consists of an icon that may be updated to any icon in the `bwi-font`, a `title` attribute, and an `aria-label`.
* Icon buttons are used when no text accompanies the button. It consists of an icon that may be updated to any icon in the `bwi-font`, a `title` attribute, and an `aria-label` that are added via the `label` input.
* The most common use of the icon button is in the banner, toast, and modal components as a close button. It can also be found in tables as the 3 dot option menu, or on navigation list items when there are options that need to be collapsed into a menu.
@@ -94,6 +95,14 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableE
readonly size = model<IconButtonSize>("default");
/**
* label input will be used to set the `aria-label` attributes on the button.
* This is for accessibility purposes, as it provides a text alternative for the icon button.
*
* NOTE: It will also be used to set the `title` attribute on the button if no `title` is provided.
*/
readonly label = input<string>();
@HostBinding("class") get classList() {
return [
"tw-font-semibold",
@@ -159,5 +168,15 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableE
return this.elementRef.nativeElement;
}
constructor(private elementRef: ElementRef) {}
constructor(private elementRef: ElementRef) {
const originalTitle = this.elementRef.nativeElement.getAttribute("title");
effect(() => {
setA11yTitleAndAriaLabel({
element: this.elementRef.nativeElement,
title: originalTitle ?? this.label(),
label: this.label(),
});
});
}
}

View File

@@ -9,6 +9,7 @@ export default {
component: BitIconButtonComponent,
args: {
bitIconButton: "bwi-plus",
label: "Your button label here",
},
argTypes: {
buttonType: {

View File

@@ -102,10 +102,10 @@ Actions are commonly icon buttons or badge buttons.
<button type="button" bitBadge variant="primary">Auto-fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" aria-label="Copy"></button>
<button type="button" bitIconButton="bwi-clone" label="Copy"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v" aria-label="Options"></button>
<button type="button" bitIconButton="bwi-ellipsis-v" label="Options"></button>
</bit-item-action>
</ng-container>
</bit-item>

View File

@@ -77,10 +77,10 @@ export const Default: Story = {
<button type="button" bitBadge variant="primary">Fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" size="small"></button>
<button type="button" bitIconButton="bwi-clone" size="small" label="Clone"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
<button type="button" bitIconButton="bwi-ellipsis-v" size="small" label="More options"></button>
</bit-item-action>
</ng-container>
</bit-item>
@@ -150,10 +150,10 @@ export const TextOverflowTruncate: Story = {
</bit-item-content>
<ng-container slot="end">
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" size="small"></button>
<button type="button" bitIconButton="bwi-clone" size="small" label="Clone"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
<button type="button" bitIconButton="bwi-ellipsis-v" size="small" label="More options"></button>
</bit-item-action>
</ng-container>
</bit-item>
@@ -173,10 +173,10 @@ export const TextOverflowWrap: Story = {
</bit-item-content>
<ng-container slot="end">
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" size="small"></button>
<button type="button" bitIconButton="bwi-clone" size="small" label="Clone"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
<button type="button" bitIconButton="bwi-ellipsis-v" size="small" label="More options"></button>
</bit-item-action>
</ng-container>
</bit-item>
@@ -198,10 +198,10 @@ const multipleActionListTemplate = /*html*/ `
<button type="button" bitBadge variant="primary">Fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" size="small"></button>
<button type="button" bitIconButton="bwi-clone" size="small" label="Clone"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
<button type="button" bitIconButton="bwi-ellipsis-v" size="small" label="More options"></button>
</bit-item-action>
</ng-container>
</bit-item>
@@ -217,10 +217,10 @@ const multipleActionListTemplate = /*html*/ `
<button type="button" bitBadge variant="primary">Fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" size="small"></button>
<button type="button" bitIconButton="bwi-clone" size="small" label="Clone"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
<button type="button" bitIconButton="bwi-ellipsis-v" size="small" label="More options"></button>
</bit-item-action>
</ng-container>
</bit-item>
@@ -236,10 +236,10 @@ const multipleActionListTemplate = /*html*/ `
<button type="button" bitBadge variant="primary">Fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" size="small"></button>
<button type="button" bitIconButton="bwi-clone" size="small" label="Clone"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
<button type="button" bitIconButton="bwi-ellipsis-v" size="small" label="More options"></button>
</bit-item-action>
</ng-container>
</bit-item>
@@ -255,10 +255,10 @@ const multipleActionListTemplate = /*html*/ `
<button type="button" bitBadge variant="primary">Fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" size="small"></button>
<button type="button" bitIconButton="bwi-clone" size="small" label="Clone"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
<button type="button" bitIconButton="bwi-ellipsis-v" size="small" label="More options"></button>
</bit-item-action>
</ng-container>
</bit-item>
@@ -274,10 +274,10 @@ const multipleActionListTemplate = /*html*/ `
<button type="button" bitBadge variant="primary">Fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" size="small"></button>
<button type="button" bitIconButton="bwi-clone" size="small" label="Clone"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
<button type="button" bitIconButton="bwi-ellipsis-v" size="small" label="More options"></button>
</bit-item-action>
</ng-container>
</bit-item>
@@ -293,10 +293,10 @@ const multipleActionListTemplate = /*html*/ `
<button type="button" bitBadge variant="primary">Fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" size="small"></button>
<button type="button" bitIconButton="bwi-clone" size="small" label="Clone"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
<button type="button" bitIconButton="bwi-ellipsis-v" size="small" label="More options"></button>
</bit-item-action>
</ng-container>
</bit-item>
@@ -410,10 +410,10 @@ export const VirtualScrolling: Story = {
<button type="button" bitBadge variant="primary">Fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" size="small"></button>
<button type="button" bitIconButton="bwi-clone" size="small" label="Clone"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
<button type="button" bitIconButton="bwi-ellipsis-v" size="small" label="More options"></button>
</bit-item-action>
</ng-container>
</bit-item>
@@ -440,10 +440,10 @@ export const WithoutBorderRadius: Story = {
<button type="button" bitBadge variant="primary">Fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone"></button>
<button type="button" bitIconButton="bwi-clone" label="Clone"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v"></button>
<button type="button" bitIconButton="bwi-ellipsis-v" label="More options"></button>
</bit-item-action>
</ng-container>
</bit-item>

View File

@@ -18,11 +18,10 @@
[buttonType]="'nav-contrast'"
(click)="toggle($event)"
size="small"
[title]="'toggleCollapse' | i18n"
aria-haspopup="true"
[attr.aria-expanded]="open().toString()"
[attr.aria-controls]="contentId"
[attr.aria-label]="['toggleCollapse' | i18n, text()].join(' ')"
[label]="['toggleCollapse' | i18n, text()].join(' ')"
></button>
</ng-template>
<ng-container slot="end">

View File

@@ -95,7 +95,7 @@ export const WithChildButtons: Story = {
[bitIconButton]="'bwi-pencil-square'"
[buttonType]="'nav-contrast'"
size="small"
aria-label="option 2"
label="Edit"
></button>
<button
type="button"
@@ -104,7 +104,7 @@ export const WithChildButtons: Story = {
[bitIconButton]="'bwi-check'"
[buttonType]="'nav-contrast'"
size="small"
aria-label="option 3"
label="Confirm"
></button>
</bit-nav-item>
`,

View File

@@ -37,7 +37,7 @@
buttonType="nav-contrast"
size="small"
(click)="sideNavService.toggle()"
[attr.aria-label]="'toggleSideNavigation' | i18n"
[label]="'toggleSideNavigation' | i18n"
[attr.aria-expanded]="data.open"
aria-controls="bit-side-nav"
></button>

View File

@@ -18,8 +18,7 @@
<button
type="button"
bitIconButton="bwi-close"
[attr.title]="'close' | i18n"
[attr.aria-label]="'close' | i18n"
[label]="'close' | i18n"
(click)="closed.emit()"
size="small"
></button>

View File

@@ -51,7 +51,7 @@ padding to align the header with the border radius of the card/item.
<bit-section>
<bit-section-header>
<h2 bitTypography="h6">I'm a section header</h2>
<button bitIconButton="bwi-star" size="small" slot="end"></button>
<button bitIconButton="bwi-star" label="Favorite" size="small" slot="end"></button>
</bit-section-header>
<bit-card>
<h3 bitTypography="h3">I'm card content</h3>

View File

@@ -69,7 +69,7 @@ export const HeaderVariants: Story = {
<h2 bitTypography="h6">
Title with icon button suffix
</h2>
<button type="button" bitIconButton="bwi-refresh" size="small"></button>
<button type="button" label="Refresh" bitIconButton="bwi-refresh" size="small"></button>
</bit-section-header>
`,
}),
@@ -88,7 +88,7 @@ export const HeaderEndSlotVariants: Story = {
<h2 bitTypography="h6">
Title with end slot icon button
</h2>
<button type="button" bitIconButton="bwi-star" size="small" slot="end"></button>
<button type="button" label="Favorite" bitIconButton="bwi-star" size="small" slot="end"></button>
</bit-section-header>
`,
}),
@@ -103,7 +103,7 @@ export const HeaderWithPadding: Story = {
<h2 bitTypography="h6">
Card as immediate sibling
</h2>
<button type="button" bitIconButton="bwi-star" size="small" slot="end"></button>
<button type="button" label="Favorite" bitIconButton="bwi-star" size="small" slot="end"></button>
</bit-section-header>
<bit-card>
<h3 bitTypography="h3">bit-section-header has padding</h3>
@@ -114,7 +114,7 @@ export const HeaderWithPadding: Story = {
<h2 bitTypography="h6">
Card nested in immediate sibling
</h2>
<button type="button" bitIconButton="bwi-star" size="small" slot="end"></button>
<button type="button" label="Favorite" bitIconButton="bwi-star" size="small" slot="end"></button>
</bit-section-header>
<div>
<bit-card>
@@ -127,7 +127,7 @@ export const HeaderWithPadding: Story = {
<h2 bitTypography="h6">
Item as immediate sibling
</h2>
<button type="button" bitIconButton="bwi-star" size="small" slot="end"></button>
<button type="button" label="Favorite" bitIconButton="bwi-star" size="small" slot="end"></button>
</bit-section-header>
<bit-item>
<bit-item-content bitTypography="body1">bit-section-header has padding</bit-item-content>
@@ -138,7 +138,7 @@ export const HeaderWithPadding: Story = {
<h2 bitTypography="h6">
Item nested in immediate sibling
</h2>
<button type="button" bitIconButton="bwi-star" size="small" slot="end"></button>
<button type="button" label="Favorite" bitIconButton="bwi-star" size="small" slot="end"></button>
</bit-section-header>
<bit-item-group>
<bit-item>
@@ -160,7 +160,7 @@ export const HeaderWithoutPadding: Story = {
<h2 bitTypography="h6">
No card or item used
</h2>
<button bitIconButton="bwi-star" size="small" slot="end"></button>
<button bitIconButton="bwi-star" size="small" slot="end" label="Favorite"></button>
</bit-section-header>
<div>
<h3 bitTypography="h3">just a div, so bit-section-header has no padding</h3>
@@ -171,7 +171,7 @@ export const HeaderWithoutPadding: Story = {
<h2 bitTypography="h6">
Card nested in non-immediate sibling
</h2>
<button bitIconButton="bwi-star" size="small" slot="end"></button>
<button bitIconButton="bwi-star" size="small" slot="end" label="Favorite"></button>
</bit-section-header>
<div class="tw-text-main">
a div here

View File

@@ -36,7 +36,7 @@ import { TableDataSource, TableModule } from "../../../table";
<button
bitIconButton="bwi-ellipsis-v"
type="button"
aria-label="Options"
label="Options"
(click)="openDefaultDialog()"
></button>
</td>

View File

@@ -80,7 +80,13 @@ import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module";
</button>
</bit-label>
<input bitInput type="password" formControlName="password" />
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
<button
type="button"
label="Toggle password visibility"
bitIconButton
bitSuffix
bitPasswordInputToggle
></button>
</bit-form-field>
<div class="tw-mb-6">

View File

@@ -23,6 +23,7 @@ import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module";
type="button"
bitIconButton="bwi-ellipsis-v"
[bitMenuTriggerFor]="menu1"
label="Options"
></button>
<bit-menu #menu1>
<a href="#" bitMenuItem>Anchor link</a>
@@ -40,6 +41,7 @@ import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module";
type="button"
bitIconButton="bwi-ellipsis-v"
[bitMenuTriggerFor]="menu2"
label="Options"
></button>
<bit-menu #menu2>
<a href="#" bitMenuItem>Anchor link</a>

View File

@@ -25,6 +25,7 @@
type="button"
size="small"
(click)="this.onClose.emit()"
[label]="'close' | i18n"
></button>
</div>
<div class="tw-h-1 tw-w-full tw-bg-text-main/30" [style.width]="progressWidth() + '%'"></div>

View File

@@ -0,0 +1,3 @@
import requireLabelOnBiticonbutton from "./require-label-on-biticonbutton.mjs";
export default { rules: { "require-label-on-biticonbutton": requireLabelOnBiticonbutton } };

View File

@@ -0,0 +1,51 @@
export const errorMessage =
"Elements with 'bitIconButton' must also have a 'label' attribute for accessibility.";
export default {
meta: {
type: "problem",
docs: {
description:
"Require a label attribute on elements with bitIconButton, except when ignored attributes are present",
category: "Best Practices",
recommended: false,
},
schema: [
{
type: "object",
properties: {
ignoreIfHas: {
type: "array",
items: { type: "string" },
description: "Attributes that, if present, will skip the label requirement.",
},
},
additionalProperties: false,
},
],
},
create(context) {
const [{ ignoreIfHas = [] } = {}] = context.options;
return {
Element(node) {
const allAttrNames = [
...(node.attributes?.map((attr) => attr.name) ?? []),
...(node.inputs?.map((input) => input.name) ?? []),
...(node.templateAttrs?.map((attr) => attr.name) ?? []),
];
const hasBitIconButton = allAttrNames.includes("bitIconButton");
const hasLabel = allAttrNames.includes("label");
const shouldIgnore = ignoreIfHas.some((attr) => allAttrNames.includes(attr));
if (hasBitIconButton && !shouldIgnore && !hasLabel) {
context.report({
node,
message: errorMessage,
});
}
},
};
},
};

View File

@@ -89,6 +89,7 @@
appStopClick
bitSuffix
(click)="generatePassword()"
[label]="'generatePassword' | i18n"
></button>
<button
type="button"
@@ -98,6 +99,7 @@
bitSuffix
[appCopyClick]="filePassword"
[valueLabel]="'password' | i18n"
[label]="'password' | i18n"
showToast
></button>
<bit-hint>{{ "exportPasswordDescription" | i18n }}</bit-hint>

View File

@@ -12,7 +12,7 @@
bitIconButton="bwi-clone"
[appCopyClick]="credential.credential"
[valueLabel]="getGeneratedValueText(credential)"
[appA11yTitle]="getCopyText(credential)"
[label]="getCopyText(credential)"
showToast
>
{{ getCopyText(credential) }}

Some files were not shown because too many files have changed in this diff Show More