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:
parent
5c540a86f4
commit
3b5b2d6bd6
10
.github/whitelist-capital-letters.txt
vendored
10
.github/whitelist-capital-letters.txt
vendored
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 }"
|
||||
>
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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");
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
|
@ -3,7 +3,7 @@
|
||||
<button
|
||||
id="sendBtn"
|
||||
bitLink
|
||||
linkType="contrast"
|
||||
linkType="secondary"
|
||||
bitButton
|
||||
type="button"
|
||||
buttonType="unstyled"
|
||||
|
@ -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,6 +93,7 @@
|
||||
</bit-hint>
|
||||
</bit-radio-button>
|
||||
<ng-container *ngIf="updateMethod === licenseOptions.SYNC">
|
||||
<div class="tw-mt-6">
|
||||
<button
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
@ -111,12 +111,13 @@
|
||||
>
|
||||
{{ "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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -1,5 +1,5 @@
|
||||
html {
|
||||
font-size: 14px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
body {
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -50,7 +50,7 @@
|
||||
<a
|
||||
href="https://bitwarden.com/contact/"
|
||||
bitLink
|
||||
linkType="contrast"
|
||||
linkType="secondary"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
|
@ -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
|
||||
|
@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
libs/angular/src/scss/webfonts/dm-sans.woff2
Normal file
BIN
libs/angular/src/scss/webfonts/dm-sans.woff2
Normal file
Binary file not shown.
@ -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
|
||||
|
@ -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] : [])
|
||||
|
@ -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 />
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 />
|
||||
|
||||
|
@ -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>
|
||||
`,
|
||||
}),
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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 state’s color with a 60% opacity or
|
||||
`tw-opacity-60`.
|
||||
|
||||
<Story of={stories.Disabled} />
|
||||
|
||||
## Block
|
||||
|
@ -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: {
|
||||
|
@ -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>
|
||||
<div bitTypography="body2">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</aside>
|
||||
|
@ -12,7 +12,7 @@ describe("Callout", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [CalloutComponent],
|
||||
imports: [CalloutComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: I18nService,
|
||||
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 {}
|
||||
|
@ -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 {}
|
||||
|
@ -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()
|
||||
|
@ -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>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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 */
|
||||
|
@ -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"
|
||||
|
@ -78,6 +78,7 @@ export default {
|
||||
useFactory: () => {
|
||||
return new I18nMockService({
|
||||
close: "Close",
|
||||
loading: "Loading",
|
||||
});
|
||||
},
|
||||
},
|
||||
|
@ -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>
|
||||
|
@ -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} />
|
||||
|
@ -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.
|
||||
|
@ -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: [
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
{
|
||||
|
@ -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: {
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 {}
|
||||
|
@ -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 {
|
||||
|
14
libs/components/src/form-control/label.component.html
Normal file
14
libs/components/src/form-control/label.component.html
Normal 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>
|
34
libs/components/src/form-control/label.component.ts
Normal file
34
libs/components/src/form-control/label.component.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
import { Directive } from "@angular/core";
|
||||
|
||||
@Directive({
|
||||
selector: "bit-label",
|
||||
})
|
||||
export class BitLabel {}
|
@ -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",
|
||||
},
|
||||
})
|
||||
|
@ -19,5 +19,6 @@ export abstract class BitFormFieldControl {
|
||||
error: [string, any];
|
||||
type?: InputTypes;
|
||||
spellcheck?: boolean;
|
||||
readOnly?: boolean;
|
||||
focus?: () => void;
|
||||
}
|
||||
|
@ -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-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>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
@ -37,8 +37,6 @@ export class BitPasswordInputToggleDirective implements AfterContentInit, OnChan
|
||||
this.toggledChange.emit(this.toggled);
|
||||
|
||||
this.update();
|
||||
|
||||
this.formField.input?.focus();
|
||||
}
|
||||
|
||||
constructor(
|
||||
|
@ -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>
|
||||
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user