Skip to content

Commit

Permalink
update
Browse files Browse the repository at this point in the history
  • Loading branch information
LinboLen committed May 17, 2024
1 parent d69ab42 commit 5b44928
Show file tree
Hide file tree
Showing 26 changed files with 1,354 additions and 989 deletions.
19 changes: 9 additions & 10 deletions libs/triangle/form-field/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@
*/


export * from './src/form-field-module';
export * from './src/error';
export * from './src/form-field-component';
export { TriFormFieldControl } from './src/form-field-control';

export * from './src/directives/label';
export * from './src/directives/error';
export * from './src/directives/hint';
export * from './src/directives/prefix';
export * from './src/directives/suffix';
export * from './src/form-field';
export * from './src/module';
export * from './src/form-field-control';
export * from './src/form-field-errors';
export * from './src/hint';
export * from './src/prefix';
export * from './src/suffix';
export * from './src/label';
export * from './src/form-field-animations';

export * from './src/input-form-field/input-form-field-directive';
41 changes: 41 additions & 0 deletions libs/triangle/form-field/src/directives/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {Attribute, Directive, ElementRef, InjectionToken, Input} from '@angular/core';

let nextUniqueId = 0;

/**
* Injection token that can be used to reference instances of `MatError`. It serves as
* alternative token to the actual `MatError` class which could cause unnecessary
* retention of the class and its directive metadata.
*/
export const TRI_ERROR = new InjectionToken<TriError>('TriError');

/** Single error message to be shown underneath the form-field. */
@Directive({
selector: 'tri-error, [triError]',
host: {
'class': 'tri-form-field-error tri-form-field-bottom-align',
'aria-atomic': 'true',
'[id]': 'id',
},
providers: [{provide: TRI_ERROR, useExisting: TriError}],
standalone: true,
})
export class TriError {
@Input() id: string = `tri-error-${nextUniqueId++}`;

constructor(@Attribute('aria-live') ariaLive: string, elementRef: ElementRef) {
// If no aria-live value is set add 'polite' as a default. This is preferred over setting
// role='alert' so that screen readers do not interrupt the current task to read this aloud.
if (!ariaLive) {
elementRef.nativeElement.setAttribute('aria-live', 'polite');
}
}
}
152 changes: 152 additions & 0 deletions libs/triangle/form-field/src/directives/floating-label.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {
Directive,
ElementRef,
inject,
Input,
NgZone,
OnDestroy,
InjectionToken,
} from '@angular/core';
import {SharedResizeObserver} from '@angular/cdk/observers/private';
import {Subscription} from 'rxjs';

/** An interface that the parent form-field should implement to receive resize events. */
export interface FloatingLabelParent {
_handleLabelResized(): void;
}

/** An injion token for the parent form-field. */
export const FLOATING_LABEL_PARENT = new InjectionToken<FloatingLabelParent>('FloatingLabelParent');

/**
* Internal directive that maintains a MDC floating label. This directive does not
* use the `MDCFloatingLabelFoundation` class, as it is not worth the size cost of
* including it just to measure the label width and toggle some classes.
*
* The use of a directive allows us to conditionally render a floating label in the
* template without having to manually manage instantiation and destruction of the
* floating label component based on.
*
* The component is responsible for setting up the floating label styles, measuring label
* width for the outline notch, and providing inputs that can be used to toggle the
* label's floating or required state.
*/
@Directive({
selector: 'label[triFormFieldFloatingLabel]',
host: {
'class': 'tri-floating-label mat-mdc-floating-label',
'[class.tri-floating-label--float-above]': 'floating',
},
standalone: true,
})
export class TriFormFieldFloatingLabel implements OnDestroy {
/** Whether the label is floating. */
@Input()
get floating() {
return this._floating;
}
set floating(value: boolean) {
this._floating = value;
if (this.monitorResize) {
this._handleResize();
}
}
private _floating = false;

/** Whether to monitor for resize events on the floating label. */
@Input()
get monitorResize() {
return this._monitorResize;
}
set monitorResize(value: boolean) {
this._monitorResize = value;
if (this._monitorResize) {
this._subscribeToResize();
} else {
this._resizeSubscription.unsubscribe();
}
}
private _monitorResize = false;

/** The shared ResizeObserver. */
private _resizeObserver = inject(SharedResizeObserver);

/** The Angular zone. */
private _ngZone = inject(NgZone);

/** The parent form-field. */
private _parent = inject(FLOATING_LABEL_PARENT);

/** The current resize event subscription. */
private _resizeSubscription = new Subscription();

constructor(private _elementRef: ElementRef<HTMLElement>) {}

ngOnDestroy() {
this._resizeSubscription.unsubscribe();
}

/** Gets the width of the label. Used for the outline notch. */
getWidth(): number {
return estimateScrollWidth(this._elementRef.nativeElement);
}

/** Gets the HTML element for the floating label. */
get element(): HTMLElement {
return this._elementRef.nativeElement;
}

/** Handles resize events from the ResizeObserver. */
private _handleResize() {
// In the case where the label grows in size, the following sequence of events occurs:
// 1. The label grows by 1px triggering the ResizeObserver
// 2. The notch is expanded to accommodate the entire label
// 3. The label expands to its full width, triggering the ResizeObserver again
//
// This is expected, but If we allow this to all happen within the same macro task it causes an
// error: `ResizeObserver loop limit exceeded`. Therefore we push the notch resize out until
// the next macro task.
setTimeout(() => this._parent._handleLabelResized());
}

/** Subscribes to resize events. */
private _subscribeToResize() {
this._resizeSubscription.unsubscribe();
this._ngZone.runOutsideAngular(() => {
this._resizeSubscription = this._resizeObserver
.observe(this._elementRef.nativeElement, {box: 'border-box'})
.subscribe(() => this._handleResize());
});
}
}

