diff --git a/apps/design-land/src/app/form/form.component.html b/apps/design-land/src/app/form/form.component.html index 3e49be8dd2..201388ae2b 100644 --- a/apps/design-land/src/app/form/form.component.html +++ b/apps/design-land/src/app/form/form.component.html @@ -2,8 +2,7 @@ + [formControl]="form.controls['inputTest1']"> This is a required field >:(
diff --git a/apps/design-land/src/app/input/input.component.html b/apps/design-land/src/app/input/input.component.html index 9f30c8d256..5f559b4675 100644 --- a/apps/design-land/src/app/input/input.component.html +++ b/apps/design-land/src/app/input/input.component.html @@ -1,24 +1,23 @@

Input

-

Input is a form control element that can be used in forms.

+

The input component allows a native HTML input element to work with the form field component.

-

Examples

- -

Basic

-

A basic input without using the DaffFormFieldComponent.

- - - -

With DaffFormFieldComponent

-

An input using DaffFormField

+

Overview

+

The input component has the same functionality as a native HTML <input> element, with additional custom styling and functionality. It can't be used by itself and must be contained within a <daff-form-field>.

+

Basic input with form field

-

Disabled

+

Disabled input

The input in this example is disabled using the native HTML disabled attribute.

-

With Reactive Forms

+

Input with error messages

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

- \ No newline at end of file + + +

Input with hint

+

The input in this example has a hint.

+ + diff --git a/libs/design/input/examples/src/basic-input/basic-input.component.ts b/libs/design/input/examples/src/basic-input/basic-input.component.ts index 299731fdda..cea1ce8471 100644 --- a/libs/design/input/examples/src/basic-input/basic-input.component.ts +++ b/libs/design/input/examples/src/basic-input/basic-input.component.ts @@ -10,8 +10,9 @@ import { DaffInputModule } from '@daffodil/design'; selector: 'basic-input', templateUrl: './basic-input.component.html', changeDetection: ChangeDetectionStrategy.OnPush, - imports: [DaffInputModule], + standalone: true, + imports: [ + DaffInputModule, + ], }) -export class BasicInputComponent { - -} +export class BasicInputComponent { } 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..21f1998567 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 f344bc317f..7dd46a688f 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 @@ -2,6 +2,10 @@ import { ChangeDetectionStrategy, Component, } from '@angular/core'; +import { + ReactiveFormsModule, + UntypedFormControl, +} from '@angular/forms'; import { DaffFormFieldModule, @@ -12,9 +16,19 @@ import { // eslint-disable-next-line @angular-eslint/component-selector selector: 'input-disabled', templateUrl: './input-disabled.component.html', + styles: [` + daff-form-field { + max-width: 320px; + } + `], changeDetection: ChangeDetectionStrategy.OnPush, - imports: [DaffFormFieldModule, DaffInputModule], + standalone: true, + imports: [ + ReactiveFormsModule, + DaffFormFieldModule, + DaffInputModule, + ], }) export class InputDisabledComponent { - + disabled = new UntypedFormControl({ value : '' , disabled: 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..506edf187a 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,11 @@ - + + + @if (control.errors?.required) { + Email is a required field. + } + @if (control.errors?.email) { + Email is not valid. + }

Value: {{ control.value }}

\ No newline at end of file diff --git a/libs/design/input/examples/src/input-error/input-error.component.ts b/libs/design/input/examples/src/input-error/input-error.component.ts index b659aeea55..966057925c 100644 --- a/libs/design/input/examples/src/input-error/input-error.component.ts +++ b/libs/design/input/examples/src/input-error/input-error.component.ts @@ -10,6 +10,7 @@ import { import { DaffFormFieldModule, + DaffHintComponent, DaffInputModule, } from '@daffodil/design'; @@ -17,10 +18,16 @@ import { // eslint-disable-next-line @angular-eslint/component-selector selector: 'input-error', templateUrl: './input-error.component.html', + styles: [` + daff-form-field { + max-width: 320px; + } + `], changeDetection: ChangeDetectionStrategy.OnPush, imports: [ DaffFormFieldModule, DaffInputModule, + DaffHintComponent, ReactiveFormsModule, ], }) 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..d4fdf9d9a3 --- /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..a2057dee67 --- /dev/null +++ b/libs/design/input/examples/src/input-hint/input-hint.component.ts @@ -0,0 +1,30 @@ +import { + ChangeDetectionStrategy, + Component, +} from '@angular/core'; + +import { + DaffFormFieldModule, + DaffHintComponent, + DaffInputModule, +} from '@daffodil/design'; + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'input-hint', + templateUrl: './input-hint.component.html', + styles: [` + daff-form-field { + max-width: 320px; + } + `], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + DaffFormFieldModule, + DaffInputModule, + DaffHintComponent, + ], +}) +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..87274b93ea 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 ffc7b9de8e..a81e32d0c5 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,19 +2,37 @@ import { ChangeDetectionStrategy, Component, } from '@angular/core'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { + faUser, + faCircleXmark, +} from '@fortawesome/free-solid-svg-icons'; import { DaffFormFieldModule, DaffInputModule, + DaffPrefixSuffixModule, } from '@daffodil/design'; @Component({ // eslint-disable-next-line @angular-eslint/component-selector selector: 'input-with-form-field', templateUrl: './input-with-form-field.component.html', + styles: [` + daff-form-field { + max-width: 320px; + } + `], changeDetection: ChangeDetectionStrategy.OnPush, - imports: [DaffFormFieldModule, DaffInputModule], + standalone: true, + imports: [ + DaffFormFieldModule, + DaffInputModule, + FaIconComponent, + DaffPrefixSuffixModule, + ], }) export class InputWithFormFieldComponent { - + faUser = faUser; + faCircleXmark = faCircleXmark; } diff --git a/libs/design/scss/theme.scss b/libs/design/scss/theme.scss index 3e73b43885..d30924a649 100644 --- a/libs/design/scss/theme.scss +++ b/libs/design/scss/theme.scss @@ -27,6 +27,7 @@ @use '../button/src/button/underline/underline-theme' as underline-button; @use '../article/src/article-theme' as article; @use '../src/atoms/form/error-message/error-message-theme' as error-message; +@use '../src/atoms/form/hint/hint-theme' as hint; @use '../src/atoms/form/form-field/form-field/form-field-theme' as form-field; @use '../src/atoms/form/input/input-theme' as input; @use '../src/atoms/form/native-select/native-select-theme' as native-select; @@ -77,6 +78,7 @@ @include underline-button.daff-underline-button-theme($theme); @include breadcrumb.daff-breadcrumb-theme($theme); @include error-message.daff-error-message-theme($theme); + @include hint.daff-hint-theme($theme); @include form-field.daff-form-field-theme($theme); @include input.daff-input-theme($theme); @include native-select.daff-native-select-theme($theme); 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..cd9ba709e5 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,4 @@ :host { display: block; font-size: t.$small-font-size; - margin-top: 5px; } diff --git a/libs/design/src/atoms/form/form-field/behaviors/disableable/disableable-interface.ts b/libs/design/src/atoms/form/form-field/behaviors/disableable/disableable-interface.ts deleted file mode 100644 index 83b59b74b6..0000000000 --- a/libs/design/src/atoms/form/form-field/behaviors/disableable/disableable-interface.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { DaffFormFieldControl } from '../../form-field-control'; - -/** - * A behavior interface for tracking the disabled state of a form field. - */ -export interface DaffDisableable { - _control: DaffFormFieldControl; - disabled: boolean; -} diff --git a/libs/design/src/atoms/form/form-field/behaviors/focusable/focusable-interface.ts b/libs/design/src/atoms/form/form-field/behaviors/focusable/focusable-interface.ts deleted file mode 100644 index 10dd65e5e8..0000000000 --- a/libs/design/src/atoms/form/form-field/behaviors/focusable/focusable-interface.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { DaffFormFieldControl } from '../../form-field-control'; - -/** - * A behavior interface for tracking the focused state of a form field. - */ -export interface DaffFocusable { - _control: DaffFormFieldControl; - focused: boolean; -} 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..9486140d5b 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 @@ -1,4 +1,10 @@ import { NgControl } from '@angular/forms'; +import { + BehaviorSubject, + Observable, +} from 'rxjs'; + +import { DaffFormFieldState } from './form-field-state'; /** * @@ -11,12 +17,39 @@ 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 { - readonly ngControl: NgControl | null; - - readonly controlType?: any; +export abstract class DaffFormFieldControl { + abstract readonly controlType?: any; - readonly focused: boolean; + abstract readonly focused: boolean; abstract focus(event?: Event): void; + + abstract readonly value: T; + + constructor(public ngControl: NgControl | null) { + } + + get state(): DaffFormFieldState { + return { + focused: this.focused, + filled: !!this.value, + disabled: this.ngControl?.disabled, + error: this.ngControl?.errors && (this.ngControl?.dirty || this.ngControl?.touched), + valid: !this.ngControl?.errors && (this.ngControl?.dirty || this.ngControl?.touched), + }; + } + + _stateChanges = new BehaviorSubject({ + focused: false, + filled: false, + disabled: false, + error: false, + valid: true, + }); + + stateChanges: Observable; + + emitState() { + this._stateChanges.next(this.state); + } }; diff --git a/libs/design/src/atoms/form/form-field/form-field-state.ts b/libs/design/src/atoms/form/form-field/form-field-state.ts new file mode 100644 index 0000000000..53ab81a071 --- /dev/null +++ b/libs/design/src/atoms/form/form-field/form-field-state.ts @@ -0,0 +1,7 @@ +export interface DaffFormFieldState { + focused: boolean; + filled: boolean; + disabled: boolean; + error: boolean; + valid: boolean; +} diff --git a/libs/design/src/atoms/form/form-field/form-field.module.ts b/libs/design/src/atoms/form/form-field/form-field.module.ts index 2b061a3916..15238ff71f 100644 --- a/libs/design/src/atoms/form/form-field/form-field.module.ts +++ b/libs/design/src/atoms/form/form-field/form-field.module.ts @@ -1,20 +1,21 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; -import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { DaffFormFieldComponent } from './form-field/form-field.component'; import { DaffErrorMessageModule } from '../error-message/error-message.module'; +import { DaffFormLabelModule } from '../form-label/form-label.module'; @NgModule({ imports: [ CommonModule, - FontAwesomeModule, DaffErrorMessageModule, + DaffFormLabelModule, ], exports: [ DaffFormFieldComponent, DaffErrorMessageModule, + DaffFormLabelModule, ], declarations: [ DaffFormFieldComponent, 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 4a11e8002a..2bee19605b 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 @@ -6,6 +6,7 @@ $primary: core.daff-map-get($theme, primary); $secondary: core.daff-map-get($theme, secondary); $tertiary: core.daff-map-get($theme, tertiary); + $critical: core.daff-map-get($theme, critical); $base: core.daff-map-get($theme, 'core', 'base'); $base-contrast: core.daff-map-get($theme, 'core', 'base-contrast'); $neutral: core.daff-map-get($theme, 'core', 'neutral'); @@ -13,26 +14,34 @@ .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-focused { + border: 1px solid theming.daff-color($primary); + + .daff-form-label { + color: theming.daff-color($primary); + } } &.daff-error { - border: 1px solid theming.daff-color(theming.$daff-red, 60); + border: 1px solid theming.daff-color($critical); - &:focus { - border: 1px solid theming.daff-color(theming.$daff-red, 60); + .daff-form-label { + color: theming.daff-color($critical); } } &.daff-valid { - > * { - color: $base-contrast; - } + color: $base-contrast; } + + &.daff-disabled { + background-color: transparent; + border: 1px solid theming.daff-illuminate($base, $neutral, 4); + color: theming.daff-illuminate($base, $neutral, 4); + } } } } 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..7c23b2dc7b 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,23 @@ -
- -
- -
+
+ @if (_prefix) { + + } +
+ + + @if (isSelectField) { +
+ } +
+ @if (_suffix) { + + }
+ \ 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..8f0c39e70f 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 @@ -1,21 +1,79 @@ +@use '../../../../../scss/typography' as t; + .daff-form-field { display: block; position: relative; &__control { - border-radius: 3px; - display: inline-block; - font-size: 1rem; - height: inherit; - line-height: 1.5rem; - padding: 10px 15px; - width: 100%; - } + display: flex; + align-items: center; + border-radius: 0.25rem; + box-sizing: border-box; + font-size: t.$font-size-base; + height: 3.5rem; + line-height: 1rem; + position: relative; + width: 100%; + + * { + ::-webkit-input-placeholder { + opacity: 0; + } + } + + .daff-form-label { + position: absolute; + left: 1rem; + top: 1.25rem; + pointer-events: none; + transition: all 200ms ease; + } + + &.daff-focused, + &.daff-filled { + .daff-form-label { + font-size: 0.75rem; + top: 0.375rem; + } - &__icon { - display: inline-block; - pointer-events: none; - position: absolute; - right: 15px; + * { + ::-webkit-input-placeholder { + opacity: 1; + } + } + } } + + &__icon { + content: ''; + position: absolute; + top: 48%; + right: 1rem; + display: inline-block; + border-right: 2px solid currentColor; + border-bottom: 2px solid currentColor; + width: 0.5rem; + height: 0.5rem; + transform: translateY(-50%) rotate(45deg); + transition: transform 150ms; + } + + &__form-wrapper { + flex-grow: 1; + padding: 1.5rem 1rem 0.5rem; + position: relative; + } + + .daff-hint, + .daff-error-message { + margin-top: 0.25rem; + } + + .daff-prefix { + padding-left: 1rem; + } + + .daff-suffix { + padding-right: 1rem; + } } 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 1e48aeeca5..cea9c76b3b 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 @@ -18,16 +18,13 @@ import { DaffInputModule } from '../../input/public_api'; import { DaffFormFieldControl } from '../form-field-control'; import { DaffFormFieldMissingControlMessage } from '../form-field-errors'; -@Component({ - template: ` - +@Component({ template: ` + `, - standalone: false, -}) +standalone: false }) class WrapperComponent { - formSubmittedValue: boolean; formControl = new UntypedFormControl('', Validators.required); } @@ -36,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({ @@ -120,17 +117,14 @@ describe('@daffodil/design | DaffFormFieldComponent | Usage', () => { }); -@Component({ - template: ` - +@Component({ template: ` + `, - standalone: false, -}) +standalone: false }) + +class WrapperWithoutControlComponent {} -class WrapperWithoutControlComponent { - formSubmittedValue: boolean; -} describe('@daffodil/design | DaffFormFieldComponent | Usage Without Control', () => { let fixture: ComponentFixture; 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 022b63be3a..5290f775ef 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 @@ -1,51 +1,48 @@ import { Component, ViewEncapsulation, - DoCheck, ContentChild, - Input, AfterContentInit, AfterContentChecked, HostBinding, + ChangeDetectorRef, + ChangeDetectionStrategy, } from '@angular/core'; -import { faChevronDown } from '@fortawesome/free-solid-svg-icons'; +import { DaffPrefixDirective } from '../../../../core/prefix-suffix/prefix.directive'; +import { DaffSuffixDirective } from '../../../../core/prefix-suffix/suffix.directive'; import { DaffFormFieldControl } from '../form-field-control'; import { DaffFormFieldMissingControlMessage } from '../form-field-errors'; -// ChangeDetection is ignored because this component needs to be refactored -// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: 'daff-form-field', templateUrl: './form-field.component.html', styleUrls: ['./form-field.component.scss'], encapsulation: ViewEncapsulation.None, standalone: false, + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class DaffFormFieldComponent implements DoCheck, AfterContentInit, AfterContentChecked { +export class DaffFormFieldComponent implements AfterContentInit, AfterContentChecked { + /** @docs-private */ + @HostBinding('class.daff-form-field') class = true; - /** - * @docs-private - */ - faChevronDown = faChevronDown; + /** @docs-private */ + get isSelectField(): boolean { + return this._control.controlType === 'native-select'; + } - @HostBinding('class.daff-form-field') class = true; + /** @docs-private */ + @ContentChild(DaffPrefixDirective) _prefix: DaffPrefixDirective; - /** - * The tracking property used to determine if the parent form has been submitted, - * and thus show an error message (even if the field hasn't been touched). - * - * @deprecated Deprecated in version 0.78.0. Will be removed in version 1.0.0. - */ - // eslint-disable-next-line @typescript-eslint/no-inferrable-types - @Input() formSubmitted: boolean = false; + /** @docs-private */ + @ContentChild(DaffSuffixDirective) _suffix: DaffSuffixDirective; /** - * The child form control that the form-field manages + * The child form control that the form field manages. * * @docs-private */ - @ContentChild(DaffFormFieldControl) _control: DaffFormFieldControl; + @ContentChild(DaffFormFieldControl) _control: DaffFormFieldControl; /** * Tracking property to keep a record of whether or not the @@ -53,15 +50,23 @@ 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; + + isDisabled = false; + /** * Tracking property to keep a record of whether or not the * form field should be marked as valid. */ isValid = false; + constructor(private cd: ChangeDetectorRef) {} + /** - * @docs - * * Determines whether or not the form field should display its focused state. */ get isFocused() { @@ -69,23 +74,7 @@ export class DaffFormFieldComponent implements DoCheck, AfterContentInit, AfterC } /** - * 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. + * Validate whether or not the FormField is in a "usable" state. */ private _validateFormControl() { if (!this._control) { @@ -102,6 +91,15 @@ export class DaffFormFieldComponent implements DoCheck, AfterContentInit, AfterC */ ngAfterContentInit() { this._validateFormControl(); + + this._control.stateChanges?.subscribe(({ focused, filled, disabled, error, valid }) => { + this.isFilled = filled; + this.isError = error; + this.isDisabled = disabled; + this.isValid = valid; + + this.cd.markForCheck(); + }); } /** diff --git a/libs/design/src/atoms/form/hint/hint-theme.scss b/libs/design/src/atoms/form/hint/hint-theme.scss new file mode 100644 index 0000000000..1e1c675946 --- /dev/null +++ b/libs/design/src/atoms/form/hint/hint-theme.scss @@ -0,0 +1,11 @@ +@use '../../../../scss/core'; +@use '../../../../scss/theming'; + +@mixin daff-hint-theme($theme) { + $base-contrast: core.daff-map-get($theme, 'core', 'base-contrast'); + $neutral: core.daff-map-get($theme, 'core', 'neutral'); + + .daff-hint { + color: theming.daff-illuminate($base-contrast, $neutral, 2); + } +} diff --git a/libs/design/src/atoms/form/hint/hint.component.scss b/libs/design/src/atoms/form/hint/hint.component.scss new file mode 100644 index 0000000000..cd9ba709e5 --- /dev/null +++ b/libs/design/src/atoms/form/hint/hint.component.scss @@ -0,0 +1,6 @@ +@use '../../../../scss/typography' as t; + +:host { + display: block; + font-size: t.$small-font-size; +} diff --git a/libs/design/src/atoms/form/hint/hint.component.spec.ts b/libs/design/src/atoms/form/hint/hint.component.spec.ts new file mode 100644 index 0000000000..d41505bedd --- /dev/null +++ b/libs/design/src/atoms/form/hint/hint.component.spec.ts @@ -0,0 +1,54 @@ +import { + Component, + DebugElement, +} from '@angular/core'; +import { + waitForAsync, + ComponentFixture, + TestBed, +} from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +import { DaffHintComponent } from './hint.component'; + +@Component({ + template: `Hint`, + standalone: true, + imports: [ + DaffHintComponent, + ], +}) + +class WrapperComponent {} + +describe('@daffodil/design | DaffHintComponent', () => { + let wrapper: WrapperComponent; + let de: DebugElement; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + WrapperComponent, + ], + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(WrapperComponent); + wrapper = fixture.componentInstance; + de = fixture.debugElement.query(By.css('daff-hint')); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(wrapper).toBeTruthy(); + }); + + it('should add a class of "daff-hint" to the host element', () => { + expect(de.classes).toEqual(jasmine.objectContaining({ + 'daff-hint': true, + })); + }); +}); diff --git a/libs/design/src/atoms/form/hint/hint.component.ts b/libs/design/src/atoms/form/hint/hint.component.ts new file mode 100644 index 0000000000..1fa5047d54 --- /dev/null +++ b/libs/design/src/atoms/form/hint/hint.component.ts @@ -0,0 +1,16 @@ +import { + Component, + ChangeDetectionStrategy, + HostBinding, +} from '@angular/core'; + +@Component({ + selector: 'daff-hint', + template: '', + styleUrls: ['./hint.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, +}) +export class DaffHintComponent { + @HostBinding('class.daff-hint') class = true; +} diff --git a/libs/design/src/atoms/form/hint/public_api.ts b/libs/design/src/atoms/form/hint/public_api.ts new file mode 100644 index 0000000000..6b04b60aed --- /dev/null +++ b/libs/design/src/atoms/form/hint/public_api.ts @@ -0,0 +1 @@ +export { DaffHintComponent } from './hint.component'; diff --git a/libs/design/src/atoms/form/input/README.md b/libs/design/src/atoms/form/input/README.md index 01f1f693a1..2a22a1f00b 100644 --- a/libs/design/src/atoms/form/input/README.md +++ b/libs/design/src/atoms/form/input/README.md @@ -1,24 +1,25 @@ # Input -Input is a form control element that can be used in forms. +The input component allows a native HTML input element to work with the form field component. -## Examples - -### Basic -A basic input without using the `DaffFormFieldComponent`. - - - -### With `DaffFormFieldComponent` -An input using `DaffFormField` +## Overview +The input component has the same functionality as a native HTML `` element, with additional custom styling and functionality. It can't be used by itself and must be contained within a [``](/libs/design/src/atoms/form/form-field/README.md). +## Basic input with form field -### Disabled +## Examples + +### Disabled input The input in this example is disabled using the native HTML disabled attribute. -### With Reactive Forms +### Input with error messages The input in this example uses the `ReactiveFormsModule` to display errors. - \ No newline at end of file + + +### Input with hint +The input in this example has a hint. + + diff --git a/libs/design/src/atoms/form/input/input-theme.scss b/libs/design/src/atoms/form/input/input-theme.scss index f3677e4e20..ef2ac4c94f 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-get($theme, 'core', 'base'); $base-contrast: core.daff-map-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.spec.ts b/libs/design/src/atoms/form/input/input.component.spec.ts index 61558062d1..5d5c0d468b 100644 --- a/libs/design/src/atoms/form/input/input.component.spec.ts +++ b/libs/design/src/atoms/form/input/input.component.spec.ts @@ -13,18 +13,15 @@ import { By } from '@angular/platform-browser'; import { DaffInputComponent } from './input.component'; @Component({ - template: ``, + template: ``, standalone: false, }) -class WrapperComponent { - formSubmittedValue: boolean; -} +class WrapperComponent {} describe('@daffodil/design | DaffInputComponent', () => { let wrapper: WrapperComponent; let fixture: ComponentFixture; let stubFormControl; - let stubFormSubmitted: boolean; let component: DaffInputComponent; let componentDE: DebugElement; @@ -40,11 +37,9 @@ describe('@daffodil/design | DaffInputComponent', () => { beforeEach(() => { stubFormControl = new UntypedFormControl(); - stubFormSubmitted = false; fixture = TestBed.createComponent(WrapperComponent); wrapper = fixture.componentInstance; - wrapper.formSubmittedValue = stubFormSubmitted; fixture.detectChanges(); componentDE = fixture.debugElement.query(By.css('[daff-input]')); @@ -55,10 +50,6 @@ describe('@daffodil/design | DaffInputComponent', () => { expect(wrapper).toBeTruthy(); }); - it('should be able to take formSubmitted as input', () => { - expect(component.formSubmitted).toEqual(stubFormSubmitted); - }); - describe('onFocus', () => { it('should call focus on the native element', () => { spyOn(componentDE.nativeElement, 'focus'); diff --git a/libs/design/src/atoms/form/input/input.component.ts b/libs/design/src/atoms/form/input/input.component.ts index 47311dfaa2..497a213278 100644 --- a/libs/design/src/atoms/form/input/input.component.ts +++ b/libs/design/src/atoms/form/input/input.component.ts @@ -1,13 +1,19 @@ import { Component, - Input, Optional, Self, ElementRef, HostListener, ChangeDetectionStrategy, + HostBinding, + OnInit, } from '@angular/core'; import { NgControl } from '@angular/forms'; +import { + merge, + of, + map, +} from 'rxjs'; import { DaffFormFieldControl } from '../form-field/form-field-control'; @@ -18,7 +24,7 @@ import { DaffFormFieldControl } from '../form-field/form-field-control'; // eslint-disable-next-line @angular-eslint/component-selector selector: 'input[daff-input]', template: '', - styleUrls: ['./input.component.scss'], + styleUrl: './input.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, providers: [ // eslint-disable-next-line @typescript-eslint/no-use-before-define @@ -26,38 +32,51 @@ import { DaffFormFieldControl } from '../form-field/form-field-control'; ], standalone: false, }) -export class DaffInputComponent implements DaffFormFieldControl { +export class DaffInputComponent extends DaffFormFieldControl implements DaffFormFieldControl, OnInit { + + /** @docs-private */ + @HostBinding('class.daff-input') class = true; - /** - * Has the form been submitted. - */ - @Input() formSubmitted: boolean; + /** @docs-private */ + controlType = 'native-input'; focused = false; - /** - * @docs-private - */ + /** @docs-private */ @HostListener('focus') focus() { this.focused = true; + this.emitState(); + } - /** - * @docs-private - */ + /** @docs-private */ @HostListener('blur') blur() { this.focused = false; + this.emitState(); } constructor( - /** - * @docs-private - */ + /** @docs-private */ @Optional() @Self() public ngControl: NgControl, private _elementRef: ElementRef, - ) {} + ) { + super(ngControl); + } + + ngOnInit() { + this.stateChanges = merge( + this._stateChanges.asObservable(), + this.ngControl ? this.ngControl.statusChanges : of(undefined), + ).pipe( + map(() => this.state), + ); + } 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 3533cd6a93..95bf559992 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 @@ -27,12 +27,15 @@ import { DaffFormFieldControl } from '../form-field/form-field-control'; standalone: false, }) -export class DaffNativeSelectComponent implements DaffFormFieldControl { +export class DaffNativeSelectComponent extends DaffFormFieldControl implements DaffFormFieldControl { /** * @docs-private */ controlType = 'native-select'; + /** + * @docs-private + */ @HostBinding('class.daff-native-select') class = true; /** @@ -46,6 +49,7 @@ export class DaffNativeSelectComponent implements DaffFormFieldControl { */ @HostListener('focus') focus() { this.focused = true; + this.emitState(); } /** @@ -53,6 +57,7 @@ export class DaffNativeSelectComponent implements DaffFormFieldControl { */ @HostListener('blur') blur() { this.focused = false; + this.emitState(); } constructor( @@ -61,10 +66,15 @@ export class DaffNativeSelectComponent implements DaffFormFieldControl { */ @Optional() @Self() public ngControl: NgControl, private _elementRef: ElementRef, - ) {} + ) { + super(ngControl); + } onFocus() { 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 41777c1927..ff77d05629 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 @@ -30,7 +30,7 @@ import { DaffFormFieldControl } from '../form-field/form-field-control'; changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, }) -export class DaffQuantityFieldComponent implements ControlValueAccessor, DaffFormFieldControl { +export class DaffQuantityFieldComponent extends DaffFormFieldControl implements ControlValueAccessor, DaffFormFieldControl { @ViewChild(DaffQuantityInputComponent) input: DaffQuantityInputComponent; @ViewChild(DaffQuantitySelectComponent) select: DaffQuantitySelectComponent; @@ -107,6 +107,8 @@ export class DaffQuantityFieldComponent implements ControlValueAccessor, DaffFor @Optional() @Self() public ngControl: NgControl, private cd: ChangeDetectorRef, ) { + super(ngControl); + if (this.ngControl != null) { this.ngControl.valueAccessor = this; } @@ -140,4 +142,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; + } + } } diff --git a/libs/design/src/public_api.ts b/libs/design/src/public_api.ts index d9b735fea0..4727bc7835 100644 --- a/libs/design/src/public_api.ts +++ b/libs/design/src/public_api.ts @@ -6,6 +6,7 @@ export * from './atoms/form/core/public_api'; export * from './atoms/form/form-field/public_api'; export * from './atoms/form/error-message/public_api'; +export * from './atoms/form/hint/public_api'; export * from './atoms/form/input/public_api'; export * from './atoms/form/native-select/public_api'; export * from './atoms/form/checkbox/public_api';