diff --git a/apps/design-land/src/app/input/input.component.html b/apps/design-land/src/app/input/input.component.html index 9f30c8d256..09ba796c7f 100644 --- a/apps/design-land/src/app/input/input.component.html +++ b/apps/design-land/src/app/input/input.component.html @@ -18,7 +18,17 @@

Disabled

+

Input With Hint

+

The input in this example has a hint.

+ + +

With Reactive Forms

The input in this example uses the ReactiveFormsModule to display errors.

- \ No newline at end of file + + +

Password With Reactive Forms

+

This is a special case where hints and errors for passwords are displayed.

+ + diff --git a/libs/design/input/examples/src/examples.ts b/libs/design/input/examples/src/examples.ts index acee588cb3..22a5c662c4 100644 --- a/libs/design/input/examples/src/examples.ts +++ b/libs/design/input/examples/src/examples.ts @@ -1,11 +1,15 @@ import { BasicInputComponent } from './basic-input/basic-input.component'; import { InputDisabledComponent } from './input-disabled/input-disabled.component'; import { InputErrorComponent } from './input-error/input-error.component'; +import { InputHintComponent } from './input-hint/input-hint.component'; import { InputWithFormFieldComponent } from './input-with-form-field/input-with-form-field.component'; +import { PasswordWithFormFieldComponent } from './password-with-form-field/password-with-form-field.component'; export const INPUT_EXAMPLES = [ BasicInputComponent, InputWithFormFieldComponent, InputDisabledComponent, InputErrorComponent, + PasswordWithFormFieldComponent, + InputHintComponent, ]; diff --git a/libs/design/input/examples/src/input-disabled/input-disabled.component.html b/libs/design/input/examples/src/input-disabled/input-disabled.component.html index edf6f1b289..7a115365f9 100644 --- a/libs/design/input/examples/src/input-disabled/input-disabled.component.html +++ b/libs/design/input/examples/src/input-disabled/input-disabled.component.html @@ -1,3 +1,4 @@ - + + \ No newline at end of file diff --git a/libs/design/input/examples/src/input-disabled/input-disabled.component.ts b/libs/design/input/examples/src/input-disabled/input-disabled.component.ts index 38a12ae23d..08be9bd56a 100644 --- a/libs/design/input/examples/src/input-disabled/input-disabled.component.ts +++ b/libs/design/input/examples/src/input-disabled/input-disabled.component.ts @@ -17,5 +17,5 @@ import { imports: [DaffFormFieldModule, DaffInputModule], }) export class InputDisabledComponent { - + isDisabled = true; } diff --git a/libs/design/input/examples/src/input-error/input-error.component.html b/libs/design/input/examples/src/input-error/input-error.component.html index e8683fad5e..47143b6798 100644 --- a/libs/design/input/examples/src/input-error/input-error.component.html +++ b/libs/design/input/examples/src/input-error/input-error.component.html @@ -1,4 +1,12 @@ - + + + @if (control.errors?.required) { + Email is a required field. + } + @if (control.errors?.email) { + Email is not valid. + } +
Hint goes here.

Value: {{ control.value }}

