1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-12-21 16:18:28 +01:00

[CL-265] CL/extension refresh feature branch (#8696)

* [CL-245] Update palette to new light and dark theme colors (#8633)

* [CL-245] Add new color swatches to storybook (#8697)

* [CL-238] update typography (#8997)

* [CL-230] [CL-296] Update button styles (#9345)

* [CL-237] Update menu styles for extension refresh (#9525)

* [CL-267] Add 100-level color variants and update primary-600 (#9550)

* [CL-286] Update badge to use focus-visible instead of focus (#9551)

* [CL-250] Update badge styles for extension refresh (#9572)

* [CL-234] callout style refresh (#9920)

* [CL-233] Update form field styles (#9776)

* [CL-239][CL-251][CL-342] dialog style refresh (#10096)

* [CL-239] simple dialog style refresh

* [CL-342] fix text overflow in dialog; add story

* [CL-244] readonly fields (#10164)

* [CL-352] Fix Angular errors related to form element changes (#10211)

* [CL-273] Update styles for checkbox and form control (#10146)

* [CL-274] Update styling for radio button (#10333)

* [CL-338] Remove extra space in item content when end slot is empty (#10350)

* [CL-377] Fix extension style conflict for input background (#10351)

* [CL-271] Update styles for toggle (#10377)

* [CL-381] Update spacing around form elements (#10432)

* [CL-229] Update icon button styles (#10405)

* [CL-380] Remove hover state from disabled form fields (#10639)

* [CL-405] Allow toggle group input to be full width (#10658)

* [CL-389] Exclude end slot label content from truncation (#10508)

* [CL-383] Remove manual focus when password toggle is clicked (#10749)

* [CL-278][CL-391] misc bit-item style fixes (#10758)

* [CL-391] use pointer cursor on hover when link or button

* [CL-210] Change base font size from 14px to 16px (#10779)

* [CL-291] Finalize styling for chip select (#10771)

* [CL-257] update banner component styles (#10766)

* [CL-443] Fix sizing issues (#10893)

* [CL-445] Fix small sizing and spacing issues (#10962)

* [CL-382] Reduce element shifting on readonly hover (#10956)

* [CL-396] Update theme colors to new hexes (#10968)

* [CL-395] Remove text headers color (#10997)

* [CL-404] Switch to primary-600 for all focus indicators (#11015)

* [CL-397] Remove primary-500 (#11036)

* [CL-447] Ensure DM Sans displays correctly at all font weights (#11041)

* [CL-448] Scrollbar Styles (#11111)

* CL-252/update toast (#10996)

* [CL-275] Update link styles (#11174)

* [CL-446] Update hover state for unselected chip selects (#11172)

* [CL-454] Improve color a11y for toast and banner interactive elements (#11200)

* [CL-457] Center input text for select and multiselect (#11239)

* [CL-455] Do not use responsive margin for sections in dialogs or extension (#11243)

* [CL-459] Fix chip behavior when opening menu while item is selected (#11227)

* [CL-388] Update vertical nav colors for new palette (#11226)

* scope styled scrollbar to only select elements (#11247)

* edit radio buttons to be block inputs and update spacing (#11291)

* [CL-453] Fix multiselect chip spacing and truncation (#11300)

* [PM-11131] Prevent duplicated sr labels on form field icon buttons (#11383)

* [CL-303] Prevent chip menu from running offscreen (#11348)

* [CL-476] Fix DM Sans font on Windows (#11409)

* implements scrollbar styles for firefox/chrome and safari (#11447)

* [CL-472] Fix search background color in extension (#11466)

* [CL-481] Style updates for bit-item, bit-card, and primary-100 (#11473)

* [CL-478] Remove underline on hover for most components (#11477)

* [CL-477] Remove focus styles for readonly input (#11510)

* [CL-487] Fix vault items virtual scroll height (#11581)

* [PM-8625] Increase popup width (#11686)

* [CL-494] Wrap long words in toggle group (#11659)

* [CL-13820] Add class to remove link underline (#11762)

* [CL-435] Prevent Windows extension from shifting (#11851)

* [CL-503] Add notification color variables (#11802)

* [PM-14043] Update size of toggle group label to fit more content (#11881)

* [CL-498] Set chip menu width minimum to chip select width (#11905)

---------

Co-authored-by: Will Martin <contact@willmartian.com>
Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>
Co-authored-by: Nick Krantz <125900171+nick-livefront@users.noreply.github.com>
Co-authored-by: Merissa Weinstein <merissa.k.weinstein@gmail.com>
Co-authored-by: Danielle Flinn <43477473+danielleflinn@users.noreply.github.com>
This commit is contained in:
Victoria League 2024-11-15 09:21:17 -05:00 committed by GitHub
parent 5c540a86f4
commit 3b5b2d6bd6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
150 changed files with 1767 additions and 1059 deletions

View File

@ -5,16 +5,6 @@
./apps/browser/store/windows/Assets
./bitwarden_license/README.md
./libs/angular/src/directives/cipherListVirtualScroll.directive.ts
./libs/angular/src/scss/webfonts/Open_Sans-italic-700.woff
./libs/angular/src/scss/webfonts/Open_Sans-normal-300.woff
./libs/angular/src/scss/webfonts/Open_Sans-normal-700.woff
./libs/angular/src/scss/webfonts/Open_Sans-italic-300.woff
./libs/angular/src/scss/webfonts/Open_Sans-italic-600.woff
./libs/angular/src/scss/webfonts/Open_Sans-italic-800.woff
./libs/angular/src/scss/webfonts/Open_Sans-italic-400.woff
./libs/angular/src/scss/webfonts/Open_Sans-normal-600.woff
./libs/angular/src/scss/webfonts/Open_Sans-normal-800.woff
./libs/angular/src/scss/webfonts/Open_Sans-normal-400.woff
./libs/admin-console/README.md
./libs/auth/README.md
./libs/billing/README.md

View File

@ -2,7 +2,7 @@
$dark-icon-themes: "theme_dark", "theme_solarizedDark", "theme_nord";
$font-family-sans-serif: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
$font-family-sans-serif: "DM Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
$font-family-source-code-pro: "Source Code Pro", monospace;
$font-size-base: 14px;
$text-color: #212529;

View File

@ -1,5 +1,5 @@
<footer
class="tw-p-3 tw-border-0 tw-border-solid tw-border-t tw-border-secondary-300 tw-bg-background"
class="tw-p-4 tw-border-0 tw-border-solid tw-border-t tw-border-secondary-300 tw-bg-background"
>
<div class="tw-max-w-screen-sm tw-mx-auto tw-flex tw-justify-between tw-w-full">
<div class="tw-flex tw-justify-start tw-gap-2">

View File

@ -1,5 +1,5 @@
<header
class="tw-p-4 tw-transition-colors tw-duration-200 tw-border-0 tw-border-b tw-border-solid"
class="tw-px-4 tw-py-3 tw-transition-colors tw-duration-200 tw-border-0 tw-border-b tw-border-solid"
[ngClass]="{
'tw-bg-background-alt tw-border-transparent':
this.background === 'alt' && !pageContentScrolled(),
@ -17,7 +17,7 @@
[attr.aria-label]="'back' | i18n"
[bitAction]="backAction"
></button>
<h1 *ngIf="pageTitle" bitTypography="h3" class="!tw-mb-0.5 tw-text-headers">
<h1 *ngIf="pageTitle" bitTypography="h3" class="!tw-mb-0.5">
{{ pageTitle }}
</h1>
<ng-content></ng-content>

View File

@ -39,7 +39,7 @@ class ExtensionContainerComponent {}
@Component({
selector: "vault-placeholder",
template: `
<bit-section disableMargin>
<bit-section>
<bit-item-group aria-label="Mock Vault Items">
<bit-item *ngFor="let item of data; index as i">
<button bit-item-content>

View File

@ -12,7 +12,7 @@
<ng-content select="[slot=above-scroll-area]"></ng-content>
</div>
<div
class="tw-max-w-screen-sm tw-mx-auto tw-overflow-y-auto tw-flex tw-flex-col tw-w-full tw-h-full"
class="tw-max-w-screen-sm tw-mx-auto tw-overflow-y-auto tw-flex tw-flex-col tw-w-full tw-h-full tw-styled-scrollbar"
(scroll)="handleScroll($event)"
[ngClass]="{ 'tw-invisible': loading }"
>

View File

@ -104,7 +104,7 @@ import "../platform/popup/locales";
maxOpened: 2,
autoDismiss: true,
closeButton: true,
positionClass: "toast-bottom-full-width",
positionClass: "toast-top-full-width",
}),
BrowserAnimationsModule,
BrowserModule,

View File

@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 100% 100%">
<text fill="%23333333" x="50%" y="50%" font-family="\'Open Sans\', \'Helvetica Neue\', Helvetica, Arial, sans-serif"
<text fill="%23333333" x="50%" y="50%" font-family="\'DM Sans\', \'Helvetica Neue\', Helvetica, Arial, sans-serif"
font-size="18" text-anchor="middle">
Loading...
</text>

View File

@ -6,6 +6,10 @@
margin: 0;
}
html {
overflow: hidden;
}
html,
body {
font-family: $font-family-sans-serif;
@ -15,7 +19,7 @@ body {
}
body {
width: 375px !important;
width: 380px !important;
height: 600px !important;
position: relative;
min-height: 100vh;
@ -83,9 +87,9 @@ a:not(popup-page a, popup-tab-navigation a) {
}
}
input,
select,
textarea {
input:not(bit-form-field input, bit-search input),
select:not(bit-form-field select),
textarea:not(bit-form-field textarea) {
@include themify($themes) {
color: themed("textColor");
background-color: themed("inputBackgroundColor");
@ -95,7 +99,7 @@ textarea {
input,
select,
textarea,
button {
button:not(bit-chip-select button) {
font-size: $font-size-base;
font-family: $font-family-sans-serif;
}
@ -286,7 +290,7 @@ header:not(bit-callout header, bit-dialog header, popup-page header) {
}
}
input {
input:not(bit-form-field input) {
width: 100%;
margin: 0;
border: none;

View File

@ -306,7 +306,7 @@ input[type="password"]::-ms-reveal {
// contrast against the background, so its inversion is also still readable)
// and suppress user selection for most elements (to make it more app-like)
::selection {
:not(bit-form-field input)::selection {
@include themify($themes) {
color: themed("backgroundColor");
background-color: themed("primaryAccentColor");

View File

@ -301,7 +301,7 @@ app-fido2-v1 {
margin-top: 32px;
.subtitle {
font-family: Open Sans;
font-family: "DM Sans";
font-size: 24px;
font-style: normal;
font-weight: 600;

View File

@ -3,3 +3,24 @@
@tailwind utilities;
@import "../../../../../libs/components/src/tw-theme.css";
@layer components {
/** Safari Support */
html.browser_safari .tw-styled-scrollbar::-webkit-scrollbar {
@apply tw-overflow-auto;
}
html.browser_safari .tw-styled-scrollbar::-webkit-scrollbar-thumb {
@apply tw-bg-secondary-500 tw-rounded-lg tw-border-4 tw-border-solid tw-border-transparent tw-bg-clip-content;
}
html.browser_safari .tw-styled-scrollbar::-webkit-scrollbar-track {
@apply tw-bg-background-alt;
}
html.browser_safari .tw-styled-scrollbar::-webkit-scrollbar-thumb:hover {
@apply tw-bg-secondary-600;
}
/* FireFox & Chrome support */
html:not(.browser_safari) .tw-styled-scrollbar {
scrollbar-color: rgb(var(--color-secondary-500)) rgb(var(--color-background-alt));
}
}

View File

@ -2,9 +2,9 @@
$dark-icon-themes: "theme_dark", "theme_solarizedDark", "theme_nord";
$font-family-sans-serif: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
$font-family-sans-serif: "DM Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
$font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace;
$font-size-base: 14px;
$font-size-base: 16px;
$font-size-large: 18px;
$font-size-xlarge: 22px;
$font-size-xxlarge: 28px;

View File

@ -58,7 +58,11 @@
</div>
</div>
<div *ngIf="vaultState === null" cdkVirtualScrollingElement class="tw-h-full tw-p-3">
<div
*ngIf="vaultState === null"
cdkVirtualScrollingElement
class="tw-h-full tw-p-3 tw-styled-scrollbar"
>
<app-autofill-vault-list-items></app-autofill-vault-list-items>
<app-vault-list-items-container
[title]="'favorites' | i18n"

View File

@ -111,7 +111,7 @@ export class AvatarComponent implements OnChanges, OnInit {
textTag.setAttribute("fill", Utils.pickTextColorBasedOnBgColor(color, 135, true));
textTag.setAttribute(
"font-family",
'"Open Sans","Helvetica Neue",Helvetica,Arial,' +
'"DM Sans","Helvetica Neue",Helvetica,Arial,' +
'sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"',
);
textTag.textContent = character;

View File

@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 100% 100%">
<text fill="%23333333" x="50%" y="50%" font-family="\'Open Sans\', \'Helvetica Neue\', Helvetica, Arial, sans-serif"
<text fill="%23333333" x="50%" y="50%" font-family="\'DM Sans\', \'Helvetica Neue\', Helvetica, Arial, sans-serif"
font-size="18" text-anchor="middle">
Loading...
</text>

View File

@ -2,7 +2,7 @@
$dark-icon-themes: "theme_dark", "theme_nord";
$font-family-sans-serif: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
$font-family-sans-serif: "DM Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
$font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace;
$font-size-base: 14px;
$font-size-large: 18px;

View File

@ -30,7 +30,7 @@
<a href="/" class="tw-btn-secondary tw-inline-block">Go to your web vault</a>
</main>
<footer class="tw-mt-auto tw-h-40 tw-bg-primary-500 tw-flex tw-justify-center tw-items-center">
<footer class="tw-mt-auto tw-h-40 tw-bg-primary-600 tw-flex tw-justify-center tw-items-center">
<i class="bwi bwi-shield tw-text-contrast tw-text-4xl"></i>
</footer>
</body>

View File

@ -81,8 +81,8 @@ export class GroupsComponent {
protected searchControl = new FormControl("");
// Fixed sizes used for cdkVirtualScroll
protected rowHeight = 46;
protected rowHeightClass = `tw-h-[46px]`;
protected rowHeight = 52;
protected rowHeightClass = `tw-h-[52px]`;
protected ModalTabType = GroupAddEditTabType;
private refreshGroups$ = new BehaviorSubject<void>(null);

View File

@ -104,8 +104,8 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
);
// Fixed sizes used for cdkVirtualScroll
protected rowHeight = 62;
protected rowHeightClass = `tw-h-[62px]`;
protected rowHeight = 69;
protected rowHeightClass = `tw-h-[69px]`;
constructor(
apiService: ApiService,

View File

@ -3,13 +3,7 @@
<div class="tw-flex" *ngIf="!hideMultiSelect">
<bit-form-field *ngIf="permissionMode == 'edit'" class="tw-mr-3 tw-shrink-0">
<bit-label>{{ "permission" | i18n }}</bit-label>
<!--
Built-in select height differs between browsers, this fix makes sure we match bit-multi-select height.
We might want to reconsider this fix when/if we implement
[CL-78] [Improvement] Completely restyled selects (https://bitwarden.atlassian.net/browse/CL-78)
-->
<select
class="tw-h-[35px]"
bitInput
[disabled]="disabled"
[(ngModel)]="initialPermission"

View File

@ -3,7 +3,7 @@
<button
id="sendBtn"
bitLink
linkType="contrast"
linkType="secondary"
bitButton
type="button"
buttonType="unstyled"

View File

@ -65,7 +65,7 @@
{{ "launchCloudSubscription" | i18n }}
</a>
<form [formGroup]="form">
<bit-radio-group formControlName="updateMethod">
<bit-radio-group formControlName="updateMethod" [block]="true">
<h2 class="mt-5">
{{ "licenseAndBillingManagement" | i18n }}
</h2>
@ -73,7 +73,6 @@
id="automatic-sync"
[value]="licenseOptions.SYNC"
[disabled]="disableLicenseSyncControl"
class="tw-block"
*ngIf="showAutomaticSyncAndManualUpload"
>
<bit-label
@ -94,29 +93,31 @@
</bit-hint>
</bit-radio-button>
<ng-container *ngIf="updateMethod === licenseOptions.SYNC">
<button
bitButton
buttonType="secondary"
type="button"
(click)="manageBillingSyncSelfHosted()"
>
{{ "manageBillingTokenSync" | i18n }}
</button>
<button
bitButton
buttonType="primary"
type="button"
[bitAction]="syncLicense"
[disabled]="!billingSyncEnabled"
>
{{ "syncLicense" | i18n }}
</button>
<div class="tw-mt-6">
<button
bitButton
buttonType="secondary"
type="button"
(click)="manageBillingSyncSelfHosted()"
>
{{ "manageBillingTokenSync" | i18n }}
</button>
<button
bitButton
buttonType="primary"
type="button"
[bitAction]="syncLicense"
[disabled]="!billingSyncEnabled"
>
{{ "syncLicense" | i18n }}
</button>
</div>
</ng-container>
<bit-radio-button
id="manual-upload"
[value]="licenseOptions.UPLOAD"
class="tw-mt-6 tw-block"
class="tw-mt-6"
*ngIf="showAutomaticSyncAndManualUpload"
>
<bit-label>{{ "manualUpload" | i18n }}</bit-label>
@ -128,7 +129,7 @@
<bit-label class="tw-mb-6 tw-block" *ngIf="!showAutomaticSyncAndManualUpload">
{{ "licenseAndBillingManagementDesc" | i18n }}
</bit-label>
<h3 *ngIf="showAutomaticSyncAndManualUpload" class="tw-font-semibold">
<h3 *ngIf="showAutomaticSyncAndManualUpload" class="tw-font-semibold tw-mt-6">
{{ "uploadLicense" | i18n }}
</h3>
<app-update-license

View File

@ -157,9 +157,9 @@ export class StripeService {
base: {
color: null,
fontFamily:
'"Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif, ' +
'"DM Sans", "Helvetica Neue", Helvetica, Arial, sans-serif, ' +
'"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
fontSize: "14px",
fontSize: "16px",
fontSmoothing: "antialiased",
"::placeholder": {
color: null,

View File

@ -84,9 +84,9 @@ export class PaymentComponent implements OnInit, OnDestroy {
base: {
color: null,
fontFamily:
'"Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif, ' +
'"DM Sans", "Helvetica Neue", Helvetica, Arial, sans-serif, ' +
'"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
fontSize: "14px",
fontSize: "16px",
fontSmoothing: "antialiased",
"::placeholder": {
color: null,

View File

@ -16,8 +16,8 @@ import { VaultItem } from "./vault-item";
import { VaultItemEvent } from "./vault-item-event";
// Fixed manual row height required due to how cdk-virtual-scroll works
export const RowHeight = 65;
export const RowHeightClass = `tw-h-[65px]`;
export const RowHeight = 75.5;
export const RowHeightClass = `tw-h-[75.5px]`;
const MaxSelectionCount = 500;

View File

@ -28,7 +28,7 @@
{{ "updateBrowserDesc" | i18n }}
<a
bitLink
linkType="contrast"
linkType="secondary"
target="_blank"
href="https://browser-update.org/update-browser.html"
rel="noreferrer noopener"
@ -45,7 +45,7 @@
(onClose)="dismissBanner(VisibleVaultBanner.KDFSettings)"
>
{{ "lowKDFIterationsBanner" | i18n }}
<a bitLink linkType="contrast" routerLink="/settings/security/security-keys">
<a bitLink linkType="secondary" routerLink="/settings/security/security-keys">
{{ "changeKDFSettings" | i18n }}
</a>
</bit-banner>
@ -66,7 +66,7 @@
(onClose)="dismissBanner(VisibleVaultBanner.Premium)"
>
{{ "premiumUpgradeUnlockFeatures" | i18n }}
<a bitLink linkType="contrast" routerLink="/settings/subscription/premium">
<a bitLink linkType="secondary" routerLink="/settings/subscription/premium">
{{ "goPremium" | i18n }}
</a>
</bit-banner>

View File

@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 100% 100%">
<text fill="%23FBFBFB" x="50%" y="50%" font-family="\'Open Sans\', \'Helvetica Neue\', Helvetica, Arial, sans-serif"
<text fill="%23FBFBFB" x="50%" y="50%" font-family="\'DM Sans\', \'Helvetica Neue\', Helvetica, Arial, sans-serif"
font-size="18" text-anchor="middle">
Loading...
</text>

View File

@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 100% 100%">
<text fill="%23333333" x="50%" y="50%" font-family="\'Open Sans\', \'Helvetica Neue\', Helvetica, Arial, sans-serif"
<text fill="%23333333" x="50%" y="50%" font-family="\'DM Sans\', \'Helvetica Neue\', Helvetica, Arial, sans-serif"
font-size="18" text-anchor="middle">
Loading...
</text>

View File

@ -1,5 +1,5 @@
html {
font-size: 14px;
font-size: 16px;
}
body {

View File

@ -20,7 +20,7 @@ input[type="search"]::-webkit-search-cancel-button {
-webkit-appearance: -cancel-button;
}
label:not(.form-check-label):not(.btn),
label:not(.form-check-label):not(.btn):not(:has(bit-label)),
label.bold {
font-weight: 600;
@include themify($themes) {
@ -136,12 +136,12 @@ input[type="checkbox"] {
background-color: rgb(var(--color-background));
&:hover {
border-color: rgb(var(--color-primary-500));
border-color: rgb(var(--color-primary-600));
}
&.is-focused {
outline: 0;
border-color: rgb(var(--color-primary-500));
border-color: rgb(var(--color-primary-600));
}
&.is-invalid {

View File

@ -20,7 +20,7 @@ $theme-colors: (
$body-bg: $white;
$body-color: #333333;
$font-family-sans-serif: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif,
$font-family-sans-serif: "DM Sans", "Helvetica Neue", Helvetica, Arial, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
$h1-font-size: 1.7rem;

View File

@ -33,12 +33,10 @@
<button
type="button"
bitSuffix
bitButton
bitIconButton="bwi-clone"
appA11yTitle="{{ 'copyDnsTxtRecord' | i18n }}"
(click)="copyDnsTxt()"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
></button>
</bit-form-field>
<bit-callout

View File

@ -50,8 +50,8 @@ export class MembersComponent extends BaseMembersComponent<ProviderUser> {
dataSource = new MembersTableDataSource();
loading = true;
providerId: string;
rowHeight = 62;
rowHeightClass = `tw-h-[62px]`;
rowHeight = 69;
rowHeightClass = `tw-h-[69px]`;
status: ProviderUserStatusType = null;
userStatusType = ProviderUserStatusType;

View File

@ -50,7 +50,7 @@
<a
href="https://bitwarden.com/contact/"
bitLink
linkType="contrast"
linkType="secondary"
target="_blank"
rel="noreferrer"
>

View File

@ -126,7 +126,7 @@
/>
<button
bitSuffix
bitButton
bitLink
[disabled]="!enableTestKeyConnector"
type="button"
(click)="validateKeyConnectorUrl()"
@ -353,14 +353,12 @@
<bit-label>{{ "spMetadataUrl" | i18n }}</bit-label>
<input bitInput disabled [value]="spMetadataUrl" />
<button
bitButton
bitIconButton="bwi-external-link"
bitSuffix
type="button"
[appLaunchClick]="spMetadataUrl"
[appA11yTitle]="'launch' | i18n"
>
<i class="bwi bwi-lg bwi-external-link" aria-hidden="true"></i>
</button>
></button>
<button
bitIconButton="bwi-clone"
bitSuffix

View File

@ -1,89 +1,8 @@
@font-face {
font-family: "Open Sans";
font-style: italic;
font-weight: 300;
font-display: auto;
src: url(webfonts/Open_Sans-italic-300.woff) format("woff");
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: "Open Sans";
font-style: italic;
font-weight: 400;
font-display: auto;
src: url(webfonts/Open_Sans-italic-400.woff) format("woff");
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: "Open Sans";
font-style: italic;
font-weight: 600;
font-display: auto;
src: url(webfonts/Open_Sans-italic-600.woff) format("woff");
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: "Open Sans";
font-style: italic;
font-weight: 700;
font-display: auto;
src: url(webfonts/Open_Sans-italic-700.woff) format("woff");
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: "Open Sans";
font-style: italic;
font-weight: 800;
font-display: auto;
src: url(webfonts/Open_Sans-italic-800.woff) format("woff");
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: "Open Sans";
font-style: normal;
font-weight: 300;
font-display: auto;
src: url(webfonts/Open_Sans-normal-300.woff) format("woff");
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: "Open Sans";
font-style: normal;
font-weight: 400;
font-display: auto;
src: url(webfonts/Open_Sans-normal-400.woff) format("woff");
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: "Open Sans";
font-style: normal;
font-weight: 600;
font-display: auto;
src: url(webfonts/Open_Sans-normal-600.woff) format("woff");
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: "Open Sans";
font-style: normal;
font-weight: 700;
font-display: auto;
src: url(webfonts/Open_Sans-normal-700.woff) format("woff");
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: "Open Sans";
font-style: normal;
font-weight: 800;
font-display: auto;
src: url(webfonts/Open_Sans-normal-800.woff) format("woff");
unicode-range: U+0-10FFFF;
font-family: "DM Sans";
src:
url("webfonts/dm-sans.woff2") format("woff2 supports variations"),
url("webfonts/dm-sans.woff2") format("woff2-variations");
font-display: swap;
font-weight: 100 900;
}

Binary file not shown.

View File

@ -116,7 +116,7 @@ export class AvatarComponent implements OnChanges {
textTag.setAttribute("fill", Utils.pickTextColorBasedOnBgColor(color, 135, true));
textTag.setAttribute(
"font-family",
'"Open Sans","Helvetica Neue",Helvetica,Arial,' +
'"DM Sans","Helvetica Neue",Helvetica,Arial,' +
'sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"',
);
// Warning do not use innerHTML here, characters are user provided

View File

@ -5,21 +5,25 @@ import { FocusableElement } from "../shared/focusable-element";
export type BadgeVariant = "primary" | "secondary" | "success" | "danger" | "warning" | "info";
const styles: Record<BadgeVariant, string[]> = {
primary: ["tw-bg-primary-600"],
secondary: ["tw-bg-text-muted"],
success: ["tw-bg-success-600"],
danger: ["tw-bg-danger-600"],
warning: ["tw-bg-warning-600"],
info: ["tw-bg-info-600"],
primary: ["tw-bg-primary-100", "tw-border-primary-700", "!tw-text-primary-700"],
secondary: ["tw-bg-secondary-100", "tw-border-secondary-700", "!tw-text-secondary-700"],
success: ["tw-bg-success-100", "tw-border-success-700", "!tw-text-success-700"],
danger: ["tw-bg-danger-100", "tw-border-danger-700", "!tw-text-danger-700"],
warning: ["tw-bg-warning-100", "tw-border-warning-700", "!tw-text-warning-700"],
info: ["tw-bg-info-100", "tw-border-info-700", "!tw-text-info-700"],
};
const hoverStyles: Record<BadgeVariant, string[]> = {
primary: ["hover:tw-bg-primary-700"],
secondary: ["hover:tw-bg-secondary-700"],
success: ["hover:tw-bg-success-700"],
danger: ["hover:tw-bg-danger-700"],
warning: ["hover:tw-bg-warning-700"],
info: ["hover:tw-bg-info-700"],
primary: ["hover:tw-bg-primary-600", "hover:tw-border-primary-600", "hover:!tw-text-contrast"],
secondary: [
"hover:tw-bg-secondary-600",
"hover:tw-border-secondary-600",
"hover:!tw-text-contrast",
],
success: ["hover:tw-bg-success-600", "hover:tw-border-success-600", "hover:!tw-text-contrast"],
danger: ["hover:tw-bg-danger-600", "hover:tw-border-danger-600", "hover:!tw-text-contrast"],
warning: ["hover:tw-bg-warning-600", "hover:tw-border-warning-600", "hover:!tw-text-black"],
info: ["hover:tw-bg-info-600", "hover:tw-border-info-600", "hover:!tw-text-black"],
};
@Directive({
@ -30,22 +34,29 @@ export class BadgeDirective implements FocusableElement {
@HostBinding("class") get classList() {
return [
"tw-inline-block",
"tw-py-0.5",
"tw-px-1.5",
"tw-font-bold",
"tw-py-1",
"tw-px-2",
"tw-font-medium",
"tw-text-center",
"tw-align-text-top",
"!tw-text-contrast",
"tw-rounded",
"tw-border-none",
"tw-rounded-full",
"tw-border-[0.5px]",
"tw-border-solid",
"tw-box-border",
"tw-whitespace-nowrap",
"tw-text-xs",
"hover:tw-no-underline",
"focus:tw-outline-none",
"focus:tw-ring",
"focus:tw-ring-offset-2",
"focus:tw-ring-primary-700",
"focus-visible:tw-outline-none",
"focus-visible:tw-ring-2",
"focus-visible:tw-ring-offset-2",
"focus-visible:tw-ring-primary-600",
"disabled:tw-bg-secondary-300",
"disabled:hover:tw-bg-secondary-300",
"disabled:tw-border-secondary-300",
"disabled:hover:tw-border-secondary-300",
"disabled:!tw-text-muted",
"disabled:hover:!tw-text-muted",
"disabled:tw-cursor-not-allowed",
]
.concat(styles[this.variant])
.concat(this.hasHoverEffects ? hoverStyles[this.variant] : [])

View File

@ -10,12 +10,17 @@ import { BadgeModule } from "@bitwarden/components";
# Badge
The Badge directive can be used on a `<span>` (non clickable events), or an `<a>` or `<button>` tag
for interactive events. The Focus and Hover states only apply to badges used for interactive events.
Badges are primarily used as labels, counters, and small buttons.
Typically Badges are only used with text set to `text-xs`. If additional sizes are needed, the
component configurations may be reviewed and adjusted.
The Badge directive can be used on a `<span>` (non clickable events), or an `<a>` or `<button>` tag
for interactive events. The Focus and Hover states only apply to badges used for interactive events.
The `disabled` state only applies to buttons.
The story below uses the `<button>` element to demonstrate all the possible states.
<Primary />
<Controls />

View File

@ -26,10 +26,49 @@ export default {
type Story = StoryObj<BadgeDirective>;
export const Variants: Story = {
render: (args) => ({
props: args,
template: /*html*/ `
<span class="tw-text-main tw-mx-1">Default</span>
<button class="tw-mx-1" bitBadge variant="primary" [truncate]="truncate">Primary</button>
<button class="tw-mx-1" bitBadge variant="secondary" [truncate]="truncate">Secondary</button>
<button class="tw-mx-1" bitBadge variant="success" [truncate]="truncate">Success</button>
<button class="tw-mx-1" bitBadge variant="danger" [truncate]="truncate">Danger</button>
<button class="tw-mx-1" bitBadge variant="warning" [truncate]="truncate">Warning</button>
<button class="tw-mx-1" bitBadge variant="info" [truncate]="truncate">Info</button>
<br/><br/>
<span class="tw-text-main tw-mx-1">Hover</span>
<button class="tw-mx-1 tw-test-hover" bitBadge variant="primary" [truncate]="truncate">Primary</button>
<button class="tw-mx-1 tw-test-hover" bitBadge variant="secondary" [truncate]="truncate">Secondary</button>
<button class="tw-mx-1 tw-test-hover" bitBadge variant="success" [truncate]="truncate">Success</button>
<button class="tw-mx-1 tw-test-hover" bitBadge variant="danger" [truncate]="truncate">Danger</button>
<button class="tw-mx-1 tw-test-hover" bitBadge variant="warning" [truncate]="truncate">Warning</button>
<button class="tw-mx-1 tw-test-hover" bitBadge variant="info" [truncate]="truncate">Info</button>
<br/><br/>
<span class="tw-text-main tw-mx-1">Focus Visible</span>
<button class="tw-mx-1 tw-test-focus-visible" bitBadge variant="primary" [truncate]="truncate">Primary</button>
<button class="tw-mx-1 tw-test-focus-visible" bitBadge variant="secondary" [truncate]="truncate">Secondary</button>
<button class="tw-mx-1 tw-test-focus-visible" bitBadge variant="success" [truncate]="truncate">Success</button>
<button class="tw-mx-1 tw-test-focus-visible" bitBadge variant="danger" [truncate]="truncate">Danger</button>
<button class="tw-mx-1 tw-test-focus-visible" bitBadge variant="warning" [truncate]="truncate">Warning</button>
<button class="tw-mx-1 tw-test-focus-visible" bitBadge variant="info" [truncate]="truncate">Info</button>
<br/><br/>
<span class="tw-text-main tw-mx-1">Disabled</span>
<button disabled class="tw-mx-1" bitBadge variant="primary" [truncate]="truncate">Primary</button>
<button disabled class="tw-mx-1" bitBadge variant="secondary" [truncate]="truncate">Secondary</button>
<button disabled class="tw-mx-1" bitBadge variant="success" [truncate]="truncate">Success</button>
<button disabled class="tw-mx-1" bitBadge variant="danger" [truncate]="truncate">Danger</button>
<button disabled class="tw-mx-1" bitBadge variant="warning" [truncate]="truncate">Warning</button>
<button disabled class="tw-mx-1" bitBadge variant="info" [truncate]="truncate">Info</button>
`,
}),
};
export const Primary: Story = {
render: (args) => ({
props: args,
template: `
template: /*html*/ `
<span class="tw-text-main">Span </span><span bitBadge [variant]="variant" [truncate]="truncate">Badge containing lengthy text</span>
<br /><br />
<span class="tw-text-main">Link </span><a href="#" bitBadge [variant]="variant" [truncate]="truncate">Badge</a>

View File

@ -1,18 +1,21 @@
<div
class="tw-flex tw-items-center tw-gap-2 tw-p-2 tw-pl-4 tw-text-contrast tw-border-transparent tw-bg-clip-padding tw-border-solid tw-border-b tw-border-0"
class="tw-flex tw-items-center tw-gap-2 tw-p-2 tw-pl-4 tw-text-main tw-border-transparent tw-bg-clip-padding tw-border-solid tw-border-b tw-border-0"
[ngClass]="bannerClass"
[attr.role]="useAlertRole ? 'status' : null"
[attr.aria-live]="useAlertRole ? 'polite' : null"
>
<i class="bwi tw-align-middle tw-text-base" [ngClass]="icon" *ngIf="icon" aria-hidden="true"></i>
<span class="tw-grow tw-text-base">
<!-- Overriding focus-visible color for link buttons for a11y against colored background -->
<span class="tw-grow tw-text-base [&>button[bitlink]:focus-visible:before]:!tw-ring-text-main">
<ng-content></ng-content>
</span>
<!-- Overriding hover and focus-visible colors for a11y against colored background -->
<button
*ngIf="showClose"
class="hover:tw-border-text-main focus-visible:before:tw-ring-text-main"
type="button"
bitIconButton="bwi-close"
buttonType="contrast"
buttonType="main"
size="default"
(click)="onClose.emit()"
[attr.title]="'close' | i18n"

View File

@ -28,13 +28,13 @@ export class BannerComponent implements OnInit {
get bannerClass() {
switch (this.bannerType) {
case "danger":
return "tw-bg-danger-600";
return "tw-bg-danger-100 tw-border-b-danger-700";
case "info":
return "tw-bg-info-600";
return "tw-bg-info-100 tw-border-b-info-700";
case "premium":
return "tw-bg-success-600";
return "tw-bg-success-100 tw-border-b-success-700";
case "warning":
return "tw-bg-warning-600";
return "tw-bg-warning-100 tw-border-b-warning-700";
}
}
}

View File

@ -15,7 +15,8 @@ persist across all pages a user navigates to.
- Use banners sparingly, as they can feel intrusive to the user if they appear unexpectedly. Their
effectiveness may decrease if too many are used.
- Avoid stacking multiple banners.
- Banners support a button link (text button).
- Banners can contain a button or anchor that uses the `bitLink` directive with
`linkType="secondary"`.
<Primary />

View File

@ -53,7 +53,7 @@ export const Premium: Story = {
template: `
<bit-banner [bannerType]="bannerType" (onClose)="onClose($event)" [showClose]=showClose>
Content Really Long Text Lorem Ipsum Ipsum Ipsum
<button bitLink linkType="contrast">Button</button>
<button bitLink linkType="secondary">Button</button>
</bit-banner>
`,
}),

View File

@ -27,57 +27,6 @@ describe("Button", () => {
linkDebugElement = fixture.debugElement.query(By.css("a"));
}));
it("should apply classes based on type", () => {
testAppComponent.buttonType = "primary";
fixture.detectChanges();
expect(buttonDebugElement.nativeElement.classList.contains("tw-bg-primary-600")).toBe(true);
expect(linkDebugElement.nativeElement.classList.contains("tw-bg-primary-600")).toBe(true);
testAppComponent.buttonType = "secondary";
fixture.detectChanges();
expect(buttonDebugElement.nativeElement.classList.contains("tw-border-text-muted")).toBe(true);
expect(linkDebugElement.nativeElement.classList.contains("tw-border-text-muted")).toBe(true);
testAppComponent.buttonType = "danger";
fixture.detectChanges();
expect(buttonDebugElement.nativeElement.classList.contains("tw-border-danger-600")).toBe(true);
expect(linkDebugElement.nativeElement.classList.contains("tw-border-danger-600")).toBe(true);
testAppComponent.buttonType = "unstyled";
fixture.detectChanges();
expect(
Array.from(buttonDebugElement.nativeElement.classList).some((klass: string) =>
klass.startsWith("tw-bg"),
),
).toBe(false);
expect(
Array.from(linkDebugElement.nativeElement.classList).some((klass: string) =>
klass.startsWith("tw-bg"),
),
).toBe(false);
testAppComponent.buttonType = null;
fixture.detectChanges();
expect(buttonDebugElement.nativeElement.classList.contains("tw-border-text-muted")).toBe(true);
expect(linkDebugElement.nativeElement.classList.contains("tw-border-text-muted")).toBe(true);
});
it("should apply block when true and inline-block when false", () => {
testAppComponent.block = true;
fixture.detectChanges();
expect(buttonDebugElement.nativeElement.classList.contains("tw-block")).toBe(true);
expect(linkDebugElement.nativeElement.classList.contains("tw-block")).toBe(true);
expect(buttonDebugElement.nativeElement.classList.contains("tw-inline-block")).toBe(false);
expect(linkDebugElement.nativeElement.classList.contains("tw-inline-block")).toBe(false);
testAppComponent.block = false;
fixture.detectChanges();
expect(buttonDebugElement.nativeElement.classList.contains("tw-inline-block")).toBe(true);
expect(linkDebugElement.nativeElement.classList.contains("tw-inline-block")).toBe(true);
expect(buttonDebugElement.nativeElement.classList.contains("tw-block")).toBe(false);
expect(linkDebugElement.nativeElement.classList.contains("tw-block")).toBe(false);
});
it("should not be disabled when loading and disabled are false", () => {
testAppComponent.loading = false;
testAppComponent.disabled = false;

View File

@ -4,9 +4,9 @@ import { Input, HostBinding, Component } from "@angular/core";
import { ButtonLikeAbstraction, ButtonType } from "../shared/button-like.abstraction";
const focusRing = [
"focus-visible:tw-ring",
"focus-visible:tw-ring-2",
"focus-visible:tw-ring-offset-2",
"focus-visible:tw-ring-primary-700",
"focus-visible:tw-ring-primary-600",
"focus-visible:tw-z-10",
];
@ -17,24 +17,15 @@ const buttonStyles: Record<ButtonType, string[]> = {
"!tw-text-contrast",
"hover:tw-bg-primary-700",
"hover:tw-border-primary-700",
"disabled:tw-bg-primary-600/60",
"disabled:tw-border-primary-600/60",
"disabled:!tw-text-contrast/60",
"disabled:tw-bg-clip-padding",
"disabled:tw-cursor-not-allowed",
...focusRing,
],
secondary: [
"tw-bg-transparent",
"tw-border-text-muted",
"!tw-text-muted",
"hover:tw-bg-text-muted",
"hover:tw-border-text-muted",
"tw-border-primary-600",
"!tw-text-primary-600",
"hover:tw-bg-primary-600",
"hover:tw-border-primary-600",
"hover:!tw-text-contrast",
"disabled:tw-bg-transparent",
"disabled:tw-border-text-muted/60",
"disabled:!tw-text-muted/60",
"disabled:tw-cursor-not-allowed",
...focusRing,
],
danger: [
@ -44,10 +35,6 @@ const buttonStyles: Record<ButtonType, string[]> = {
"hover:tw-bg-danger-600",
"hover:tw-border-danger-600",
"hover:!tw-text-contrast",
"disabled:tw-bg-transparent",
"disabled:tw-border-danger-600/60",
"disabled:!tw-text-danger/60",
"disabled:tw-cursor-not-allowed",
...focusRing,
],
unstyled: [],
@ -64,14 +51,22 @@ export class ButtonComponent implements ButtonLikeAbstraction {
"tw-font-semibold",
"tw-py-1.5",
"tw-px-3",
"tw-rounded",
"tw-rounded-full",
"tw-transition",
"tw-border",
"tw-border-2",
"tw-border-solid",
"tw-text-center",
"tw-no-underline",
"hover:tw-no-underline",
"focus:tw-outline-none",
"disabled:tw-bg-secondary-300",
"disabled:hover:tw-bg-secondary-300",
"disabled:tw-border-secondary-300",
"disabled:hover:tw-border-secondary-300",
"disabled:!tw-text-muted",
"disabled:hover:!tw-text-muted",
"disabled:tw-cursor-not-allowed",
"disabled:hover:tw-no-underline",
]
.concat(this.block ? ["tw-w-full", "tw-block"] : ["tw-inline-block"])
.concat(buttonStyles[this.buttonType ?? "secondary"]);
@ -99,8 +94,4 @@ export class ButtonComponent implements ButtonLikeAbstraction {
@Input() loading = false;
@Input() disabled = false;
setButtonType(value: "primary" | "secondary" | "danger" | "unstyled") {
this.buttonType = value;
}
}

View File

@ -64,9 +64,6 @@ Use the danger styling only in settings when the user may preform a permanent ac
## Disabled UI
Both the disabled and loading states use the default states color with a 60% opacity or
`tw-opacity-60`.
<Story of={stories.Disabled} />
## Block

View File

@ -23,9 +23,21 @@ type Story = StoryObj<ButtonComponent>;
export const Primary: Story = {
render: (args) => ({
props: args,
template: `
template: /*html*/ `
<div class="tw-flex tw-gap-4 tw-mb-6">
<button bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [block]="block">Button</button>
<a bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [block]="block" href="#" class="tw-ml-2">Link</a>
<button bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [block]="block" class="tw-test-hover">Button:hover</button>
<button bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [block]="block" class="tw-test-focus-visible">Button:focus-visible</button>
<button bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [block]="block" class="tw-test-hover tw-test-focus-visible">Button:hover:focus-visible</button>
<button bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [block]="block" class="tw-test-active">Button:active</button>
</div>
<div class="tw-flex tw-gap-4">
<a href="#" bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [block]="block">Anchor</a>
<a href="#" bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [block]="block" class="tw-test-hover">Anchor:hover</a>
<a href="#" bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [block]="block" class="tw-test-focus-visible">Anchor:focus-visible</a>
<a href="#" bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [block]="block" class="tw-test-hover tw-test-focus-visible">Anchor:hover:focus-visible</a>
<a href="#" bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [block]="block" class="tw-test-active">Anchor:active</a>
</div>
`,
}),
args: {

View File

@ -1,16 +1,13 @@
<aside
class="tw-mb-4 tw-box-border tw-rounded tw-border tw-border-l-8 tw-border-solid tw-border-secondary-300 tw-bg-background-alt tw-px-5 tw-py-3 tw-leading-5 tw-text-main"
class="tw-mb-4 tw-box-border tw-rounded-lg tw-border tw-border-l-4 tw-border-solid tw-bg-background tw-pl-3 tw-pr-2 tw-py-2 tw-leading-5 tw-text-main"
[ngClass]="calloutClass"
[attr.aria-labelledby]="titleId"
>
<header
id="{{ titleId }}"
class="tw-mb-2 tw-mt-0 tw-text-base tw-font-bold tw-uppercase"
[ngClass]="headerClass"
*ngIf="title"
>
<i class="bwi {{ icon }}" *ngIf="icon" aria-hidden="true"></i>
<header id="{{ titleId }}" class="tw-mb-1 tw-mt-0 tw-text-base tw-font-semibold" *ngIf="title">
<i class="bwi" [ngClass]="[icon, headerClass]" *ngIf="icon" aria-hidden="true"></i>
{{ title }}
</header>
<ng-content></ng-content>
<div bitTypography="body2">
<ng-content></ng-content>
</div>
</aside>

View File

@ -12,7 +12,7 @@ describe("Callout", () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [CalloutComponent],
imports: [CalloutComponent],
providers: [
{
provide: I18nService,

View File

@ -2,6 +2,9 @@ import { Component, Input, OnInit } from "@angular/core";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { SharedModule } from "../shared";
import { TypographyModule } from "../typography";
export type CalloutTypes = "success" | "info" | "warning" | "danger";
const defaultIcon: Record<CalloutTypes, string> = {
@ -22,6 +25,8 @@ let nextId = 0;
@Component({
selector: "bit-callout",
templateUrl: "callout.component.html",
standalone: true,
imports: [SharedModule, TypographyModule],
})
export class CalloutComponent implements OnInit {
@Input() type: CalloutTypes = "info";
@ -42,13 +47,13 @@ export class CalloutComponent implements OnInit {
get calloutClass() {
switch (this.type) {
case "danger":
return "tw-border-l-danger-600";
return "tw-border-danger-600";
case "info":
return "tw-border-l-info-600";
return "tw-border-info-600";
case "success":
return "tw-border-l-success-600";
return "tw-border-success-600";
case "warning":
return "tw-border-l-warning-600";
return "tw-border-warning-600";
}
}

View File

@ -2,6 +2,10 @@ import { Meta, Story, Primary, Controls } from "@storybook/addon-docs";
import * as stories from "./callout.stories";
```ts
import { CalloutModule } from "@bitwarden/components";
```
<Meta of={stories} />
# Callouts

View File

@ -1,11 +1,9 @@
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { CalloutComponent } from "./callout.component";
@NgModule({
imports: [CommonModule],
imports: [CalloutComponent],
exports: [CalloutComponent],
declarations: [CalloutComponent],
})
export class CalloutModule {}

View File

@ -9,7 +9,7 @@ import { ChangeDetectionStrategy, Component } from "@angular/core";
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
class:
"tw-box-border tw-block tw-bg-background tw-text-main tw-border-solid tw-border-b tw-border-0 tw-border-b-secondary-300 [&:not(bit-layout_*)]:tw-rounded-lg tw-py-4 tw-px-3",
"tw-box-border tw-block tw-bg-background tw-text-main tw-border-solid tw-border-b tw-border-0 tw-border-b-secondary-300 [&:not(bit-layout_*)]:tw-rounded-lg [&:not(bit-layout_*)]:tw-border-b-shadow tw-py-4 tw-px-3",
},
})
export class CardComponent {}

View File

@ -17,12 +17,13 @@ export class CheckboxComponent implements BitFormControlAbstraction {
"tw-transition",
"tw-cursor-pointer",
"tw-inline-block",
"tw-align-sub",
"tw-rounded",
"tw-border",
"tw-border-solid",
"tw-border-secondary-600",
"tw-h-3.5",
"tw-w-3.5",
"tw-border-secondary-500",
"tw-h-5",
"tw-w-5",
"tw-mr-1.5",
"tw-bottom-[-1px]", // Fix checkbox looking off-center
"tw-flex-none", // Flexbox fix for bit-form-control
@ -35,13 +36,16 @@ export class CheckboxComponent implements BitFormControlAbstraction {
"hover:tw-border-2",
"[&>label]:tw-border-2",
"focus-visible:tw-ring-2",
"focus-visible:tw-ring-offset-2",
"focus-visible:tw-ring-primary-700",
// if it exists, the parent form control handles focus
"[&:not(bit-form-control_*)]:focus-visible:tw-ring-2",
"[&:not(bit-form-control_*)]:focus-visible:tw-ring-offset-2",
"[&:not(bit-form-control_*)]:focus-visible:tw-ring-primary-600",
"disabled:tw-cursor-auto",
"disabled:tw-border",
"disabled:hover:tw-border",
"disabled:tw-bg-secondary-100",
"disabled:hover:tw-bg-secondary-100",
"checked:tw-bg-primary-600",
"checked:tw-border-primary-600",
@ -53,6 +57,7 @@ export class CheckboxComponent implements BitFormControlAbstraction {
"checked:before:tw-mask-position-[center]",
"checked:before:tw-mask-repeat-[no-repeat]",
"checked:disabled:tw-border-secondary-100",
"checked:disabled:hover:tw-border-secondary-100",
"checked:disabled:tw-bg-secondary-100",
"checked:disabled:before:tw-bg-text-muted",
@ -78,11 +83,11 @@ export class CheckboxComponent implements BitFormControlAbstraction {
@HostBinding("style.--mask-image")
protected maskImage =
`url('data:image/svg+xml,%3Csvg class="svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="8" height="8" viewBox="0 0 10 10"%3E%3Cpath d="M0.5 6.2L2.9 8.6L9.5 1.4" fill="none" stroke="white" stroke-width="2"%3E%3C/path%3E%3C/svg%3E')`;
`url('data:image/svg+xml,%3Csvg class="svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12" viewBox="0 0 10 10"%3E%3Cpath d="M0.5 6.2L2.9 8.6L9.5 1.4" fill="none" stroke="white" stroke-width="2"%3E%3C/path%3E%3C/svg%3E')`;
@HostBinding("style.--indeterminate-mask-image")
protected indeterminateImage =
`url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="none" viewBox="0 0 13 13"%3E%3Cpath stroke="%23fff" stroke-width="2" d="M2.5 6.5h8"/%3E%3C/svg%3E%0A')`;
`url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 13 13"%3E%3Cpath stroke="%23fff" stroke-width="2" d="M2.5 6.5h8"/%3E%3C/svg%3E%0A')`;
@HostBinding()
@Input()

View File

@ -11,12 +11,14 @@ import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
import { I18nService } from "@bitwarden/common/src/platform/abstractions/i18n.service";
import { BadgeModule } from "../badge";
import { FormControlModule } from "../form-control";
import { TableModule } from "../table";
import { I18nMockService } from "../utils/i18n-mock.service";
import { CheckboxModule } from "./checkbox.module";
const template = `
const template = /*html*/ `
<form [formGroup]="formObj">
<bit-form-control>
<input type="checkbox" bitCheckbox formControlName="checkbox" />
@ -54,7 +56,14 @@ export default {
decorators: [
moduleMetadata({
declarations: [ExampleComponent],
imports: [FormsModule, ReactiveFormsModule, FormControlModule, CheckboxModule],
imports: [
FormsModule,
ReactiveFormsModule,
FormControlModule,
CheckboxModule,
TableModule,
BadgeModule,
],
providers: [
{
provide: I18nService,
@ -82,7 +91,10 @@ type Story = StoryObj<ExampleComponent>;
export const Default: Story = {
render: (args) => ({
props: args,
template: `<app-example [checked]="checked" [disabled]="disabled"></app-example>`,
template: /*html*/ `
<app-example></app-example>
<app-example [checked]="true"></app-example>
`,
}),
parameters: {
docs: {
@ -91,9 +103,39 @@ export const Default: Story = {
},
},
},
args: {
checked: false,
disabled: false,
};
export const LongLabel: Story = {
render: () => ({
props: {
formObj: new FormGroup({
checkbox: new FormControl(false),
}),
},
template: /*html*/ `
<form [formGroup]="formObj" class="tw-w-96">
<bit-form-control>
<input type="checkbox" bitCheckbox formControlName="checkbox">
<bit-label>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur iaculis consequat enim vitae elementum.
Ut non odio est. </bit-label>
</bit-form-control>
<bit-form-control>
<input type="checkbox" bitCheckbox formControlName="checkbox">
<bit-label>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur iaculis consequat enim vitae elementum.
Ut non odio est.
<span slot="end" bitBadge variant="success">Premium</span>
</bit-label>
</bit-form-control>
</form>
`,
}),
parameters: {
docs: {
source: {
code: template,
},
},
},
};
@ -104,7 +146,7 @@ export const Hint: Story = {
checkbox: new FormControl(false),
}),
},
template: `
template: /*html*/ `
<form [formGroup]="formObj">
<bit-form-control>
<input type="checkbox" bitCheckbox formControlName="checkbox" />
@ -131,20 +173,37 @@ export const Hint: Story = {
},
};
export const Disabled: Story = {
render: (args) => ({
props: args,
template: /*html*/ `
<app-example [disabled]="true"></app-example>
<app-example [checked]="true" [disabled]="true"></app-example>
`,
}),
parameters: {
docs: {
source: {
code: template,
},
},
},
};
export const Custom: Story = {
render: (args) => ({
props: args,
template: `
template: /*html*/ `
<div class="tw-flex tw-flex-col tw-w-32">
<label class="tw-text-main tw-flex tw-bg-secondary-300 tw-p-2 tw-items-baseline">
<label class="tw-text-main tw-flex tw-bg-secondary-300 tw-p-2">
A-Z
<input class="tw-ml-auto focus-visible:tw-ring-offset-secondary-300" type="checkbox" bitCheckbox />
</label>
<label class="tw-text-main tw-flex tw-bg-secondary-300 tw-p-2 tw-items-baseline">
<label class="tw-text-main tw-flex tw-bg-secondary-300 tw-p-2">
a-z
<input class="tw-ml-auto focus-visible:tw-ring-offset-secondary-300" type="checkbox" bitCheckbox />
</label>
<label class="tw-text-main tw-flex tw-bg-secondary-300 tw-p-2 tw-items-baseline">
<label class="tw-text-main tw-flex tw-bg-secondary-300 tw-p-2">
0-9
<input class="tw-ml-auto focus-visible:tw-ring-offset-secondary-300" type="checkbox" bitCheckbox />
</label>
@ -156,8 +215,51 @@ export const Custom: Story = {
export const Indeterminate: Story = {
render: (args) => ({
props: args,
template: `
template: /*html*/ `
<input type="checkbox" bitCheckbox [indeterminate]="true">
`,
}),
};
export const InTableRow: Story = {
render: () => ({
template: /*html*/ `
<bit-table>
<ng-container header>
<tr>
<th bitCell>
<input
type="checkbox"
bitCheckbox
id="checkAll"
class="tw-mr-2"
/>
<label for="checkAll" class="tw-mb-0">
All
</label>
</th>
<th bitCell>
Foo
</th>
<th bitCell>
Bar
</th>
</tr>
</ng-container>
<ng-template body>
<tr bitRow>
<td bitCell>
<input
type="checkbox"
bitCheckbox
id="checkOne"
/>
</td>
<td bitCell>Lorem</td>
<td bitCell>Ipsum</td>
</tr>
</ng-template>
</bit-table>
`,
}),
};

View File

@ -1,25 +1,30 @@
<div
bitTypography="body2"
class="tw-inline-flex tw-items-center tw-rounded-full tw-border-solid tw-border tw-border-text-muted"
[ngClass]="[
selectedOption
? 'tw-bg-text-muted tw-text-contrast tw-gap-1'
: 'tw-bg-transparent tw-text-muted tw-gap-1.5',
focusVisibleWithin() ? 'tw-ring-2 tw-ring-primary-500 tw-ring-offset-1' : '',
fullWidth ? 'tw-w-full' : 'tw-max-w-52',
]"
class="tw-inline-flex tw-items-center tw-rounded-full tw-w-full tw-border-solid tw-border tw-gap-1.5 tw-group/chip-select"
[ngClass]="{
'tw-bg-text-muted hover:tw-bg-secondary-700 tw-text-contrast hover:!tw-border-secondary-700':
selectedOption && !disabled,
'tw-bg-transparent hover:tw-border-secondary-700 !tw-text-muted hover:tw-bg-secondary-100':
!selectedOption && !disabled,
'tw-bg-secondary-300 tw-text-muted tw-border-transparent': disabled,
'tw-border-text-muted': !disabled,
'tw-ring-2 tw-ring-primary-600 tw-ring-offset-1': focusVisibleWithin(),
}"
>
<!-- Primary button -->
<button
type="button"
class="fvw-target tw-inline-flex tw-gap-1.5 tw-items-center tw-justify-between tw-bg-transparent hover:tw-bg-transparent tw-border-none tw-outline-none tw-w-full tw-py-1 tw-pl-3 last:tw-pr-3 tw-truncate tw-text-[inherit]"
class="fvw-target tw-inline-flex tw-gap-1.5 tw-items-center tw-justify-between tw-bg-transparent hover:tw-bg-transparent tw-border-none tw-outline-none tw-w-full tw-py-1 tw-pl-3 last:tw-pr-3 [&:not(:last-child)]:tw-pr-0 tw-truncate tw-text-[color:inherit] tw-text-[length:inherit]"
[ngClass]="{
'tw-cursor-not-allowed': disabled,
'group-hover/chip-select:tw-text-secondary-700': !selectedOption && !disabled,
}"
[bitMenuTriggerFor]="menu"
[disabled]="disabled"
[title]="label"
#menuTrigger="menuTrigger"
(click)="setMenuWidth()"
#chipSelectButton
>
<span class="tw-inline-flex tw-items-center tw-gap-1.5 tw-truncate">
<i class="bwi !tw-text-[inherit]" [ngClass]="icon"></i>
@ -27,7 +32,7 @@
</span>
<i
*ngIf="!selectedOption"
class="bwi"
class="bwi tw-mt-0.5"
[ngClass]="menuTrigger.isOpen ? 'bwi-angle-up' : 'bwi-angle-down'"
></i>
</button>
@ -38,7 +43,7 @@
type="button"
[attr.aria-label]="'removeItem' | i18n: label"
[disabled]="disabled"
class="tw-bg-transparent hover:tw-bg-transparent tw-outline-none tw-rounded-full tw-p-1 tw-my-1 tw-mr-1 tw-text-[inherit] tw-border-solid tw-border tw-border-text-muted hover:tw-border-text-contrast hover:disabled:tw-border-transparent tw-aspect-square tw-flex tw-items-center tw-justify-center tw-h-fit focus-visible:tw-ring-2 tw-ring-text-contrast focus-visible:hover:tw-border-transparent"
class="tw-bg-transparent hover:tw-bg-transparent tw-outline-none tw-rounded-full tw-py-0.5 tw-px-1 tw-mr-1 tw-text-[color:inherit] tw-text-[length:inherit] tw-border-solid tw-border tw-border-transparent hover:tw-border-text-contrast hover:disabled:tw-border-transparent tw-flex tw-items-center tw-justify-center focus-visible:tw-ring-2 tw-ring-text-contrast focus-visible:hover:tw-border-transparent"
[ngClass]="{
'tw-cursor-not-allowed': disabled,
}"
@ -48,13 +53,18 @@
</button>
</div>
<bit-menu #menu>
<div *ngIf="renderedOptions" class="tw-max-h-80 tw-min-w-52 tw-max-w-80 tw-text-sm">
<bit-menu #menu (closed)="handleMenuClosed()">
<div
*ngIf="renderedOptions"
class="tw-max-h-80 tw-min-w-32 tw-max-w-80 tw-text-sm"
[ngStyle]="menuWidth && { width: menuWidth + 'px' }"
>
<ng-container *ngIf="getParent(renderedOptions) as parent">
<button
type="button"
bitMenuItem
(click)="viewOption(parent, $event)"
class="tw-text-[length:inherit]"
[title]="'backTo' | i18n: parent.label ?? placeholderText"
>
<i slot="start" class="bwi bwi-angle-left" aria-hidden="true"></i>
@ -66,6 +76,7 @@
bitMenuItem
(click)="selectOption(renderedOptions, $event)"
[title]="'viewItemsIn' | i18n: renderedOptions.label"
class="tw-text-[length:inherit]"
>
<i slot="start" class="bwi bwi-list" aria-hidden="true"></i>
{{ "viewItemsIn" | i18n: renderedOptions.label }}
@ -79,6 +90,7 @@
(click)="option.children?.length ? viewOption(option, $event) : selectOption(option, $event)"
[disabled]="option.disabled"
[title]="option.label"
class="tw-text-[length:inherit]"
[attr.aria-haspopup]="option.children?.length ? 'menu' : null"
>
<i

View File

@ -2,6 +2,8 @@ import {
AfterViewInit,
Component,
DestroyRef,
ElementRef,
HostBinding,
HostListener,
Input,
QueryList,
@ -44,6 +46,7 @@ export type ChipSelectOption<T> = Option<T> & {
export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, AfterViewInit {
@ViewChild(MenuComponent) menu: MenuComponent;
@ViewChildren(MenuItemDirective) menuItems: QueryList<MenuItemDirective>;
@ViewChild("chipSelectButton") chipSelectButton: ElementRef<HTMLButtonElement>;
/** Text to show when there is no selected option */
@Input({ required: true }) placeholderText: string;
@ -81,6 +84,11 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
this.focusVisibleWithin.set(false);
}
@HostBinding("class")
get classList() {
return ["tw-inline-block", this.fullWidth ? "tw-w-full" : "tw-max-w-52"];
}
private destroyRef = inject(DestroyRef);
/** Tree constructed from `this.options` */
@ -92,6 +100,12 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
/** The option that is currently selected by the user */
protected selectedOption: ChipSelectOption<T>;
/**
* The initial calculated width of the menu when it opens, which is used to
* keep the width consistent as the user navigates through submenus
*/
protected menuWidth: number | null = null;
/** The label to show in the chip button */
protected get label(): string {
return this.selectedOption?.label || this.placeholderText;
@ -102,6 +116,24 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
return this.selectedOption?.icon || this.placeholderIcon;
}
/**
* Set the rendered options based on whether or not an option is already selected, so that the correct
* submenu displays.
*/
protected setOrResetRenderedOptions(): void {
this.renderedOptions = this.selectedOption
? this.selectedOption.children?.length > 0
? this.selectedOption
: this.getParent(this.selectedOption)
: this.rootTree;
}
protected handleMenuClosed(): void {
this.setOrResetRenderedOptions();
// reset menu width so that it can be recalculated upon open
this.menuWidth = null;
}
protected selectOption(option: ChipSelectOption<T>, _event: MouseEvent) {
this.selectedOption = option;
this.onChange(option);
@ -179,6 +211,19 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
});
}
/**
* Calculate the width of the menu based on whichever is larger, the chip select width or the width of
* the initially rendered options
*/
protected setMenuWidth() {
const chipWidth = this.chipSelectButton.nativeElement.getBoundingClientRect().width;
const firstMenuItemWidth =
this.menu.menuItems.first.elementRef.nativeElement.getBoundingClientRect().width;
this.menuWidth = Math.max(chipWidth, firstMenuItemWidth);
}
/** Control Value Accessor */
private notifyOnChange?: (value: T) => void;
@ -187,11 +232,7 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
/** Implemented as part of NG_VALUE_ACCESSOR */
writeValue(obj: T): void {
this.selectedOption = this.findOption(this.rootTree, obj);
/** Update the rendered options for next time the menu is opened */
this.renderedOptions = this.selectedOption
? this.getParent(this.selectedOption)
: this.rootTree;
this.setOrResetRenderedOptions();
}
/** Implemented as part of NG_VALUE_ACCESSOR */

View File

@ -35,6 +35,47 @@ export default {
type Story = StoryObj<ChipSelectComponent & { value: any }>;
export const Default: Story = {
render: (args) => ({
props: {
...args,
},
template: /* html */ `
<bit-chip-select
placeholderText="Folder"
placeholderIcon="bwi-folder"
[options]="options"
></bit-chip-select>
<bit-chip-select
placeholderText="Folder"
placeholderIcon="bwi-folder"
[options]="options"
[ngModel]="value"
></bit-chip-select>
`,
}),
args: {
options: [
{
label: "Foo",
value: "foo",
icon: "bwi-folder",
},
{
label: "Bar",
value: "bar",
icon: "bwi-exclamation-triangle tw-text-danger",
},
{
label: "Baz",
value: "baz",
disabled: true,
},
],
value: "foo",
},
};
export const MenuOpen: Story = {
render: (args) => ({
props: {
...args,
@ -122,7 +163,7 @@ export const NestedOptions: Story = {
icon: "bwi-folder",
children: [
{
label: "Foo1",
label: "Foo1 very long name of folder but even longer than you thought",
value: "foo1",
icon: "bwi-folder",
children: [
@ -170,12 +211,17 @@ export const TextOverflow: Story = {
};
export const Disabled: Story = {
...Default,
render: (args) => ({
props: {
...args,
},
template: /* html */ `
<bit-chip-select
placeholderText="Folder"
placeholderIcon="bwi-folder"
[options]="options"
disabled
></bit-chip-select>
<bit-chip-select
placeholderText="Folder"
placeholderIcon="bwi-folder"

View File

@ -78,6 +78,7 @@ export default {
useFactory: () => {
return new I18nMockService({
close: "Close",
loading: "Loading",
});
},
},

View File

@ -1,12 +1,17 @@
<section
class="tw-flex tw-w-full tw-flex-col tw-self-center tw-overflow-hidden tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-text-main"
class="tw-flex tw-w-full tw-flex-col tw-self-center tw-overflow-hidden tw-rounded-xl tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-text-main"
[ngClass]="width"
@fadeIn
>
<header
class="tw-flex tw-justify-between tw-items-center tw-gap-4 tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300 tw-p-4"
>
<h1 bitDialogTitleContainer bitTypography="h3" noMargin class="tw-mb-0 tw-truncate">
<h1
bitDialogTitleContainer
bitTypography="h3"
noMargin
class="tw-text-main tw-mb-0 tw-truncate"
>
{{ title }}
<span *ngIf="subtitle" class="tw-text-muted tw-font-normal tw-text-sm">
{{ subtitle }}
@ -24,15 +29,19 @@
></button>
</header>
<div class="tw-relative tw-flex tw-flex-col tw-overflow-hidden">
<div
class="tw-relative tw-flex tw-flex-col tw-overflow-hidden"
[ngClass]="{
'tw-min-h-60': loading,
}"
>
<div
*ngIf="loading"
class="tw-absolute tw-flex tw-h-full tw-w-full tw-items-center tw-justify-center"
>
<i class="bwi bwi-spinner bwi-spin bwi-3x"></i>
<i class="bwi bwi-spinner bwi-spin bwi-lg" [attr.aria-label]="'loading' | i18n"></i>
</div>
<div
class="tw-pb-8"
[ngClass]="{
'tw-p-4': !disablePadding,
'tw-overflow-y-auto': !loading,
@ -46,7 +55,7 @@
</div>
<footer
class="tw-flex tw-flex-row tw-items-center tw-gap-2 tw-border-0 tw-border-t tw-border-solid tw-border-secondary-300 tw-bg-background-alt tw-p-4"
class="tw-flex tw-flex-row tw-items-center tw-gap-2 tw-border-0 tw-border-t tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-4"
>
<ng-content select="[bitDialogFooter]"></ng-content>
</footer>

View File

@ -75,3 +75,10 @@ loading state.
Use tabs to separate related content within a dialog.
<Story of={stories.TabContent} />
## Background Color
The `background` input can be set to `alt` to change the background color. This is useful for
dialogs that contain multiple card sections.
<Story of={stories.WithCards} />

View File

@ -4,6 +4,10 @@ import * as stories from "./dialog.service.stories";
<Meta title="Component Library/Dialogs" />
```ts
import { DialogModule } from "@bitwarden/components";
```
# Dialog
Dialogs are used throughout the app to help the user focus on a specific action.

View File

@ -1,4 +1,5 @@
import { Component } from "@angular/core";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -144,7 +145,7 @@ export default {
component: StoryDialogComponent,
decorators: [
moduleMetadata({
imports: [ButtonModule, DialogModule, CalloutModule],
imports: [ButtonModule, BrowserAnimationsModule, DialogModule, CalloutModule],
}),
applicationConfig({
providers: [

View File

@ -1,5 +1,5 @@
<div
class="tw-my-4 tw-flex tw-max-h-screen tw-w-96 tw-max-w-90vw tw-flex-col tw-overflow-hidden tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-text-contrast tw-text-main"
class="tw-my-4 tw-flex tw-max-h-screen tw-w-96 tw-max-w-90vw tw-flex-col tw-overflow-hidden tw-rounded-3xl tw-border tw-border-solid tw-border-secondary-300 tw-bg-text-contrast tw-text-main"
@fadeIn
>
<div class="tw-flex tw-flex-col tw-items-center tw-gap-2 tw-px-4 tw-pt-4 tw-text-center">
@ -11,13 +11,15 @@
</ng-template>
<h1
bitDialogTitleContainer
class="tw-mb-0 tw-text-base tw-font-semibold tw-w-full tw-break-words tw-hyphens-auto"
bitTypography="h3"
noMargin
class="tw-w-full tw-text-main tw-break-words tw-hyphens-auto"
>
<ng-content select="[bitDialogTitle]"></ng-content>
</h1>
</div>
<div
class="tw-overflow-y-auto tw-px-4 tw-pb-4 tw-pt-2 tw-text-center tw-text-base tw-break-words tw-hyphens-auto"
class="tw-overflow-y-auto tw-px-4 tw-pb-4 tw-text-center tw-text-base tw-break-words tw-hyphens-auto"
>
<ng-content select="[bitDialogContent]"></ng-content>
</div>

View File

@ -1,5 +1,6 @@
import { DialogModule, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -8,11 +9,8 @@ import { ButtonModule } from "../../button";
import { IconButtonModule } from "../../icon-button";
import { SharedModule } from "../../shared/shared.module";
import { I18nMockService } from "../../utils/i18n-mock.service";
import { DialogModule } from "../dialog.module";
import { DialogService } from "../dialog.service";
import { DialogCloseDirective } from "../directives/dialog-close.directive";
import { DialogTitleContainerDirective } from "../directives/dialog-title-container.directive";
import { SimpleDialogComponent } from "./simple-dialog.component";
interface Animal {
animal: string;
@ -65,13 +63,14 @@ export default {
component: StoryDialogComponent,
decorators: [
moduleMetadata({
declarations: [
StoryDialogContentComponent,
DialogCloseDirective,
DialogTitleContainerDirective,
SimpleDialogComponent,
declarations: [StoryDialogContentComponent],
imports: [
SharedModule,
IconButtonModule,
ButtonModule,
BrowserAnimationsModule,
DialogModule,
],
imports: [SharedModule, IconButtonModule, ButtonModule, DialogModule],
providers: [
DialogService,
{

View File

@ -1,17 +1,17 @@
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
import { ButtonModule } from "../../button";
import { DialogTitleContainerDirective } from "../directives/dialog-title-container.directive";
import { DialogModule } from "../dialog.module";
import { IconDirective, SimpleDialogComponent } from "./simple-dialog.component";
import { SimpleDialogComponent } from "./simple-dialog.component";
export default {
title: "Component Library/Dialogs/Simple Dialog",
component: SimpleDialogComponent,
decorators: [
moduleMetadata({
imports: [ButtonModule],
declarations: [IconDirective, DialogTitleContainerDirective],
imports: [ButtonModule, NoopAnimationsModule, DialogModule],
}),
],
parameters: {

View File

@ -1,13 +1,19 @@
<label [class]="labelClasses">
<label
class="tw-transition tw-select-none tw-mb-0 tw-inline-flex tw-rounded tw-p-0.5 has-[:focus-visible]:tw-ring has-[:focus-visible]:tw-ring-primary-600"
[ngClass]="[formControl.disabled ? 'tw-cursor-auto' : 'tw-cursor-pointer']"
>
<ng-content></ng-content>
<span [class]="labelContentClasses">
<span>
<span
class="tw-inline-flex tw-flex-col"
[ngClass]="formControl.disabled ? 'tw-text-muted' : 'tw-text-main'"
>
<span bitTypography="body1">
<ng-content select="bit-label"></ng-content>
<span *ngIf="required" class="tw-text-xs tw-font-normal"> ({{ "required" | i18n }})</span>
</span>
<ng-content select="bit-hint" *ngIf="!hasError"></ng-content>
</span>
</label>
<div *ngIf="hasError" class="tw-mt-1 tw-text-danger">
<div *ngIf="hasError" class="tw-mt-1 tw-text-danger tw-text-xs tw-ml-0.5">
<i class="bwi bwi-error"></i> {{ displayError }}
</div>

View File

@ -33,27 +33,11 @@ export class FormControlComponent {
@HostBinding("class") get classes() {
return []
.concat(this.inline ? ["tw-inline-block", "tw-mr-4"] : ["tw-block"])
.concat(this.disableMargin ? [] : ["tw-mb-6"]);
.concat(this.disableMargin ? [] : ["tw-mb-4"]);
}
constructor(private i18nService: I18nService) {}
protected get labelClasses() {
return [
"tw-transition",
"tw-select-none",
"tw-mb-0",
"tw-inline-flex",
"tw-items-baseline",
].concat(this.formControl.disabled ? "tw-cursor-auto" : "tw-cursor-pointer");
}
protected get labelContentClasses() {
return ["tw-inline-flex", "tw-flex-col", "tw-font-semibold"].concat(
this.formControl.disabled ? "tw-text-muted" : "tw-text-main",
);
}
get required() {
return this.formControl.required;
}

View File

@ -4,11 +4,11 @@ import { SharedModule } from "../shared";
import { FormControlComponent } from "./form-control.component";
import { BitHintComponent } from "./hint.component";
import { BitLabel } from "./label.directive";
import { BitLabel } from "./label.component";
@NgModule({
imports: [SharedModule],
declarations: [FormControlComponent, BitLabel, BitHintComponent],
imports: [SharedModule, BitLabel],
declarations: [FormControlComponent, BitHintComponent],
exports: [FormControlComponent, BitLabel, BitHintComponent],
})
export class FormControlModule {}

View File

@ -6,7 +6,7 @@ let nextId = 0;
@Directive({
selector: "bit-hint",
host: {
class: "tw-text-muted tw-font-normal tw-inline-block tw-mt-1",
class: "tw-text-muted tw-font-normal tw-inline-block tw-mt-1 tw-text-xs",
},
})
export class BitHintComponent {

View File

@ -0,0 +1,14 @@
<ng-template #endSlotContent>
<ng-content select="[slot=end]"></ng-content>
</ng-template>
<!-- labels inside a form control (checkbox, radio button) should not truncate -->
<span [ngClass]="{ 'tw-truncate': !isInsideFormControl }">
<ng-content></ng-content>
<ng-container *ngIf="isInsideFormControl">
<ng-container *ngTemplateOutlet="endSlotContent"></ng-container>
</ng-container>
</span>
<ng-container *ngIf="!isInsideFormControl">
<ng-container *ngTemplateOutlet="endSlotContent"></ng-container>
</ng-container>

View File

@ -0,0 +1,34 @@
import { CommonModule } from "@angular/common";
import { Component, ElementRef, HostBinding, Input, Optional } from "@angular/core";
import { FormControlComponent } from "./form-control.component";
// Increments for each instance of this component
let nextId = 0;
@Component({
selector: "bit-label",
standalone: true,
templateUrl: "label.component.html",
imports: [CommonModule],
})
export class BitLabel {
constructor(
private elementRef: ElementRef<HTMLInputElement>,
@Optional() private parentFormControl: FormControlComponent,
) {}
@HostBinding("class") @Input() get classList() {
return ["tw-inline-flex", "tw-gap-1", "tw-items-baseline", "tw-flex-row", "tw-min-w-0"];
}
@HostBinding("title") get title() {
return this.elementRef.nativeElement.textContent.trim();
}
@HostBinding() @Input() id = `bit-label-${nextId++}`;
get isInsideFormControl() {
return !!this.parentFormControl;
}
}

View File

@ -1,6 +0,0 @@
import { Directive } from "@angular/core";
@Directive({
selector: "bit-label",
})
export class BitLabel {}

View File

@ -9,7 +9,7 @@ let nextId = 0;
selector: "bit-error",
template: `<i class="bwi bwi-error"></i> {{ displayError }}`,
host: {
class: "tw-block tw-mt-1 tw-text-danger",
class: "tw-block tw-mt-1 tw-text-danger tw-text-xs",
"aria-live": "assertive",
},
})

View File

@ -19,5 +19,6 @@ export abstract class BitFormFieldControl {
error: [string, any];
type?: InputTypes;
spellcheck?: boolean;
readOnly?: boolean;
focus?: () => void;
}

View File

@ -1,16 +1,114 @@
<label class="tw-mb-1 tw-block tw-font-semibold tw-text-main" [attr.for]="input.labelForId">
<ng-content select="bit-label"></ng-content>
<span *ngIf="input.required" class="tw-text-xs tw-font-normal"> ({{ "required" | i18n }})</span>
</label>
<div class="tw-flex">
<div *ngIf="prefixChildren.length" class="tw-flex">
<ng-content select="[bitPrefix]"></ng-content>
</div>
<!-- We need to use templates since the content slots are repeated between the readonly and read-write views. -->
<ng-template #defaultContent>
<ng-content></ng-content>
<div *ngIf="suffixChildren.length" class="tw-flex">
<ng-content select="[bitSuffix]"></ng-content>
</ng-template>
<ng-template #labelContent>
<ng-content select="bit-label"></ng-content>
</ng-template>
<ng-template #prefixContent>
<ng-content select="[bitPrefix]"></ng-content>
</ng-template>
<ng-template #suffixContent>
<ng-content select="[bitSuffix]"></ng-content>
</ng-template>
<div *ngIf="!readOnly; else readOnlyView" class="tw-w-full tw-relative tw-group/bit-form-field">
<div class="tw-absolute tw-w-full tw-h-full tw-top-0 tw-pointer-events-none tw-z-20">
<div class="tw-w-full tw-h-full tw-flex">
<div
class="tw-min-w-3 tw-border-r-0 group-focus-within/bit-form-field:tw-border-r-0 !tw-rounded-l-lg"
[ngClass]="inputBorderClasses"
></div>
<div
class="tw-px-1 tw-shrink tw-min-w-0 tw-mt-px tw-border-x-0 tw-border-t-0 group-focus-within/bit-form-field:tw-border-x-0 group-focus-within/bit-form-field:tw-border-t-0 tw-hidden group-has-[bit-label]/bit-form-field:tw-block"
[ngClass]="inputBorderClasses"
>
<label
class="tw-flex tw-gap-1 tw-text-sm tw-text-muted -tw-translate-y-[0.675rem] tw-mb-0 tw-max-w-full tw-pointer-events-auto"
[attr.for]="input.labelForId"
>
<ng-container *ngTemplateOutlet="labelContent"></ng-container>
<span *ngIf="input.required" class="tw-text-[0.625rem] tw-relative tw-bottom-[-1px]">
({{ "required" | i18n }})</span
>
</label>
</div>
<div
class="tw-min-w-3 tw-grow tw-border-l-0 group-focus-within/bit-form-field:tw-border-l-0 !tw-rounded-r-lg"
[ngClass]="inputBorderClasses"
></div>
</div>
</div>
<div
class="tw-gap-1 tw-bg-background tw-rounded-lg tw-flex tw-min-h-11 [&:not(:has(button:enabled)):has(input:read-only)]:tw-bg-secondary-100 [&:not(:has(button:enabled)):has(textarea:read-only)]:tw-bg-secondary-100"
>
<div
#prefixContainer
class="tw-flex tw-items-center tw-gap-1 tw-pl-3 tw-py-2"
[hidden]="!prefixHasChildren()"
>
<ng-container *ngTemplateOutlet="prefixContent"></ng-container>
</div>
<div
class="default-content tw-w-full tw-relative tw-py-2 has-[bit-select]:tw-p-0 has-[bit-multi-select]:tw-p-0 has-[input:read-only:not([hidden])]:tw-bg-secondary-100 has-[textarea:read-only:not([hidden])]:tw-bg-secondary-100"
[ngClass]="[
prefixHasChildren() ? '' : 'tw-rounded-l-lg tw-pl-3',
suffixHasChildren() ? '' : 'tw-rounded-r-lg tw-pr-3',
]"
>
<ng-container *ngTemplateOutlet="defaultContent"></ng-container>
</div>
<div
#suffixContainer
class="tw-flex tw-items-center tw-gap-1 tw-pr-3 tw-py-2"
[hidden]="!suffixHasChildren()"
>
<ng-container *ngTemplateOutlet="suffixContent"></ng-container>
</div>
</div>
</div>
<ng-template #readOnlyView>
<div class="tw-w-full tw-relative">
<label
class="tw-flex tw-gap-1 tw-text-sm tw-text-muted tw-mb-0 tw-max-w-full"
[attr.for]="input.labelForId"
>
<ng-container *ngTemplateOutlet="labelContent"></ng-container>
</label>
<div
class="tw-gap-1 tw-flex tw-min-h-[1.85rem] tw-border-0 tw-border-solid"
[ngClass]="{
'tw-border-secondary-300/50 tw-border-b tw-pb-[2px]': !disableReadOnlyBorder,
'tw-border-transparent tw-pb-[3px]': disableReadOnlyBorder,
}"
>
<div
#prefixContainer
[hidden]="!prefixHasChildren()"
class="tw-flex tw-items-center tw-gap-1 tw-pl-1"
>
<ng-container *ngTemplateOutlet="prefixContent"></ng-container>
</div>
<div
class="default-content tw-w-full tw-pb-0 tw-relative [&>*]:tw-p-0 [&>*::selection]:tw-bg-primary-700 [&>*::selection]:tw-text-contrast"
>
<ng-container *ngTemplateOutlet="defaultContent"></ng-container>
</div>
<div
#suffixContainer
[hidden]="!suffixHasChildren()"
class="tw-flex tw-items-center tw-gap-1 tw-pr-1"
>
<ng-container *ngTemplateOutlet="suffixContent"></ng-container>
</div>
</div>
</div>
</ng-template>
<ng-container [ngSwitch]="input.hasError">
<ng-content select="bit-hint" *ngSwitchCase="false"></ng-content>
<bit-error [error]="input.error" *ngSwitchCase="true"></bit-error>

View File

@ -1,22 +1,22 @@
import { coerceBooleanProperty } from "@angular/cdk/coercion";
import {
AfterContentChecked,
booleanAttribute,
Component,
ContentChild,
ContentChildren,
ElementRef,
HostBinding,
HostListener,
Input,
QueryList,
ViewChild,
signal,
} from "@angular/core";
import { BitHintComponent } from "../form-control/hint.component";
import { BitLabel } from "../form-control/label.component";
import { inputBorderClasses } from "../input/input.directive";
import { BitErrorComponent } from "./error.component";
import { BitFormFieldControl } from "./form-field-control";
import { BitPrefixDirective } from "./prefix.directive";
import { BitSuffixDirective } from "./suffix.directive";
@Component({
selector: "bit-form-field",
@ -25,30 +25,74 @@ import { BitSuffixDirective } from "./suffix.directive";
export class BitFormFieldComponent implements AfterContentChecked {
@ContentChild(BitFormFieldControl) input: BitFormFieldControl;
@ContentChild(BitHintComponent) hint: BitHintComponent;
@ContentChild(BitLabel) label: BitLabel;
@ViewChild("prefixContainer") prefixContainer: ElementRef<HTMLDivElement>;
@ViewChild("suffixContainer") suffixContainer: ElementRef<HTMLDivElement>;
@ViewChild(BitErrorComponent) error: BitErrorComponent;
@ContentChildren(BitPrefixDirective) prefixChildren: QueryList<BitPrefixDirective>;
@ContentChildren(BitSuffixDirective) suffixChildren: QueryList<BitSuffixDirective>;
@Input({ transform: booleanAttribute })
disableMargin = false;
private _disableMargin = false;
@Input() set disableMargin(value: boolean | "") {
this._disableMargin = coerceBooleanProperty(value);
}
get disableMargin() {
return this._disableMargin;
}
/**
* NOTE: Placeholder to match the API of the form-field component in the `ps/extension` branch,
* no functionality is implemented as of now.
*/
/** If `true`, remove the bottom border for `readonly` inputs */
@Input({ transform: booleanAttribute })
disableReadOnlyBorder = false;
protected prefixHasChildren = signal(false);
protected suffixHasChildren = signal(false);
get inputBorderClasses(): string {
const shouldFocusBorderAppear = this.defaultContentIsFocused();
const groupClasses = [
this.input.hasError
? "group-hover/bit-form-field:tw-border-danger-700"
: "group-hover/bit-form-field:tw-border-primary-600",
// the next 2 selectors override the above hover selectors when the input (or text area) is non-interactive (i.e. readonly, disabled)
"group-has-[input:read-only]/bit-form-field:group-hover/bit-form-field:tw-border-secondary-500",
"group-has-[textarea:read-only]/bit-form-field:group-hover/bit-form-field:tw-border-secondary-500",
"group-focus-within/bit-form-field:tw-outline-none",
shouldFocusBorderAppear ? "group-focus-within/bit-form-field:tw-border-2" : "",
shouldFocusBorderAppear ? "group-focus-within/bit-form-field:tw-border-primary-600" : "",
shouldFocusBorderAppear
? "group-focus-within/bit-form-field:group-hover/bit-form-field:tw-border-primary-600"
: "",
];
const baseInputBorderClasses = inputBorderClasses(this.input.hasError);
const borderClasses = baseInputBorderClasses.concat(groupClasses);
return borderClasses.join(" ");
}
@HostBinding("class")
get classList() {
return ["tw-block"].concat(this.disableMargin ? [] : ["tw-mb-6"]);
return ["tw-block"]
.concat(this.disableMargin ? [] : ["tw-mb-4"])
.concat(this.readOnly ? [] : "tw-pt-2");
}
/**
* If the currently focused element is not part of the default content, then we don't want to show focus on the
* input field itself.
*
* This is necessary because the `tw-group/bit-form-field` wraps the input and any prefix/suffix
* buttons
*/
protected defaultContentIsFocused = signal(false);
@HostListener("focusin", ["$event.target"])
onFocusIn(target: HTMLElement) {
this.defaultContentIsFocused.set(target.matches(".default-content *:focus-visible"));
}
@HostListener("focusout")
onFocusOut() {
this.defaultContentIsFocused.set(false);
}
protected get readOnly(): boolean {
return this.input.readOnly;
}
ngAfterContentChecked(): void {
@ -59,5 +103,8 @@ export class BitFormFieldComponent implements AfterContentChecked {
} else {
this.input.ariaDescribedBy = undefined;
}
this.prefixHasChildren.set(this.prefixContainer?.nativeElement.childElementCount > 0);
this.suffixHasChildren.set(this.suffixContainer?.nativeElement.childElementCount > 0);
}
}

View File

@ -1,3 +1,4 @@
import { TextFieldModule } from "@angular/cdk/text-field";
import {
AbstractControl,
UntypedFormBuilder,
@ -9,14 +10,19 @@ import {
} from "@angular/forms";
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
import { A11yTitleDirective } from "@bitwarden/angular/src/directives/a11y-title.directive";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { AsyncActionsModule } from "../async-actions";
import { BadgeModule } from "../badge";
import { ButtonModule } from "../button";
import { CardComponent } from "../card";
import { CheckboxModule } from "../checkbox";
import { IconButtonModule } from "../icon-button";
import { InputModule } from "../input/input.module";
import { LinkModule } from "../link";
import { RadioButtonModule } from "../radio-button";
import { SectionComponent } from "../section";
import { SelectModule } from "../select";
import { I18nMockService } from "../utils/i18n-mock.service";
@ -39,7 +45,13 @@ export default {
CheckboxModule,
RadioButtonModule,
SelectModule,
LinkModule,
CardComponent,
SectionComponent,
TextFieldModule,
BadgeModule,
],
declarations: [A11yTitleDirective],
providers: [
{
provide: I18nService,
@ -49,6 +61,7 @@ export default {
required: "required",
inputRequired: "Input is required.",
inputEmail: "Input is not an email-address.",
toggleVisibility: "Toggle visibility",
});
},
},
@ -74,6 +87,7 @@ const defaultFormObj = fb.group({
email: ["", [Validators.required, Validators.email, forbiddenNameValidator(/bit/i)]],
terms: [false, [Validators.requiredTrue]],
updates: ["yes"],
file: [""],
});
// Custom error message, `message` is shown as the error message
@ -96,7 +110,7 @@ export const Default: Story = {
submit: submit,
...args,
},
template: `
template: /*html*/ `
<form [formGroup]="formObj">
<bit-form-field>
<bit-label>Label</bit-label>
@ -108,13 +122,68 @@ export const Default: Story = {
}),
};
export const LabelWithIcon: Story = {
render: (args) => ({
props: {
formObj: defaultFormObj,
submit: submit,
...args,
},
template: /*html*/ `
<form [formGroup]="formObj">
<bit-form-field>
<bit-label>
Label
<a href="#" slot="end" bitLink>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a>
</bit-label>
<input bitInput formControlName="name" />
<bit-hint>Optional Hint</bit-hint>
</bit-form-field>
</form>
`,
}),
};
export const LongLabel: Story = {
render: (args) => ({
props: {
formObj: defaultFormObj,
submit: submit,
...args,
},
template: /*html*/ `
<form [formGroup]="formObj" style="width: 200px">
<bit-form-field>
<bit-label>
Hello I am a very long label with lots of very cool helpful information
</bit-label>
<input bitInput formControlName="name" />
<bit-hint>Optional Hint</bit-hint>
</bit-form-field>
<bit-form-field>
<bit-label>
Hello I am a very long label with lots of very cool helpful information
<a href="#" slot="end" bitLink>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a>
</bit-label>
<input bitInput formControlName="name" />
<bit-hint>Optional Hint</bit-hint>
</bit-form-field>
</form>
`,
}),
};
export const Required: Story = {
render: (args) => ({
props: {
formObj: formObj,
...args,
},
template: `
template: /*html*/ `
<bit-form-field>
<bit-label>Label</bit-label>
<input bitInput required placeholder="Placeholder" />
@ -134,7 +203,7 @@ export const Hint: Story = {
formObj: formObj,
...args,
},
template: `
template: /*html*/ `
<bit-form-field [formGroup]="formObj">
<bit-label>FormControl</bit-label>
<input bitInput formControlName="required" placeholder="Placeholder" />
@ -147,7 +216,7 @@ export const Hint: Story = {
export const Disabled: Story = {
render: (args) => ({
props: args,
template: `
template: /*html*/ `
<bit-form-field>
<bit-label>Label</bit-label>
<input bitInput placeholder="Placeholder" disabled />
@ -160,16 +229,54 @@ export const Disabled: Story = {
export const Readonly: Story = {
render: (args) => ({
props: args,
template: `
template: /*html*/ `
<bit-form-field>
<bit-label>Input</bit-label>
<input bitInput value="Foobar" readonly />
</bit-form-field>
<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>
</bit-form-field>
<bit-form-field>
<bit-label>Textarea</bit-label>
<textarea bitInput rows="4" readonly>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</textarea>
</bit-form-field>
<div class="tw-p-4 tw-mt-10 tw-border-2 tw-border-solid tw-border-black tw-bg-background-alt">
<h2 bitTypography="h2">Inside card</h2>
<bit-section>
<bit-card>
<bit-form-field>
<bit-label>Input</bit-label>
<input bitInput value="Foobar" readonly />
</bit-form-field>
<bit-form-field>
<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>
</bit-form-field>
<bit-form-field>
<bit-label>Textarea <span slot="end" bitBadge variant="success">Premium</span></bit-label>
<textarea bitInput rows="3" readonly class="tw-resize-none">Row1
Row2
Row3</textarea>
</bit-form-field>
<bit-form-field disableMargin disableReadOnlyBorder>
<bit-label>Sans margin & border</bit-label>
<input bitInput value="Foobar" readonly />
</bit-form-field>
</bit-card>
</bit-section>
</div>
`,
}),
args: {},
@ -178,7 +285,7 @@ export const Readonly: Story = {
export const InputGroup: Story = {
render: (args) => ({
props: args,
template: `
template: /*html*/ `
<bit-form-field>
<bit-label>Label</bit-label>
<input bitInput placeholder="Placeholder" />
@ -193,13 +300,19 @@ export const InputGroup: Story = {
export const ButtonInputGroup: Story = {
render: (args) => ({
props: args,
template: `
template: /*html*/ `
<bit-form-field>
<button bitPrefix bitIconButton="bwi-star"></button>
<bit-label>
Label
<a href="#" slot="end" bitLink [appA11yTitle]="'More info'">
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a>
</bit-label>
<button bitPrefix bitIconButton="bwi-star" [appA11yTitle]="'Favorite Label'"></button>
<input bitInput placeholder="Placeholder" />
<button bitSuffix bitIconButton="bwi-eye"></button>
<button bitSuffix bitIconButton="bwi-clone"></button>
<button bitSuffix bitButton>
<button bitSuffix bitIconButton="bwi-eye" [appA11yTitle]="'Hide Label'"></button>
<button bitSuffix bitIconButton="bwi-clone" [appA11yTitle]="'Clone Label'"></button>
<button bitSuffix bitLink>
Apply
</button>
</bit-form-field>
@ -211,14 +324,32 @@ export const ButtonInputGroup: Story = {
export const DisabledButtonInputGroup: Story = {
render: (args) => ({
props: args,
template: `
template: /*html*/ `
<bit-form-field>
<bit-label>Label</bit-label>
<button bitPrefix bitIconButton="bwi-star" disabled></button>
<button bitPrefix bitIconButton="bwi-star" disabled [appA11yTitle]="'Favorite Label'"></button>
<input bitInput placeholder="Placeholder" disabled />
<button bitSuffix bitIconButton="bwi-eye" disabled></button>
<button bitSuffix bitIconButton="bwi-clone" disabled></button>
<button bitSuffix bitButton disabled>
<button bitSuffix bitIconButton="bwi-eye" disabled [appA11yTitle]="'Hide Label'"></button>
<button bitSuffix bitIconButton="bwi-clone" disabled [appA11yTitle]="'Clone Label'"></button>
<button bitSuffix bitLink disabled>
Apply
</button>
</bit-form-field>
`,
}),
args: {},
};
export const PartiallyDisabledButtonInputGroup: Story = {
render: (args) => ({
props: args,
template: /*html*/ `
<bit-form-field>
<bit-label>Label</bit-label>
<input bitInput placeholder="Placeholder" disabled />
<button bitSuffix bitIconButton="bwi-eye" [appA11yTitle]="'Hide Label'"></button>
<button bitSuffix bitIconButton="bwi-clone" [appA11yTitle]="'Clone Label'"></button>
<button bitSuffix bitLink disabled>
Apply
</button>
</bit-form-field>
@ -230,7 +361,7 @@ export const DisabledButtonInputGroup: Story = {
export const Select: Story = {
render: (args: BitFormFieldComponent) => ({
props: args,
template: `
template: /*html*/ `
<bit-form-field>
<bit-label>Label</bit-label>
<select bitInput>
@ -246,7 +377,7 @@ export const Select: Story = {
export const AdvancedSelect: Story = {
render: (args: BitFormFieldComponent) => ({
props: args,
template: `
template: /*html*/ `
<bit-form-field>
<bit-label>Label</bit-label>
<bit-select>
@ -258,10 +389,40 @@ export const AdvancedSelect: Story = {
}),
};
export const FileInput: Story = {
render: (args) => ({
props: {
formObj: defaultFormObj,
submit: submit,
...args,
},
template: /*html*/ `
<form [formGroup]="formObj">
<bit-form-field>
<bit-label>File</bit-label>
<div class="tw-text-main">
<button bitButton type="button" buttonType="secondary">
Choose File
</button>
No file chosen
</div>
<input
bitInput
#fileSelector
type="file"
formControlName="file"
hidden
/>
</bit-form-field>
</form>
`,
}),
};
export const Textarea: Story = {
render: (args: BitFormFieldComponent) => ({
props: args,
template: `
template: /*html*/ `
<bit-form-field>
<bit-label>Textarea</bit-label>
<textarea bitInput rows="4"></textarea>

View File

@ -66,9 +66,9 @@ export const actionsData = {
};
const fb = new FormBuilder();
const formObjFactory = () =>
const formObjFactory = (isDisabled = false) =>
fb.group({
select: [[], [Validators.required]],
select: fb.control({ value: [], disabled: isDisabled }, { validators: [Validators.required] }),
});
function submit(formObj: FormGroup) {
@ -85,7 +85,7 @@ export const Loading: Story = {
...args,
onItemsConfirmed: actionsData.onItemsConfirmed,
},
template: `
template: /*html*/ `
<form [formGroup]="formObj" (ngSubmit)="submit(formObj)">
<bit-form-field>
<bit-label>{{ name }}</bit-label>
@ -100,7 +100,6 @@ export const Loading: Story = {
</bit-multi-select>
<bit-hint>{{ hint }}</bit-hint>
</bit-form-field>
<button type="submit" bitButton buttonType="primary">Submit</button>
</form>
`,
}),
@ -113,8 +112,33 @@ export const Loading: Story = {
};
export const Disabled: Story = {
...Loading,
render: (args) => ({
props: {
formObj: formObjFactory(true),
submit: submit,
...args,
onItemsConfirmed: actionsData.onItemsConfirmed,
},
template: /*html*/ `
<form [formGroup]="formObj" (ngSubmit)="submit(formObj)">
<bit-form-field>
<bit-label>{{ name }}</bit-label>
<bit-multi-select
class="tw-w-full"
formControlName="select"
[baseItems]="baseItems"
[removeSelectedItems]="removeSelectedItems"
[loading]="loading"
[disabled]="disabled"
(onItemsConfirmed)="onItemsConfirmed($event)">
</bit-multi-select>
<bit-hint>{{ hint }}</bit-hint>
</bit-form-field>
</form>
`,
}),
args: {
baseItems: [] as any,
name: "Disabled",
disabled: true,
hint: "This is what a disabled multi-select looks like",
@ -178,7 +202,7 @@ export const Members: Story = {
{
id: "7",
listName: "Final listName (fname@mail.me)",
labelName: "(fname@mail.me)",
labelName: "Final listName (fname@mail.me)",
icon: "bwi-user",
},
],
@ -269,34 +293,3 @@ export const RemoveSelected: Story = {
removeSelectedItems: true,
},
};
export const Standalone: Story = {
render: (args) => ({
props: {
...args,
onItemsConfirmed: actionsData.onItemsConfirmed,
},
template: `
<bit-multi-select
class="tw-w-full"
[baseItems]="baseItems"
[removeSelectedItems]="removeSelectedItems"
[loading]="loading"
[disabled]="disabled"
(onItemsConfirmed)="onItemsConfirmed($event)">
</bit-multi-select>
`,
}),
args: {
baseItems: [
{ id: "1", listName: "Group 1", labelName: "Group 1", icon: "bwi-family" },
{ id: "2", listName: "Group 2", labelName: "Group 2", icon: "bwi-family" },
{ id: "3", listName: "Group 3", labelName: "Group 3", icon: "bwi-family" },
{ id: "4", listName: "Group 4", labelName: "Group 4", icon: "bwi-family" },
{ id: "5", listName: "Group 5", labelName: "Group 5", icon: "bwi-family" },
{ id: "6", listName: "Group 6", labelName: "Group 6", icon: "bwi-family" },
{ id: "7", listName: "Group 7", labelName: "Group 7", icon: "bwi-family" },
],
removeSelectedItems: true,
},
};

View File

@ -37,8 +37,6 @@ export class BitPasswordInputToggleDirective implements AfterContentInit, OnChan
this.toggledChange.emit(this.toggled);
this.update();
this.formField.input?.focus();
}
constructor(

View File

@ -43,7 +43,7 @@ type Story = StoryObj<BitPasswordInputToggleDirective>;
export const Default: Story = {
render: (args) => ({
props: args,
template: `
template: /*html*/ `
<form>
<bit-form-field>
<bit-label>Password</bit-label>
@ -58,7 +58,7 @@ export const Default: Story = {
export const Binding: Story = {
render: (args) => ({
props: args,
template: `
template: /*html*/ `
<form>
<bit-form-field>
<bit-label>Password</bit-label>

View File

@ -1,51 +1,20 @@
import { Directive, HostBinding, Input, OnInit, Optional } from "@angular/core";
import { ButtonLikeAbstraction } from "../shared/button-like.abstraction";
export const PrefixClasses = [
"tw-bg-background-alt",
"tw-border",
"tw-border-solid",
"tw-border-secondary-600",
"tw-text-muted",
"tw-rounded-none",
];
export const PrefixButtonClasses = [
"hover:tw-bg-text-muted",
"hover:tw-text-contrast",
"disabled:tw-opacity-100",
"disabled:tw-bg-secondary-100",
"disabled:hover:tw-bg-secondary-100",
"disabled:hover:tw-text-muted",
"focus-visible:tw-ring-primary-700",
"focus-visible:tw-border-primary-700",
"focus-visible:tw-ring-1",
"focus-visible:tw-ring-inset",
"focus-visible:tw-ring-primary-700",
"focus-visible:tw-z-10",
];
export const PrefixStaticContentClasses = ["tw-block", "tw-px-3", "tw-py-1.5"];
import { BitIconButtonComponent } from "../icon-button/icon-button.component";
@Directive({
selector: "[bitPrefix]",
})
export class BitPrefixDirective implements OnInit {
constructor(@Optional() private buttonComponent: ButtonLikeAbstraction) {}
@HostBinding("class") @Input() get classList() {
return PrefixClasses.concat([
"tw-border-r-0",
"first:tw-rounded-l",
"focus-visible:tw-border-r",
"focus-visible:tw-mr-[-1px]",
]).concat(this.buttonComponent != undefined ? PrefixButtonClasses : PrefixStaticContentClasses);
return ["tw-text-muted"];
}
ngOnInit(): void {
this.buttonComponent?.setButtonType("unstyled");
constructor(@Optional() private iconButtonComponent: BitIconButtonComponent) {}
ngOnInit() {
if (this.iconButtonComponent) {
this.iconButtonComponent.size = "small";
}
}
}

View File

@ -1,26 +1,20 @@
import { Directive, HostBinding, Input, OnInit, Optional } from "@angular/core";
import { ButtonLikeAbstraction } from "../shared/button-like.abstraction";
import { PrefixButtonClasses, PrefixClasses, PrefixStaticContentClasses } from "./prefix.directive";
import { BitIconButtonComponent } from "../icon-button/icon-button.component";
@Directive({
selector: "[bitSuffix]",
})
export class BitSuffixDirective implements OnInit {
constructor(@Optional() private buttonComponent: ButtonLikeAbstraction) {}
@HostBinding("class") @Input() get classList() {
return PrefixClasses.concat([
"tw-border-l-0",
"last:tw-rounded-r",
"focus-visible:tw-border-l",
"focus-visible:tw-ml-[-1px]",
]).concat(this.buttonComponent != undefined ? PrefixButtonClasses : PrefixStaticContentClasses);
return ["tw-text-muted"];
}
ngOnInit(): void {
this.buttonComponent?.setButtonType("unstyled");
constructor(@Optional() private iconButtonComponent: BitIconButtonComponent) {}
ngOnInit() {
if (this.iconButtonComponent) {
this.iconButtonComponent.size = "small";
}
}
}

View File

@ -16,6 +16,7 @@ import { CheckboxModule } from "../checkbox";
import { FormControlModule } from "../form-control";
import { FormFieldModule } from "../form-field";
import { InputModule } from "../input/input.module";
import { MultiSelectModule } from "../multi-select";
import { RadioButtonModule } from "../radio-button";
import { SelectModule } from "../select";
import { I18nMockService } from "../utils/i18n-mock.service";
@ -36,6 +37,7 @@ export default {
CheckboxModule,
RadioButtonModule,
SelectModule,
MultiSelectModule,
],
providers: [
{
@ -90,7 +92,7 @@ export const FullExample: Story = {
submit: () => exampleFormObj.markAllAsTouched(),
...args,
},
template: `
template: /*html*/ `
<form [formGroup]="formObj" (ngSubmit)="submit()">
<bit-form-field>
<bit-label>Name</bit-label>
@ -109,6 +111,19 @@ export const FullExample: Story = {
</bit-select>
</bit-form-field>
<bit-form-field>
<bit-label>Groups</bit-label>
<bit-multi-select
class="tw-w-full"
formControlName="select"
[baseItems]="baseItems"
[removeSelectedItems]="removeSelectedItems"
[loading]="false"
[disabled]="false"
(onItemsConfirmed)="onItemsConfirmed($event)">
</bit-multi-select>
</bit-form-field>
<bit-form-field>
<bit-label>Age</bit-label>
<input
@ -146,5 +161,14 @@ export const FullExample: Story = {
args: {
countries,
baseItems: [
{ id: "1", listName: "Group 1", labelName: "Group 1", icon: "bwi-family" },
{ id: "2", listName: "Group 2", labelName: "Group 2", icon: "bwi-family" },
{ id: "3", listName: "Group 3", labelName: "Group 3", icon: "bwi-family" },
{ id: "4", listName: "Group 4", labelName: "Group 4", icon: "bwi-family" },
{ id: "5", listName: "Group 5", labelName: "Group 5", icon: "bwi-family" },
{ id: "6", listName: "Group 6", labelName: "Group 6", icon: "bwi-family" },
{ id: "7", listName: "Group 7", labelName: "Group 7", icon: "bwi-family" },
],
},
};

View File

@ -55,6 +55,18 @@ Be sure to use an appropriate type attribute on fields when defining new field c
`email` for email address or `number` for numerical information) to take advantage of newer input
controls like email verification, number selection, and more.
Labels accept an optional `end` slot for any content that should not be truncated if the label text
is too long, such as info link buttons or badges.
```
<bit-label>
Label text goes here
<a href="#" slot="end" bitLink>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a>
</bit-label>
```
#### Default with required attribute
<Story of={fieldStories.Default} />
@ -86,6 +98,9 @@ button consists of a label and a radio input.
The full form control + label should be selectable to allow the user a larger click target.
Labels accept an optional `end` slot for any content that is not part of the main label text, such
as info link buttons or badges.
Radio groups should always have a default selected value.
Radio groups may optionally include extra helper text below each radio button.
@ -112,6 +127,9 @@ Checkboxes can be displayed on their own or in a group (select multiple form que
displayed in a group, include an input Label and any associated required/validation logic for the
field.
Labels accept an optional `end` slot for any content that is not part of the main label text, such
as info link buttons or badges.
Unlike radio groups, checkbox groups are not required to have a default selected value.
Checkbox groups can include extra explanation text below each radio button or just the checkbox
@ -134,6 +152,12 @@ If a checkbox group has more than 4 options a
helper text.
- **Example:** "Billing Email is required if owned by a business".
### Icon Buttons in Form Fields
When adding prefix or suffix icon buttons to a form field, be sure to use the `appA11yTitle`
directive to provide a label for screenreaders. Typically, the label should follow this pattern:
`{Action} {field label}`, i.e. "Copy username".
### Form Field Errors
- When a resting field is filled out, validation is triggered when the user de-focuses the field

View File

@ -15,10 +15,10 @@ const focusRing = [
"before:tw-content-['']",
"before:tw-block",
"before:tw-absolute",
"before:-tw-inset-[3px]",
"before:tw-rounded-md",
"before:-tw-inset-[2px]",
"before:tw-rounded-lg",
"before:tw-transition",
"before:tw-ring",
"before:tw-ring-2",
"before:tw-ring-transparent",
"focus-visible:tw-z-10",
];
@ -41,9 +41,9 @@ const styles: Record<IconButtonType, string[]> = {
"!tw-text-main",
"tw-border-transparent",
"hover:tw-bg-transparent-hover",
"hover:tw-border-text-main",
"focus-visible:before:tw-ring-text-main",
"disabled:tw-opacity-60",
"hover:tw-border-primary-600",
"focus-visible:before:tw-ring-primary-600",
"disabled:!tw-text-secondary-300",
"disabled:hover:tw-border-transparent",
"disabled:hover:tw-bg-transparent",
...focusRing,
@ -55,9 +55,9 @@ const styles: Record<IconButtonType, string[]> = {
"aria-expanded:tw-bg-text-muted",
"aria-expanded:!tw-text-contrast",
"hover:tw-bg-transparent-hover",
"hover:tw-border-primary-700",
"focus-visible:before:tw-ring-primary-700",
"disabled:tw-opacity-60",
"hover:tw-border-primary-600",
"focus-visible:before:tw-ring-primary-600",
"disabled:!tw-text-secondary-300",
"aria-expanded:hover:tw-bg-secondary-700",
"aria-expanded:hover:tw-border-secondary-700",
"disabled:hover:tw-border-transparent",
@ -68,9 +68,9 @@ const styles: Record<IconButtonType, string[]> = {
"tw-bg-primary-600",
"!tw-text-contrast",
"tw-border-primary-600",
"hover:tw-bg-primary-700",
"hover:tw-border-primary-700",
"focus-visible:before:tw-ring-primary-700",
"hover:tw-bg-primary-600",
"hover:tw-border-primary-600",
"focus-visible:before:tw-ring-primary-600",
"disabled:tw-opacity-60",
"disabled:hover:tw-border-primary-600",
"disabled:hover:tw-bg-primary-600",
@ -82,26 +82,25 @@ const styles: Record<IconButtonType, string[]> = {
"tw-border-text-muted",
"hover:!tw-text-contrast",
"hover:tw-bg-text-muted",
"focus-visible:before:tw-ring-primary-700",
"focus-visible:before:tw-ring-primary-600",
"disabled:tw-opacity-60",
"disabled:hover:tw-border-text-muted",
"disabled:hover:tw-bg-transparent",
"disabled:hover:!tw-text-muted",
"disabled:hover:tw-border-text-muted",
...focusRing,
],
danger: [
"tw-bg-transparent",
"!tw-text-danger",
"tw-border-danger-600",
"hover:!tw-text-contrast",
"hover:tw-bg-danger-600",
"focus-visible:before:tw-ring-primary-700",
"disabled:tw-opacity-60",
"disabled:hover:tw-border-danger-600",
"!tw-text-danger-600",
"tw-border-transparent",
"hover:!tw-text-danger-600",
"hover:tw-bg-transparent",
"hover:tw-border-primary-600",
"focus-visible:before:tw-ring-primary-600",
"disabled:!tw-text-secondary-300",
"disabled:hover:tw-border-transparent",
"disabled:hover:tw-bg-transparent",
"disabled:hover:!tw-text-danger",
"disabled:hover:tw-border-danger-600",
"disabled:hover:!tw-text-secondary-300",
...focusRing,
],
light: [
@ -111,6 +110,7 @@ const styles: Record<IconButtonType, string[]> = {
"hover:tw-bg-transparent-hover",
"hover:tw-border-text-alt2",
"focus-visible:before:tw-ring-text-alt2",
"disabled:tw-opacity-60",
"disabled:hover:tw-border-transparent",
"disabled:hover:tw-bg-transparent",
...focusRing,
@ -145,7 +145,7 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableE
"tw-font-semibold",
"tw-border",
"tw-border-solid",
"tw-rounded",
"tw-rounded-lg",
"tw-transition",
"hover:tw-no-underline",
"focus:tw-outline-none",
@ -167,10 +167,6 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableE
@Input() loading = false;
@Input() disabled = false;
setButtonType(value: "primary" | "secondary" | "danger" | "unstyled") {
this.buttonType = value;
}
getFocusTarget() {
return this.elementRef.nativeElement;
}

View File

@ -133,6 +133,6 @@ export const Disabled: Story = {
...Default,
args: {
disabled: true,
loading: true,
loading: false,
},
};

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