/**
* Estimates the scroll width of an element.
* via https://github.com/material-components/material-components-web/blob/c0a11ef0d000a098fd0c372be8f12d6a99302855/packages/mdc-dom/ponyfill.ts
*/
function estimateScrollWidth(element: HTMLElement): number {
// Check the offsetParent. If the element inherits display: none from any
// parent, the offsetParent property will be null (see
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent).
// This check ensures we only clone the node when necessary.
const htmlEl = element as HTMLElement;
if (htmlEl.offsetParent !== null) {
return htmlEl.scrollWidth;
}

const clone = htmlEl.cloneNode(true) as HTMLElement;
clone.style.setProperty('position', 'absolute');
clone.style.setProperty('transform', 'translate(-9999px, -9999px)');
document.documentElement.appendChild(clone);
const scrollWidth = clone.scrollWidth;
clone.remove();
return scrollWidth;
}
31 changes: 31 additions & 0 deletions libs/triangle/form-field/src/directives/hint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {Directive, Input} from '@angular/core';

let nextUniqueId = 0;

/** Hint text to be shown underneath the form field control. */
@Directive({
selector: 'tri-hint',
host: {
'class': 'tri-form-field-hint tri-form-field-bottom-align',
'[class.tri-form-field-hint-end]': 'align === "end"',
'[id]': 'id',
// Remove align attribute to prevent it from interfering with layout.
'[attr.align]': 'null',
},
standalone: true,
})
export class TriHint {
/** Whether to align the hint label at the start or end of the line. */
@Input() align: 'start' | 'end' = 'start';

/** Unique ID for the hint. Used for the aria-describedby on the form field control. */
@Input() id: string = `tri-hint-${nextUniqueId++}`;
}
16 changes: 16 additions & 0 deletions libs/triangle/form-field/src/directives/label.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {Directive} from '@angular/core';

/** The floating label for a `mat-form-field`. */
@Directive({
selector: 'tri-label',
standalone: true,
})
export class TriLabel {}
64 changes: 64 additions & 0 deletions libs/triangle/form-field/src/directives/line-ripple.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {Directive, ElementRef, NgZone, OnDestroy} from '@angular/core';

/** Class added when the line ripple is active. */
const ACTIVATE_CLASS = 'tri-line-ripple--active';

/** Class added when the line ripple is being deactivated. */
const DEACTIVATING_CLASS = 'tri-line-ripple--deactivating';

/**
* Internal directive that creates an instance of the MDC line-ripple component. Using a
* directive allows us to conditionally render a line-ripple in the template without having
* to manually create and destroy the `MDCLineRipple` component whenever the condition changes.
*
* The directive sets up the styles for the line-ripple and provides an API for activating
* and deactivating the line-ripple.
*/
@Directive({
selector: 'div[triFormFieldLineRipple]',
host: {
'class': 'tri-line-ripple',
},
standalone: true,
})
export class TriFormFieldLineRipple implements OnDestroy {
constructor(
private _elementRef: ElementRef<HTMLElement>,
ngZone: NgZone,
) {
ngZone.runOutsideAngular(() => {
_elementRef.nativeElement.addEventListener('transitionend', this._handleTransitionEnd);
});
}

activate() {
const classList = this._elementRef.nativeElement.classList;
classList.remove(DEACTIVATING_CLASS);
classList.add(ACTIVATE_CLASS);
}

deactivate() {
this._elementRef.nativeElement.classList.add(DEACTIVATING_CLASS);
}

private _handleTransitionEnd = (event: TransitionEvent) => {
const classList = this._elementRef.nativeElement.classList;
const isDeactivating = classList.contains(DEACTIVATING_CLASS);

if (event.propertyName === 'opacity' && isDeactivating) {
classList.remove(ACTIVATE_CLASS, DEACTIVATING_CLASS);
}
};

ngOnDestroy() {
this._elementRef.nativeElement.removeEventListener('transitionend', this._handleTransitionEnd);
}
}
5 changes: 5 additions & 0 deletions libs/triangle/form-field/src/directives/notched-outline.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<div class="tri-notched-outline__leading"></div>
<div class="tri-notched-outline__notch" #notch>
<ng-content></ng-content>
</div>
<div class="tri-notched-outline__trailing"></div>
Loading

0 comments on commit 5b44928

Please sign in to comment.