\ No newline at end of file diff --git a/libs/design/input/examples/src/input-hint/input-hint.component.html b/libs/design/input/examples/src/input-hint/input-hint.component.html new file mode 100644 index 0000000000..ca9886d677 --- /dev/null +++ b/libs/design/input/examples/src/input-hint/input-hint.component.html @@ -0,0 +1,5 @@ + + + +
Hint goes here.
+
\ No newline at end of file diff --git a/libs/design/input/examples/src/input-hint/input-hint.component.ts b/libs/design/input/examples/src/input-hint/input-hint.component.ts new file mode 100644 index 0000000000..08e457a045 --- /dev/null +++ b/libs/design/input/examples/src/input-hint/input-hint.component.ts @@ -0,0 +1,23 @@ +import { + ChangeDetectionStrategy, + Component, +} from '@angular/core'; + +import { + DaffFormFieldModule, + DaffInputModule, +} from '@daffodil/design'; + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'input-hint', + templateUrl: './input-hint.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + DaffFormFieldModule, + DaffInputModule, + ], +}) +export class InputHintComponent { +} diff --git a/libs/design/input/examples/src/input-with-form-field/input-with-form-field.component.html b/libs/design/input/examples/src/input-with-form-field/input-with-form-field.component.html index 0fb7567fd5..6a88acbbff 100644 --- a/libs/design/input/examples/src/input-with-form-field/input-with-form-field.component.html +++ b/libs/design/input/examples/src/input-with-form-field/input-with-form-field.component.html @@ -1,3 +1,6 @@ - - + + + + + \ No newline at end of file diff --git a/libs/design/input/examples/src/input-with-form-field/input-with-form-field.component.ts b/libs/design/input/examples/src/input-with-form-field/input-with-form-field.component.ts index 7f087aa5d5..d96190dd53 100644 --- a/libs/design/input/examples/src/input-with-form-field/input-with-form-field.component.ts +++ b/libs/design/input/examples/src/input-with-form-field/input-with-form-field.component.ts @@ -2,20 +2,27 @@ import { ChangeDetectionStrategy, Component, } from '@angular/core'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { + faUser, + faCircleXmark, +} from '@fortawesome/free-solid-svg-icons'; import { DaffFormFieldModule, DaffInputModule, } from '@daffodil/design'; + @Component({ // eslint-disable-next-line @angular-eslint/component-selector selector: 'input-with-form-field', templateUrl: './input-with-form-field.component.html', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [DaffFormFieldModule, DaffInputModule], + imports: [DaffFormFieldModule, DaffInputModule, FontAwesomeModule], }) export class InputWithFormFieldComponent { - + faUser = faUser; + faCircleXmark = faCircleXmark; } diff --git a/libs/design/input/examples/src/password-with-form-field/password-with-form-field.component.html b/libs/design/input/examples/src/password-with-form-field/password-with-form-field.component.html new file mode 100644 index 0000000000..da46aaef29 --- /dev/null +++ b/libs/design/input/examples/src/password-with-form-field/password-with-form-field.component.html @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/libs/design/input/examples/src/password-with-form-field/password-with-form-field.component.ts b/libs/design/input/examples/src/password-with-form-field/password-with-form-field.component.ts new file mode 100644 index 0000000000..c576fb6fca --- /dev/null +++ b/libs/design/input/examples/src/password-with-form-field/password-with-form-field.component.ts @@ -0,0 +1,25 @@ +import { + ChangeDetectionStrategy, + Component, +} from '@angular/core'; +import { + ReactiveFormsModule, + UntypedFormControl, +} from '@angular/forms'; + +import { + DaffFormFieldModule, + DaffInputModule, +} from '@daffodil/design'; + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'password-with-form-field', + templateUrl: './password-with-form-field.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [DaffFormFieldModule, DaffInputModule, ReactiveFormsModule], +}) +export class PasswordWithFormFieldComponent { + control: UntypedFormControl = new UntypedFormControl(); +} diff --git a/libs/design/src/atoms/form/error-message/error-message.component.scss b/libs/design/src/atoms/form/error-message/error-message.component.scss index 78fc8391f7..71291cd003 100644 --- a/libs/design/src/atoms/form/error-message/error-message.component.scss +++ b/libs/design/src/atoms/form/error-message/error-message.component.scss @@ -3,5 +3,6 @@ :host { display: block; font-size: t.$small-font-size; - margin-top: 5px; + margin-top: 4px; + padding-left: 16px; } diff --git a/libs/design/src/atoms/form/form-field/form-field-control.ts b/libs/design/src/atoms/form/form-field/form-field-control.ts index 43581ba7c3..f2792ce5b6 100644 --- a/libs/design/src/atoms/form/form-field/form-field-control.ts +++ b/libs/design/src/atoms/form/form-field/form-field-control.ts @@ -11,7 +11,7 @@ import { NgControl } from '@angular/forms'; * in javascript, they get thrown out by the typescript compiler and cannot * be used for the necessary dependency injection. */ -export abstract class DaffFormFieldControl { +export abstract class DaffFormFieldControl { readonly ngControl: NgControl | null; readonly controlType?: any; @@ -19,4 +19,6 @@ export abstract class DaffFormFieldControl { readonly focused: boolean; abstract focus(event?: Event): void; + + readonly value: T; }; diff --git a/libs/design/src/atoms/form/form-field/form-field/form-field-theme.scss b/libs/design/src/atoms/form/form-field/form-field/form-field-theme.scss index cfbac94e11..0231dc5818 100644 --- a/libs/design/src/atoms/form/form-field/form-field/form-field-theme.scss +++ b/libs/design/src/atoms/form/form-field/form-field/form-field-theme.scss @@ -13,19 +13,15 @@ .daff-form-field { &__control { background: $base; - border: 1px solid theming.daff-illuminate($base, $neutral, 3); - color: theming.daff-illuminate($base-contrast, $neutral, 4); + border: 1px solid theming.daff-illuminate($base, $neutral, 6); + color: theming.daff-illuminate($base, $neutral, 6); - &:focus { - border: 1px solid $base-contrast; + &.daff-focus { + border: 1px solid theming.daff-color($primary, 'default'); } &.daff-error { border: 1px solid theming.daff-color(theming.$daff-red, 60); - - &:focus { - border: 1px solid theming.daff-color(theming.$daff-red, 60); - } } &.daff-valid { @@ -33,6 +29,17 @@ color: $base-contrast; } } + + &:has(*:disabled), + &:has(*.isDisabled) { + border: 1px solid theming.daff-illuminate($base, $neutral, 4); + color: theming.daff-illuminate($base, $neutral, 4); + } + + *:disabled, + .isDisabled { + background-color: transparent; + } } } } diff --git a/libs/design/src/atoms/form/form-field/form-field/form-field.component.html b/libs/design/src/atoms/form/form-field/form-field/form-field.component.html index 837e5e0640..5dff25d379 100644 --- a/libs/design/src/atoms/form/form-field/form-field/form-field.component.html +++ b/libs/design/src/atoms/form/form-field/form-field/form-field.component.html @@ -1,7 +1,24 @@ -
- -
- -
+
+ + + + +
+ +
+ + +
- +@if (_control?.ngControl?.touched) { + +} \ No newline at end of file diff --git a/libs/design/src/atoms/form/form-field/form-field/form-field.component.scss b/libs/design/src/atoms/form/form-field/form-field/form-field.component.scss index 48396d0826..f8a36d0f4b 100644 --- a/libs/design/src/atoms/form/form-field/form-field/form-field.component.scss +++ b/libs/design/src/atoms/form/form-field/form-field/form-field.component.scss @@ -3,19 +3,81 @@ position: relative; &__control { - border-radius: 3px; - display: inline-block; - font-size: 1rem; + border-radius: 4px; + display: flex; + font-size: 16px; height: inherit; - line-height: 1.5rem; - padding: 10px 15px; + line-height: 16px; + padding: 8px 16px; + max-width: 320px; width: 100%; + position: relative; + + label { + position: absolute; + left: 16px; + top: 50%; + transform: translateY(-50%); + transition: all 0.2s ease; + pointer-events: none; + } + + input { + padding-top: 16px; + + &:placeholder-shown ~ label, + &:focus ~ label { + top: 6px; + font-size: 12px; + transform: none; + } + } + + &.daff-filled label { + top: 6px; + font-size: 12px; + transform: none; + } } &__icon { display: inline-block; pointer-events: none; position: absolute; - right: 15px; - } + top: 50%; + transform: translateY(-50%); + + &--prefix { + left: 16px; + } + + &--suffix { + right: 16px; + } + } + + :has(fa-icon.prefix) > label, + :has(fa-icon.prefix) > input { + padding-left: 28px; + } + + .hint { + position: absolute; + top: 100%; + left: 16px; + margin-top: 8px; + font-size: 12px; + } + + &:has(.hint) { + margin-bottom: 24px; + + daff-error-message { + margin-top: 24px; + } + + &:has(daff-error-message) { + margin-bottom: 0px; + } + } } diff --git a/libs/design/src/atoms/form/form-field/form-field/form-field.component.spec.ts b/libs/design/src/atoms/form/form-field/form-field/form-field.component.spec.ts index 6bfc53a351..c56fc3b187 100644 --- a/libs/design/src/atoms/form/form-field/form-field/form-field.component.spec.ts +++ b/libs/design/src/atoms/form/form-field/form-field/form-field.component.spec.ts @@ -33,7 +33,7 @@ describe('@daffodil/design | DaffFormFieldComponent | Usage', () => { let component: DaffFormFieldComponent; let fixture: ComponentFixture; let formFieldControlElement: HTMLElement; - let control: DaffFormFieldControl; + let control: DaffFormFieldControl; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ diff --git a/libs/design/src/atoms/form/form-field/form-field/form-field.component.ts b/libs/design/src/atoms/form/form-field/form-field/form-field.component.ts index f4eb95ed2d..f31483ff14 100644 --- a/libs/design/src/atoms/form/form-field/form-field/form-field.component.ts +++ b/libs/design/src/atoms/form/form-field/form-field/form-field.component.ts @@ -21,7 +21,7 @@ import { DaffFormFieldMissingControlMessage } from '../form-field-errors'; styleUrls: ['./form-field.component.scss'], encapsulation: ViewEncapsulation.None, }) -export class DaffFormFieldComponent implements DoCheck, AfterContentInit, AfterContentChecked { +export class DaffFormFieldComponent implements AfterContentInit, AfterContentChecked { /** * @docs-private @@ -44,7 +44,7 @@ export class DaffFormFieldComponent implements DoCheck, AfterContentInit, AfterC * * @docs-private */ - @ContentChild(DaffFormFieldControl) _control: DaffFormFieldControl; + @ContentChild(DaffFormFieldControl) _control: DaffFormFieldControl; /** * Tracking property to keep a record of whether or not the @@ -52,6 +52,12 @@ export class DaffFormFieldComponent implements DoCheck, AfterContentInit, AfterC */ isError = false; + /** + * Tracking property to keep a record of whether or not the + * form field contains any user input. + */ + isFilled = false; + /** * Tracking property to keep a record of whether or not the * form field should be marked as valid. @@ -67,21 +73,6 @@ export class DaffFormFieldComponent implements DoCheck, AfterContentInit, AfterC return this._control?.focused; } - /** - * Keeps the state of the form field consistent with its child DaffFormControl - * - * TODO: consider whether or not this can be refactored to some kind of - * observable to remove unnecessary change detection. - * - * @docs-private - */ - ngDoCheck() { - if(this._control?.ngControl) { - this.isError = this._control.ngControl.errors && (this._control.ngControl.touched); - this.isValid = !this._control.ngControl.errors && this._control.ngControl.touched; - } - } - /** * Validate whether or not the FormField is in * a "usable" state. @@ -112,5 +103,10 @@ export class DaffFormFieldComponent implements DoCheck, AfterContentInit, AfterC */ ngAfterContentChecked() { this._validateFormControl(); + if (this._control?.ngControl) { + this.isError = this._control.ngControl.errors && (this._control.ngControl.touched); + this.isValid = !this._control.ngControl.errors && this._control.ngControl.touched; + } + this.isFilled = !!this._control.value; } } diff --git a/libs/design/src/atoms/form/input/input-theme.scss b/libs/design/src/atoms/form/input/input-theme.scss index c0f74ecb99..474859c6c8 100644 --- a/libs/design/src/atoms/form/input/input-theme.scss +++ b/libs/design/src/atoms/form/input/input-theme.scss @@ -5,12 +5,8 @@ $base: core.daff-map-deep-get($theme, 'core.base'); $base-contrast: core.daff-map-deep-get($theme, 'core.base-contrast'); - :host { + .daff-input { background: $base; color: $base-contrast; - - &::placeholder { - color: transparent; - } } } diff --git a/libs/design/src/atoms/form/input/input.component.scss b/libs/design/src/atoms/form/input/input.component.scss index 10416d38ec..fce43549f0 100644 --- a/libs/design/src/atoms/form/input/input.component.scss +++ b/libs/design/src/atoms/form/input/input.component.scss @@ -14,4 +14,8 @@ box-shadow: none; outline: none; } + + &:disabled { + cursor: not-allowed; + } } diff --git a/libs/design/src/atoms/form/input/input.component.ts b/libs/design/src/atoms/form/input/input.component.ts index a19241ee4c..eb15877bee 100644 --- a/libs/design/src/atoms/form/input/input.component.ts +++ b/libs/design/src/atoms/form/input/input.component.ts @@ -6,6 +6,7 @@ import { ElementRef, HostListener, ChangeDetectionStrategy, + HostBinding, } from '@angular/core'; import { NgControl } from '@angular/forms'; @@ -25,7 +26,9 @@ import { DaffFormFieldControl } from '../form-field/form-field-control'; { provide: DaffFormFieldControl, useExisting: DaffInputComponent }, ], }) -export class DaffInputComponent implements DaffFormFieldControl { +export class DaffInputComponent implements DaffFormFieldControl { + + @HostBinding('class.daff-input') class = true; /** * Has the form been submitted. @@ -59,4 +62,8 @@ export class DaffInputComponent implements DaffFormFieldControl { onFocus() { this._elementRef.nativeElement.focus(); } + + get value() { + return this._elementRef.nativeElement.value; + } } diff --git a/libs/design/src/atoms/form/native-select/native-select.component.ts b/libs/design/src/atoms/form/native-select/native-select.component.ts index 27015edf34..73a26d99a5 100644 --- a/libs/design/src/atoms/form/native-select/native-select.component.ts +++ b/libs/design/src/atoms/form/native-select/native-select.component.ts @@ -26,7 +26,7 @@ import { DaffFormFieldControl } from '../form-field/form-field-control'; ], }) -export class DaffNativeSelectComponent implements DaffFormFieldControl { +export class DaffNativeSelectComponent implements DaffFormFieldControl { /** * @docs-private */ @@ -66,4 +66,7 @@ export class DaffNativeSelectComponent implements DaffFormFieldControl { this._elementRef.nativeElement.focus(); } + get value() { + return this._elementRef.nativeElement.value; + } } diff --git a/libs/design/src/atoms/form/quantity-field/quantity-field.component.ts b/libs/design/src/atoms/form/quantity-field/quantity-field.component.ts index eece940b35..b2d2062b91 100644 --- a/libs/design/src/atoms/form/quantity-field/quantity-field.component.ts +++ b/libs/design/src/atoms/form/quantity-field/quantity-field.component.ts @@ -29,7 +29,7 @@ import { DaffFormFieldControl } from '../form-field/form-field-control'; ], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class DaffQuantityFieldComponent implements ControlValueAccessor, DaffFormFieldControl { +export class DaffQuantityFieldComponent implements ControlValueAccessor, DaffFormFieldControl { @ViewChild(DaffQuantityInputComponent) input: DaffQuantityInputComponent; @ViewChild(DaffQuantitySelectComponent) select: DaffQuantitySelectComponent; @@ -139,4 +139,13 @@ export class DaffQuantityFieldComponent implements ControlValueAccessor, DaffFor this.input.focus(); } } + + get value() { + if(this.select) { + return this.select.value; + } + if(this.input) { + return this.input.value; + } + } }