diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html index a65e6a2d6..66dc7300c 100644 --- a/.storybook/preview-head.html +++ b/.storybook/preview-head.html @@ -727,7 +727,4 @@ border-color: #666; opacity: 1; } - .oui-mdc-tab-body-wrapper { - min-height: 100px; - } diff --git a/documentation.json b/documentation.json index f019d8965..1db52d05d 100644 --- a/documentation.json +++ b/documentation.json @@ -3436,7 +3436,7 @@ { "name": "ErrorStateMatcher", "id": "injectable-ErrorStateMatcher-2a790495b111018f3cbba0de2309c414412a62be2f0fb8cffdc004f5dc3b3f11c8e19fc024a0ae5e7487dfe35577ec4246ce26c8c46297b9d2651b92f301d6e5", - "file": "ui/src/components/core/common-behaviors/error-options.ts", + "file": "ui/src/components/core/error/error-options.ts", "properties": [], "methods": [ { @@ -3493,7 +3493,7 @@ { "name": "ErrorStateMatcher", "id": "injectable-ErrorStateMatcher-2a790495b111018f3cbba0de2309c414412a62be2f0fb8cffdc004f5dc3b3f11c8e19fc024a0ae5e7487dfe35577ec4246ce26c8c46297b9d2651b92f301d6e5-1", - "file": "ui/src/components/core/error/error-options.ts", + "file": "ui/src/components/core/common-behaviors/error-options.ts", "properties": [], "methods": [ { @@ -7188,7 +7188,7 @@ { "name": "ShowOnDirtyErrorStateMatcher", "id": "injectable-ShowOnDirtyErrorStateMatcher-2a790495b111018f3cbba0de2309c414412a62be2f0fb8cffdc004f5dc3b3f11c8e19fc024a0ae5e7487dfe35577ec4246ce26c8c46297b9d2651b92f301d6e5", - "file": "ui/src/components/core/common-behaviors/error-options.ts", + "file": "ui/src/components/core/error/error-options.ts", "properties": [], "methods": [ { @@ -7245,7 +7245,7 @@ { "name": "ShowOnDirtyErrorStateMatcher", "id": "injectable-ShowOnDirtyErrorStateMatcher-2a790495b111018f3cbba0de2309c414412a62be2f0fb8cffdc004f5dc3b3f11c8e19fc024a0ae5e7487dfe35577ec4246ce26c8c46297b9d2651b92f301d6e5-1", - "file": "ui/src/components/core/error/error-options.ts", + "file": "ui/src/components/core/common-behaviors/error-options.ts", "properties": [], "methods": [ { @@ -19219,9 +19219,9 @@ }, { "name": "OuiSelectTrigger", - "id": "directive-OuiSelectTrigger-d85db46eab36d315c1fdcd891bdac570b57a0ccdc711e79255f204f23073d94dc6b0707254460df0585b83b8338fb5a5a8a9bcb9d49bc6bf529c53c663cbdac2", + "id": "component-OuiSelect-d85db46eab36d315c1fdcd891bdac570b57a0ccdc711e79255f204f23073d94dc6b0707254460df0585b83b8338fb5a5a8a9bcb9d49bc6bf529c53c663cbdac2", "file": "ui/src/components/select/select.component.ts", - "type": "directive", + "type": "component", "description": "

Allows the user to customize the trigger that is displayed when the select has a value.

\n", "rawdescription": "\n\nAllows the user to customize the trigger that is displayed when the select has a value.\n", "sourceCode": "import { ActiveDescendantKeyManager, FocusMonitor } from '@angular/cdk/a11y';\nimport { Directionality } from '@angular/cdk/bidi';\nimport { coerceBooleanProperty } from '@angular/cdk/coercion';\nimport { SelectionModel } from '@angular/cdk/collections';\nimport {\n A,\n DOWN_ARROW,\n END,\n ENTER,\n HOME,\n LEFT_ARROW,\n RIGHT_ARROW,\n SPACE,\n UP_ARROW,\n hasModifierKey,\n TAB,\n} from '@angular/cdk/keycodes';\nimport { CdkConnectedOverlay } from '@angular/cdk/overlay';\nimport {\n AfterContentInit,\n Attribute,\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n ContentChild,\n ContentChildren,\n Directive,\n DoCheck,\n ElementRef,\n EventEmitter,\n Input,\n isDevMode,\n NgZone,\n OnChanges,\n OnDestroy,\n OnInit,\n Optional,\n Output,\n QueryList,\n Self,\n SimpleChanges,\n ViewChild,\n ViewEncapsulation,\n Inject,\n} from '@angular/core';\nimport {\n ControlValueAccessor,\n FormGroupDirective,\n NgControl,\n NgForm,\n} from '@angular/forms';\nimport {\n _countGroupLabelsBeforeOption,\n _getOptionScrollPosition,\n CanDisable,\n CanDisableCtor,\n CanUpdateErrorState,\n CanUpdateErrorStateCtor,\n HasTabIndex,\n HasTabIndexCtor,\n OuiOptionSelectionChange,\n mixinErrorState,\n mixinTabIndex,\n mixinDisabled,\n} from '../core';\nimport { OuiFormField, OuiFormFieldControl } from '../form-field/public-api';\nimport { DOCUMENT } from '@angular/common';\nimport { OUI_OPTION_PARENT_COMPONENT, OuiOption } from '../core/option/option';\nimport { OuiOptgroup } from '../core/option/optgroup';\nimport { ErrorStateMatcher } from '../core/error/error-options';\nimport { defer, merge, Observable, Subject } from 'rxjs';\nimport {\n distinctUntilChanged,\n filter,\n map,\n startWith,\n switchMap,\n take,\n takeUntil,\n} from 'rxjs/operators';\nimport {\n getOuiSelectDynamicMultipleError,\n getOuiSelectNonArrayValueError,\n getOuiSelectNonFunctionValueError,\n} from './select-errors';\nimport { OuiIconRegistry } from '../icon/icon-registery';\nimport { DomSanitizer } from '@angular/platform-browser';\nimport { ICONS } from '../core/shared/icons';\n\nlet nextUniqueId = 0;\n\n/**\n * The following style constants are necessary to save here in order\n * to properly calculate the alignment of the selected option over\n * the trigger element.\n */\n\n/** The height of each select option. */\nexport const SELECT_OPTION_HEIGHT = 40;\n\n/** The panel's padding on the x-axis */\nexport const SELECT_PANEL_PADDING_X = 16;\n\n/** The panel's x axis padding if it is indented (e.g. there is an option group). */\nexport const SELECT_PANEL_INDENT_PADDING_X = SELECT_PANEL_PADDING_X * 2;\n\n/** The height of the select items in `em` units. */\nexport const SELECT_ITEM_HEIGHT_EM = 3;\n\n/** The total height of the select panel. */\nexport const SELECT_PANEL_HEIGHT = 200;\n\n// TODO(josephperrott): Revert to a constant after 2018 spec updates are fully merged.\n/**\n * Distance between the panel edge and the option text in\n * multi-selection mode.\n *\n * Calculated as:\n * (SELECT_PANEL_PADDING_X * 1.5) + 20 = 44\n * The padding is multiplied by 1.5 because the checkbox's margin is half the padding.\n * The checkbox width is 16px.\n */\nexport let SELECT_MULTIPLE_PANEL_PADDING_X = 0;\n\n/**\n * The select panel will only \"fit\" inside the viewport if it is positioned at\n * this value or more away from the viewport boundary.\n */\nexport const SELECT_PANEL_VIEWPORT_PADDING = 8;\n\n/** Change event object that is emitted when the select value has changed. */\nexport class OuiSelectChange {\n constructor(\n /** Reference to the select that emitted the change event. */\n public source: OuiSelect,\n /** Current value of the select that emitted the event. */\n public value: any\n ) {}\n}\n\n// Boilerplate for applying mixins to OuiSelect.\n/** @docs-private */\nexport class OuiSelectBase {\n constructor(\n public _elementRef: ElementRef,\n public _defaultErrorStateMatcher: ErrorStateMatcher,\n public _parentForm: NgForm,\n public _parentFormGroup: FormGroupDirective,\n public ngControl: NgControl\n ) {}\n}\n\nexport const _OuiSelectMixinBase: CanDisableCtor &\n HasTabIndexCtor &\n CanUpdateErrorStateCtor &\n typeof OuiSelectBase = mixinTabIndex(\n mixinDisabled(mixinErrorState(OuiSelectBase))\n);\n\n/**\n * Allows the user to customize the trigger that is displayed when the select has a value.\n */\n@Directive({\n // eslint-disable-next-line @angular-eslint/directive-selector\n selector: 'oui-select-trigger',\n})\nexport class OuiSelectTrigger {}\n\n@Component({\n selector: 'oui-select',\n exportAs: 'ouiSelect',\n templateUrl: 'select.html',\n styleUrls: ['select.scss'],\n // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property\n inputs: ['disabled', 'tabIndex'],\n encapsulation: ViewEncapsulation.None,\n changeDetection: ChangeDetectionStrategy.OnPush,\n // eslint-disable-next-line @angular-eslint/no-host-metadata-property\n host: {\n role: 'listbox',\n '[attr.id]': 'id',\n '[attr.tabindex]': 'tabIndex',\n '[attr.aria-label]': '_getAriaLabel()',\n '[attr.aria-labelledby]': '_getAriaLabelledby()',\n '[attr.aria-required]': 'required.toString()',\n '[attr.aria-disabled]': 'disabled.toString()',\n '[attr.aria-invalid]': 'errorState',\n '[attr.aria-owns]': 'panelOpen ? _optionIds : null',\n '[attr.aria-multiselectable]': 'multiple',\n '[attr.aria-describedby]': '_ariaDescribedby || null',\n '[attr.aria-activedescendant]': '_getAriaActiveDescendant()',\n '[class.oui-select-disabled]': 'disabled',\n '[class.oui-select-invalid]': 'errorState',\n '[class.oui-select-required]': 'required',\n '[class.oui-select-empty]': 'empty',\n class: 'oui-select oui-input',\n '(keydown)': '_handleKeydown($event)',\n '(focus)': '_onFocus()',\n '(blur)': '_onBlur()',\n },\n providers: [\n { provide: OuiFormFieldControl, useExisting: OuiSelect },\n { provide: OUI_OPTION_PARENT_COMPONENT, useExisting: OuiSelect },\n ],\n})\nexport class OuiSelect\n extends _OuiSelectMixinBase\n implements\n AfterContentInit,\n OnChanges,\n OnDestroy,\n OnInit,\n DoCheck,\n ControlValueAccessor,\n CanDisable,\n HasTabIndex,\n OuiFormFieldControl,\n CanUpdateErrorState\n{\n /**Holds selected values after done */\n @Input() savedValues = [];\n /**Done button disabled until dropdown is dirty */\n disableDoneButton = true;\n /** Whether or not the overlay panel is open. */\n private _panelOpen = false;\n\n /** Whether filling out the select is required in the form. */\n private _required = false;\n\n /** Whether filling out the select is required in the form. */\n private _actionItems = false;\n\n /** The scroll position of the overlay panel, calculated to center the selected option. */\n private _scrollTop = 0;\n\n /** The placeholder displayed in the trigger of the select. */\n private _placeholder: string;\n\n /** Whether the component is in multiple selection mode. */\n private _multiple = false;\n\n /** Search input field **/\n isSearchFieldPresent: boolean;\n\n /** Unique id for this input. */\n private _uid = `oui-select-${nextUniqueId++}`;\n\n /** The last measured value for the trigger's client bounding rect. */\n _triggerRect: ClientRect;\n\n /** The aria-describedby attribute on the select for improved a11y. */\n _ariaDescribedby: string;\n\n /** The cached font-size of the trigger element. */\n _triggerFontSize = 0;\n\n /** Deals with the selection logic. */\n _selectionModel: SelectionModel;\n\n /** Manages keyboard events for options in the panel. */\n _keyManager: ActiveDescendantKeyManager;\n\n /** The IDs of child options to be passed to the aria-owns attribute. */\n _optionIds = '';\n\n /** The value of the select panel's transform-origin property. */\n _transformOrigin = 'top';\n\n /** If there is search input field a class is added dynamically to the perfect scrollbar **/\n ouiSelectInputOuterClassName: string;\n\n /** Adding top class to overlay panel */\n cdkConnectionOverlayPanel = '';\n\n /**\n * The y-offset of the overlay panel in relation to the trigger's top start corner.\n * This must be adjusted to align the selected option text over the trigger text.\n * when the panel opens. Will change based on the y-position of the selected option.\n */\n _offsetY = 0;\n\n /**\n * This position config ensures that the top \"start\" corner of the overlay\n * is aligned with with the top \"start\" of the origin by default (overlapping\n * the trigger completely). If the panel cannot fit below the trigger, it\n * will fall back to a position above the trigger.\n */\n _positions = [\n {\n originX: 'start',\n originY: 'top',\n overlayX: 'start',\n overlayY: 'top',\n },\n {\n originX: 'start',\n originY: 'bottom',\n overlayX: 'start',\n overlayY: 'bottom',\n },\n ];\n /** Emits whenever the component is destroyed. */\n private readonly _destroy = new Subject();\n\n /** Whether the component is disabling centering of the active option over the trigger. */\n private _disableOptionCentering = false;\n\n private _focused = false;\n\n /** A name for this control that can be used by `oui-form-field`. */\n controlType = 'oui-select';\n\n /** Trigger that opens the select. */\n @ViewChild('trigger') trigger: ElementRef;\n\n /** Trigger that opens the select. */\n @ViewChild('ddCancelButton', { read: ElementRef }) ddCancelButton: ElementRef;\n\n /** Trigger that opens the select. */\n @ViewChild('ddDoneButton', { read: ElementRef }) ddDoneButton: ElementRef;\n\n /** Panel containing the select options. */\n @ViewChild('panel', { read: ElementRef }) panel: ElementRef;\n\n private _value: any;\n\n /**\n * Function used to sort the values in a select in multiple mode.\n * Follows the same logic as `Array.prototype.sort`.\n */\n @Input() sortComparator: (\n a: OuiOption,\n b: OuiOption,\n options: OuiOption[]\n ) => number;\n\n /** Aria label of the select. If not specified, the placeholder will be used as label. */\n @Input('aria-label') ariaLabel = '';\n\n /** Input that can be used to specify the `aria-labelledby` attribute. */\n @Input('aria-labelledby') ariaLabelledby: string;\n private _large = false;\n _monitorSubscription: any;\n\n /** Whether the oui-select is of large size. */\n @Input()\n get large(): boolean {\n return this._large;\n }\n set large(value) {\n this._large = coerceBooleanProperty(value);\n this._changeDetectorRef.markForCheck();\n }\n\n private _id: string;\n\n /** Event emitted when the select panel has been toggled. */\n @Output()\n readonly openedChange: EventEmitter = new EventEmitter();\n\n /** Combined stream of all of the child options' change events. */\n readonly optionSelectionChanges: Observable = defer(\n (): Observable => {\n if (this.options) {\n return merge(...this.options.map((option) => option.onSelectionChange));\n }\n\n return this._ngZone.onStable.asObservable().pipe(\n take(1),\n switchMap(() => this.optionSelectionChanges)\n );\n }\n );\n\n /**\n * Event that emits whenever the raw value of the select changes. This is here primarily\n * to facilitate the two-way binding for the `value` input.\n *\n * @docs-private\n */\n @Output() readonly valueChange: EventEmitter = new EventEmitter();\n\n /** Object used to control when error messages are shown. */\n @Input() errorStateMatcher: ErrorStateMatcher;\n\n /** All of the defined select options. */\n @ContentChildren(OuiOption, { descendants: true })\n options: QueryList;\n\n /** Event emitted when the select has been opened. */\n // eslint-disable-next-line @angular-eslint/no-output-rename\n @Output('opened')\n readonly _openedStream: Observable = this.openedChange.pipe(\n filter((o) => o),\n map(() => {})\n );\n\n /** Event emitted when the select has been closed. */\n // eslint-disable-next-line @angular-eslint/no-output-rename\n @Output('closed')\n readonly _closedStream: Observable = this.openedChange.pipe(\n filter((o) => !o),\n map(() => {\n this.isSearchFieldPresent = false;\n })\n );\n\n /** Event emitted when the selected value has been changed by the user. */\n @Output()\n readonly selectionChange: EventEmitter =\n new EventEmitter();\n\n /** Event emitted when the selected value has been changed and saved by the user. */\n @Output()\n readonly saveSelectionChange: EventEmitter =\n new EventEmitter();\n\n /** All of the defined groups of options. */\n @ContentChildren(OuiOptgroup) optionGroups: QueryList;\n\n /** User-supplied override of the trigger element. */\n @ContentChild(OuiSelectTrigger)\n customTrigger: OuiSelectTrigger;\n\n /** Classes to be passed to the select panel. Supports the same syntax as `ngClass`. */\n @Input() panelClass: string | string[] | Set | { [key: string]: any };\n\n /** Overlay pane containing the options. */\n @ViewChild(CdkConnectedOverlay)\n overlayDir: CdkConnectedOverlay;\n\n /** Emits when the panel element is finished transforming in. */\n _panelDoneAnimatingStream = new Subject();\n\n /** Comparison function to specify which option is displayed. Defaults to object equality. */\n private _compareWith = (o1: any, o2: any) => o1 === o2;\n\n /** Whether the select is focused. */\n get focused(): boolean {\n return this._focused || this._panelOpen;\n }\n /**\n * @deprecated Setter to be removed as this property is intended to be readonly.\n */\n set focused(value: boolean) {\n this._focused = value;\n }\n /** `View -> model callback called when value changes` */\n _onChange: (value: any) => void = () => {};\n\n /** `View -> model callback called when select has been touched` */\n _onTouched = () => {};\n\n /** Placeholder to be shown if no value has been selected. */\n @Input()\n get placeholder(): string {\n return this._placeholder;\n }\n set placeholder(value: string) {\n this._placeholder = value;\n this.stateChanges.next();\n }\n\n /** Whether the component is required. */\n @Input()\n get required(): boolean {\n return this._required;\n }\n set required(value: boolean) {\n this._required = coerceBooleanProperty(value);\n this.stateChanges.next();\n }\n\n /** Whether the user should be allowed to select multiple options. */\n @Input()\n get multiple(): boolean {\n return this._multiple;\n }\n set multiple(value: boolean) {\n if (this._selectionModel) {\n throw getOuiSelectDynamicMultipleError();\n }\n\n this._multiple = coerceBooleanProperty(value);\n }\n\n /** Whether the action items are required and use saveSelectionChange instead of selectionChange. */\n @Input()\n get actionItems(): boolean {\n return this._actionItems;\n }\n set actionItems(value: boolean) {\n if (this._multiple) {\n this._actionItems = coerceBooleanProperty(value);\n this.stateChanges.next();\n }\n }\n\n /** Whether to center the active option over the trigger. */\n @Input()\n get disableOptionCentering(): boolean {\n return this._disableOptionCentering;\n }\n set disableOptionCentering(value: boolean) {\n this._disableOptionCentering = coerceBooleanProperty(value);\n }\n\n /**\n * Function to compare the option values with the selected values. The first argument\n * is a value from an option. The second is a value from the selection. A boolean\n * should be returned.\n */\n @Input()\n get compareWith() {\n return this._compareWith;\n }\n set compareWith(fn: (o1: any, o2: any) => boolean) {\n if (typeof fn !== 'function') {\n throw getOuiSelectNonFunctionValueError();\n }\n this._compareWith = fn;\n if (this._selectionModel) {\n // A different comparator means the selection could change.\n this._initializeSelection();\n }\n }\n\n /** Value of the select control. */\n @Input()\n get value(): any {\n return this._value;\n }\n set value(newValue: any) {\n if (newValue !== this._value) {\n this.writeValue(newValue);\n this._value = newValue;\n }\n }\n\n /** Unique id of the element. */\n @Input()\n get id(): string {\n return this._id;\n }\n set id(value: string) {\n this._id = value || this._uid;\n this.stateChanges.next();\n }\n\n constructor(\n private _changeDetectorRef: ChangeDetectorRef,\n private _ngZone: NgZone,\n _defaultErrorStateMatcher: ErrorStateMatcher,\n elementRef: ElementRef,\n private _focusMonitor: FocusMonitor,\n @Optional() private _dir: Directionality,\n @Optional() _parentForm: NgForm,\n @Optional() _parentFormGroup: FormGroupDirective,\n @Optional() private _parentFormField: OuiFormField,\n @Self() @Optional() public ngControl: NgControl,\n @Attribute('tabindex') tabIndex: string,\n @Optional() @Inject(DOCUMENT) private _document: any,\n public _elementRef: ElementRef,\n public _ouiIconRegistry: OuiIconRegistry,\n private _domSanitizer: DomSanitizer\n ) {\n super(\n elementRef,\n _defaultErrorStateMatcher,\n _parentForm,\n _parentFormGroup,\n ngControl\n );\n this._monitorSubscription = this._focusMonitor\n .monitor(this._elementRef, true)\n .subscribe(() => this._ngZone.run(() => {}));\n this._ouiIconRegistry.addSvgIconLiteral(\n `select-arrow-icon`,\n this._domSanitizer.bypassSecurityTrustHtml(ICONS.SELECT_ARROW_ICON)\n );\n\n if (this.ngControl) {\n // Note: we provide the value accessor through here, instead of\n // the `providers` to avoid running into a circular import.\n this.ngControl.valueAccessor = this;\n }\n\n this.tabIndex = parseInt(tabIndex, 10) || 0;\n\n // Force setter to be called in case id was not specified.\n this.id = this.id;\n }\n\n ngOnInit() {\n this._selectionModel = new SelectionModel(this.multiple);\n this.stateChanges.next();\n\n // We need `distinctUntilChanged` here, because some browsers will\n // fire the animation end event twice for the same animation. See:\n // https://github.com/angular/angular/issues/24084\n this._panelDoneAnimatingStream\n .pipe(distinctUntilChanged(), takeUntil(this._destroy))\n .subscribe(() => {\n if (this.panelOpen) {\n this._scrollTop = 0;\n this.openedChange.emit(true);\n } else {\n this.openedChange.emit(false);\n this.overlayDir.offsetX = 0;\n this._changeDetectorRef.markForCheck();\n }\n });\n }\n\n ngAfterContentInit() {\n this._initKeyManager();\n\n this._selectionModel.changed\n .pipe(takeUntil(this._destroy))\n .subscribe((event) => {\n event.added.forEach((option) => option.select());\n event.removed.forEach((option) => option.deselect());\n });\n\n this.options.changes\n .pipe(startWith(null), takeUntil(this._destroy))\n .subscribe(() => {\n this._resetOptions();\n this._initializeSelection();\n });\n }\n\n ngDoCheck() {\n if (this.ngControl) {\n this.updateErrorState();\n }\n }\n\n ngOnChanges(changes: SimpleChanges) {\n // Updating the disabled state is handled by `mixinDisabled`, but we need to additionally let\n // the parent form field know to run change detection when the disabled state changes.\n if (changes.disabled) {\n this.stateChanges.next();\n }\n }\n\n ngOnDestroy() {\n this._monitorSubscription.unsubscribe();\n this._focusMonitor.stopMonitoring(this._elementRef);\n this._destroy.next();\n this._destroy.complete();\n this.stateChanges.complete();\n }\n\n /** Toggles the overlay panel open or closed. */\n toggle(): void {\n this.panelOpen ? this.close() : this.open();\n }\n\n /** Opens the overlay panel. */\n open(): void {\n if (\n this.disabled ||\n !this.options ||\n !this.options.length ||\n this._panelOpen\n ) {\n return;\n }\n\n this._triggerRect = this.trigger.nativeElement.getBoundingClientRect();\n\n this._panelOpen = true;\n this._keyManager.withHorizontalOrientation(null);\n\n this._highlightCorrectOption();\n this._changeDetectorRef.markForCheck();\n this.openedChange.emit(true);\n this._elementRef.nativeElement.classList.add(\n 'oui-select-list-options-opened'\n );\n }\n\n /** Closes the overlay panel and focuses the host element. */\n close(): void {\n if (this._panelOpen) {\n this._panelOpen = false;\n this._keyManager.withHorizontalOrientation(this._isRtl() ? 'rtl' : 'ltr');\n this._changeDetectorRef.markForCheck();\n this._onTouched();\n this.openedChange.emit(false);\n this._elementRef.nativeElement.classList.remove(\n 'oui-select-list-options-opened'\n );\n setTimeout((_) => this._document.activeElement.blur());\n }\n }\n\n /**\n * Sets the select's value. Part of the ControlValueAccessor interface\n * required to integrate with Angular's core forms API.\n *\n * @param value New value to be written to the model.\n */\n writeValue(value: any): void {\n if (this.options) {\n this._setSelectionByValue(value);\n }\n }\n\n /**\n * Saves a callback function to be invoked when the select's value\n * changes from user input. Part of the ControlValueAccessor interface\n * required to integrate with Angular's core forms API.\n *\n * @param fn Callback to be triggered when the value changes.\n */\n registerOnChange(fn: (value: any) => void): void {\n this._onChange = fn;\n }\n\n /**\n * Saves a callback function to be invoked when the select is blurred\n * by the user. Part of the ControlValueAccessor interface required\n * to integrate with Angular's core forms API.\n *\n * @param fn Callback to be triggered when the component has been touched.\n */\n registerOnTouched(fn: () => {}): void {\n this._onTouched = fn;\n }\n\n /**\n * Disables the select. Part of the ControlValueAccessor interface required\n * to integrate with Angular's core forms API.\n *\n * @param isDisabled Sets whether the component is disabled.\n */\n setDisabledState(isDisabled: boolean): void {\n this.disabled = isDisabled;\n this._changeDetectorRef.markForCheck();\n this.stateChanges.next();\n }\n\n /** Whether or not the overlay panel is open. */\n get panelOpen(): boolean {\n return this._panelOpen;\n }\n\n /** The currently selected option. */\n get selected(): OuiOption | OuiOption[] {\n return this.multiple\n ? this._selectionModel.selected\n : this._selectionModel.selected[0];\n }\n\n /** The value displayed in the trigger. */\n get triggerValue(): string {\n if (this.empty) {\n return '';\n }\n if (this._multiple) {\n const selectedOptions = this._selectionModel.selected.map(\n (option) => option.viewValueForSelect\n );\n\n if (this._isRtl()) {\n selectedOptions.reverse();\n }\n return selectedOptions.join(', ');\n }\n return this._selectionModel.selected[0].viewValueForSelect;\n }\n\n /** Whether the element is in RTL mode. */\n _isRtl(): boolean {\n return this._dir ? this._dir.value === 'rtl' : false;\n }\n\n /** Handles all keydown events on the select. */\n _handleKeydown(event: KeyboardEvent): void {\n if (!this.disabled) {\n this.panelOpen\n ? this._handleOpenKeydown(event)\n : this._handleClosedKeydown(event);\n }\n }\n\n /** Handles keyboard events while the select is closed. */\n private _handleClosedKeydown(event: KeyboardEvent): void {\n const keyCode = event.keyCode;\n const isArrowKey =\n keyCode === DOWN_ARROW ||\n keyCode === UP_ARROW ||\n keyCode === LEFT_ARROW ||\n keyCode === RIGHT_ARROW;\n const isOpenKey = keyCode === ENTER || keyCode === SPACE;\n const manager = this._keyManager;\n\n // Open the select on ALT + arrow key to match the native \n event.preventDefault();\n this.close();\n } else if (\n (keyCode === ENTER || keyCode === SPACE) &&\n manager.activeItem &&\n !hasModifierKey(event)\n ) {\n event.preventDefault();\n manager.activeItem._selectViaInteraction();\n } else if (this._multiple && keyCode === A && event.ctrlKey) {\n event.preventDefault();\n this.handleCtrlKey();\n } else if (normalNavigationCheck) {\n // Check for non multiple select dropdown that the key pressed is not Tab, Space, Enter\n if (!this.isSearchFieldPresent) this.focus();\n this.handleScrolling(manager, event, isArrowKey, keyCode);\n }\n }\n\n /**\n * Handle ctrl key\n */\n private handleCtrlKey() {\n const hasDeselectedOptions = this.options.some(\n (opt) => !opt.disabled && !opt.selected\n );\n\n this.options.forEach((option) => {\n if (!option.disabled) {\n hasDeselectedOptions ? option.select() : option.deselect();\n }\n });\n }\n\n /**\n * @param manager\n * @param event\n * @param isArrowKey\n * @param keyCode\n */\n private handleScrolling(\n manager: ActiveDescendantKeyManager,\n event: KeyboardEvent,\n isArrowKey: boolean,\n keyCode: number\n ) {\n const previouslyFocusedIndex = manager.activeItemIndex;\n\n manager.onKeydown(event);\n\n if (\n this._multiple &&\n isArrowKey &&\n event.shiftKey &&\n manager.activeItem &&\n manager.activeItemIndex !== previouslyFocusedIndex\n ) {\n manager.activeItem._selectViaInteraction();\n }\n if (isArrowKey && manager.activeItemIndex !== previouslyFocusedIndex) {\n this._scrollToOption();\n } else {\n // First or last\n if (keyCode === DOWN_ARROW) {\n manager.setFirstItemActive();\n this._setScrollTop(0);\n }\n if (keyCode === UP_ARROW) {\n manager.setLastItemActive();\n this._scrollToOption();\n }\n }\n }\n\n _onFocus() {\n if (!this.disabled) {\n this._focused = true;\n this.stateChanges.next();\n }\n }\n\n /**\n * Calls the touched callback only if the panel is closed. Otherwise, the trigger will\n * \"blur\" to the panel when it opens, causing a false positive.\n */\n _onBlur() {\n this._focused = false;\n // this.isSearchFieldPresent = false;\n\n if (!this.disabled && !this.panelOpen) {\n this._onTouched();\n this._changeDetectorRef.markForCheck();\n this.stateChanges.next();\n }\n }\n\n /**\n * Callback that is invoked when the overlay panel has been attached.\n */\n _onAttached(): void {\n this.overlayDir.positionChange.pipe(take(1)).subscribe(() => {\n this._setPseudoCheckboxPaddingSize();\n this._changeDetectorRef.detectChanges();\n this.panel.nativeElement.scrollTop = this._scrollTop;\n });\n }\n\n /** Returns the theme to be used on the panel. */\n _getPanelTheme(): string {\n return this._parentFormField ? `oui-${this._parentFormField.color}` : '';\n }\n\n // TODO(josephperrott): Remove after 2018 spec updates are fully merged.\n /** Sets the pseudo checkbox padding size based on the width of the pseudo checkbox. */\n private _setPseudoCheckboxPaddingSize() {\n if (!SELECT_MULTIPLE_PANEL_PADDING_X && this.multiple) {\n const pseudoCheckbox = this.panel.nativeElement.querySelector(\n '.oui-pseudo-checkbox'\n );\n if (pseudoCheckbox) {\n SELECT_MULTIPLE_PANEL_PADDING_X =\n SELECT_PANEL_PADDING_X * 1.5 + pseudoCheckbox.offsetWidth;\n }\n }\n }\n\n /** Whether the select has a value. */\n get empty(): boolean {\n return !this._selectionModel || this._selectionModel.isEmpty();\n }\n\n private _initializeSelection(): void {\n // Defer setting the value in order to avoid the \"Expression\n // has changed after it was checked\" errors from Angular.\n Promise.resolve().then(() => {\n this._setSelectionByValue(\n this.ngControl ? this.ngControl.value : this._value\n );\n this.savedValues = this.ngControl ? this.ngControl.value : this._value;\n if (this.multiple) {\n this._highlightFirstFilteredOption();\n }\n });\n }\n\n /**\n * Sets the selected option based on a value. If no option can be\n * found with the designated value, the select trigger is cleared.\n */\n private _setSelectionByValue(value: any | any[]): void {\n if (this.multiple && value) {\n if (!Array.isArray(value)) {\n throw getOuiSelectNonArrayValueError();\n }\n\n this._selectionModel.clear();\n value.forEach((currentValue: any) => this._selectValue(currentValue));\n this._sortValues();\n } else {\n this._selectionModel.clear();\n const correspondingOption = this._selectValue(value);\n // Shift focus to the active item. Note that we shouldn't do this in multiple\n // mode, because we don't know what option the user interacted with last.\n if (correspondingOption) {\n this._keyManager.setActiveItem(correspondingOption);\n }\n }\n this._changeDetectorRef.markForCheck();\n }\n\n /**\n * Finds and selects and option based on its value.\n *\n * @returns Option that has the corresponding value.\n */\n private _selectValue(value: any): OuiOption | undefined {\n const correspondingOption = this.options.find((option: OuiOption) => {\n try {\n // Treat null as a special reset value.\n return option.value != null && this._compareWith(option.value, value);\n } catch (error) {\n if (isDevMode()) {\n // Notify developers of errors in their comparator.\n console.warn(error);\n }\n return false;\n }\n });\n\n if (correspondingOption) {\n this._selectionModel.select(correspondingOption);\n }\n\n return correspondingOption;\n }\n\n /** Sets up a key manager to listen to keyboard events on the overlay panel. */\n private _initKeyManager() {\n this._keyManager = new ActiveDescendantKeyManager(this.options)\n .withTypeAhead()\n .withVerticalOrientation()\n .withHorizontalOrientation(this._isRtl() ? 'rtl' : 'ltr')\n .withAllowedModifierKeys(['shiftKey']);\n\n this._keyManager.tabOut.pipe(takeUntil(this._destroy)).subscribe(() => {\n // Restore focus to the trigger before closing. Ensures that the focus\n // position won't be lost if the user got focus into the overlay.\n this.focus();\n this.close();\n });\n\n this._keyManager.change.pipe(takeUntil(this._destroy)).subscribe(() => {\n if (this._panelOpen && this.panel) {\n // Panel is opened\n // Need not to scroll\n } else if (\n !this._panelOpen &&\n !this.multiple &&\n this._keyManager.activeItem\n ) {\n this._keyManager.activeItem._selectViaInteraction();\n }\n });\n }\n\n /** Drops current option subscriptions and IDs and resets from scratch. */\n private _resetOptions(): void {\n const changedOrDestroyed = merge(this.options.changes, this._destroy);\n\n this.optionSelectionChanges\n .pipe(takeUntil(changedOrDestroyed))\n .subscribe((event) => {\n this._onSelect(event.source, event.isUserInput);\n\n if (event.isUserInput && !this.multiple && this._panelOpen) {\n this.close();\n this.focus();\n }\n });\n\n // Listen to changes in the internal state of the options and react accordingly.\n // Handles cases like the labels of the selected options changing.\n merge(...this.options.map((option) => option._stateChanges))\n .pipe(takeUntil(changedOrDestroyed))\n .subscribe(() => {\n this._changeDetectorRef.markForCheck();\n this.stateChanges.next();\n });\n\n this._setOptionIds();\n }\n\n /** Invoked when an option is clicked. */\n private _onSelect(option: OuiOption, isUserInput: boolean): void {\n const wasSelected = this._selectionModel.isSelected(option);\n\n if (option.value == null && !this._multiple) {\n option.deselect();\n this._selectionModel.clear();\n this._propagateChanges(option.value);\n } else {\n option.selected\n ? this._selectionModel.select(option)\n : this._selectionModel.deselect(option);\n\n if (isUserInput) {\n this._keyManager.setActiveItem(option);\n }\n\n if (this.multiple) {\n this._sortValues();\n\n if (isUserInput) {\n // In case the user selected the option with their mouse, we\n // want to restore focus back to the trigger, in order to\n // prevent the select keyboard controls from clashing with\n // the ones from `oui-option`.\n this.focus();\n }\n }\n }\n\n if (wasSelected !== this._selectionModel.isSelected(option)) {\n this._propagateChanges();\n }\n this.disableDoneButton = false;\n this.stateChanges.next();\n }\n discardRecentChanges() {\n this.value = this.savedValues;\n this._setSelectionByValue(this.value);\n this.disableDoneButton = true;\n this.close();\n }\n doneRecentChanges() {\n this.savedValues = this.value;\n this.disableDoneButton = true;\n this.saveSelectionChange.emit(new OuiSelectChange(this, this.value));\n this.close();\n }\n /** Sorts the selected values in the selected based on their order in the panel. */\n private _sortValues() {\n if (this.multiple) {\n const options = this.options.toArray();\n\n this._selectionModel.sort((a, b) =>\n this.sortComparator\n ? this.sortComparator(a, b, options)\n : options.indexOf(a) - options.indexOf(b)\n );\n this.stateChanges.next();\n }\n }\n\n /** Emits change event to set the model value. */\n private _propagateChanges(fallbackValue?: any): void {\n let valueToEmit: any = null;\n\n if (this.multiple) {\n valueToEmit = (this.selected as OuiOption[]).map(\n (option) => option.value\n );\n } else {\n valueToEmit = this.selected\n ? (this.selected as OuiOption).value\n : fallbackValue;\n }\n\n this._value = valueToEmit;\n this.valueChange.emit(valueToEmit);\n this._onChange(valueToEmit);\n this.selectionChange.emit(new OuiSelectChange(this, valueToEmit));\n this._changeDetectorRef.markForCheck();\n }\n\n /** Records option IDs to pass to the aria-owns property. */\n private _setOptionIds() {\n this._optionIds = this.options.map((option) => option.id).join(' ');\n }\n\n /**\n * Highlights the selected item. If no option is selected, it will highlight\n * the first item instead.\n */\n private _highlightCorrectOption(): void {\n if (this.multiple) {\n this._highlightFirstFilteredOption();\n } else if (this._keyManager) {\n if (this.empty) {\n this._keyManager.setFirstItemActive();\n } else {\n this._keyManager.setActiveItem(this._selectionModel.selected[0]);\n }\n }\n }\n\n /**\n * Highlights the first of the filtered options if no element is currently highlighted\n */\n private _highlightFirstFilteredOption(): void {\n if (this._keyManager) {\n const activeElement = this._keyManager.activeItem?._getHostElement();\n // activeElement is not part of DOM if there is no parent element\n if (!activeElement || !activeElement.parentElement) {\n // highlight first element if there is no active element or active element is not part of DOM\n this._keyManager.setFirstItemActive();\n }\n }\n }\n\n /** Focuses the select element. */\n focus(): void {\n this._elementRef.nativeElement.focus();\n }\n\n /** Returns the aria-label of the select component. */\n _getAriaLabel(): string | null {\n // If an ariaLabelledby value has been set by the consumer, the select should not overwrite the\n // `aria-labelledby` value by setting the ariaLabel to the placeholder.\n return this.ariaLabelledby ? null : this.ariaLabel || this.placeholder;\n }\n\n /** Returns the aria-labelledby of the select component. */\n _getAriaLabelledby(): string | null {\n if (this.ariaLabelledby) {\n return this.ariaLabelledby;\n }\n\n return null;\n }\n\n /** Determines the `aria-activedescendant` to be set on the host. */\n _getAriaActiveDescendant(): string | null {\n if (this.panelOpen && this._keyManager && this._keyManager.activeItem) {\n return this._keyManager.activeItem.id;\n }\n\n return null;\n }\n\n /**\n * Implemented as part of OuiFormFieldControl.\n *\n * @docs-private\n */\n setDescribedByIds(ids: string[]) {\n this._ariaDescribedby = ids.join(' ');\n }\n\n /**\n * Implemented as part of OuiFormFieldControl.\n *\n * @docs-private\n */\n onContainerClick() {\n this.focus();\n this.open();\n }\n\n /**\n * Implemented as part of OuiFormFieldControl.\n *\n * @docs-private\n */\n get shouldLabelFloat(): boolean {\n return this._panelOpen || !this.empty;\n }\n\n /**\n * Add outer class to perfect scrollbar\n * This is added only when there is a search field\n */\n ouiSelectInputOuter() {\n this.ouiSelectInputOuterClassName = 'oui-select-input-outer';\n }\n\n /**\n * Custom overlay class for cdk overlay container\n */\n openCdk() {\n this.overlayDir.positionChange.pipe(take(1)).subscribe((e) => {\n this.cdkConnectionOverlayPanel = '';\n if (e.connectionPair.originY === 'top') {\n this.cdkConnectionOverlayPanel = 'select-overlay-top';\n }\n this._changeDetectorRef.detectChanges();\n setTimeout((_) => this._scrollToOption());\n });\n\n const cdkOverLayContainer = this._document.querySelector(\n '.cdk-overlay-container'\n );\n const ouiSelectPanel = this._document.querySelector('.oui-select-panel');\n cdkOverLayContainer.classList.add('oui-select-overlay-container');\n const containerWidth = this._elementRef.nativeElement.offsetWidth;\n ouiSelectPanel.style.width = `${containerWidth}px`;\n const searchQueryString = '.oui-select-search-inner';\n if (this._document.querySelector(searchQueryString)) {\n this.scrollCalc(searchQueryString);\n this.isSearchFieldPresent = true;\n }\n }\n scrollCalc(selectQueryString: string) {\n const searchInput = this._document.querySelector(selectQueryString);\n const outter = this._document.querySelector('.oui-select-panel');\n let inner = this._document.querySelector('.oui-option');\n if (inner === null) {\n inner = 0;\n }\n const scrollbarWidth = outter.offsetWidth - inner.offsetWidth;\n if (scrollbarWidth > 5) {\n searchInput.style.width = `${inner.offsetWidth}px`;\n } else {\n searchInput.style.width = `calc(100% + 8px)`;\n }\n }\n\n /**\n * Given that we are not actually focusing active options, we must manually adjust scroll\n * to reveal options below the fold. First, we find the offset of the option from the top\n * of the panel. If that offset is below the fold, the new scrollTop will be the offset -\n * the panel height + the option height, so the active option will be just visible at the\n * bottom of the panel. If that offset is above the top of the visible panel, the new scrollTop\n * will become the offset. If that offset is visible within the panel already, the scrollTop is\n * not adjusted.\n */\n private _scrollToOption(): void {\n const manager = this._keyManager;\n const index = manager.activeItemIndex || 0;\n const labelCount = _countGroupLabelsBeforeOption(\n index,\n this.options,\n this.optionGroups\n );\n const scrollTop = this._getScrollTop();\n const newScrollPosition = _getOptionScrollPosition(\n index + labelCount,\n SELECT_OPTION_HEIGHT,\n scrollTop,\n this.actionItems && this.isSearchFieldPresent && labelCount\n ? SELECT_PANEL_HEIGHT - (50 + labelCount * 19)\n : (this.actionItems && this.isSearchFieldPresent && !labelCount) ||\n (this.actionItems && !this.isSearchFieldPresent && labelCount) ||\n (!this.actionItems && this.isSearchFieldPresent && labelCount)\n ? SELECT_PANEL_HEIGHT - 50\n : SELECT_PANEL_HEIGHT\n );\n this._setScrollTop(newScrollPosition);\n }\n\n /**\n * Sets the panel scrollTop. This allows us to manually scroll to display options\n * above or below the fold, as they are not actually being focused when active.\n */\n _setScrollTop(scrollTop: number): void {\n if (this.panel) {\n this.panel.nativeElement.querySelector('.oui-select-options').scrollTop =\n scrollTop;\n }\n }\n\n /** Returns the panel's scrollTop. */\n _getScrollTop(): number {\n return this.panel ? this.panel.nativeElement.scrollTop : 0;\n }\n}\n", @@ -19598,12 +19598,12 @@ }, { "name": "OuiTabBodyPortal", - "id": "directive-OuiTabBodyPortal-0479dbe7621da9a0746cbaad99fc9fe7e9304251250d9209414bfb2d15351a0c25ab1201b5d3c77ae1abcd3a64ac5e58575191a4df0d1c7cf665c2c5f4cfb720", + "id": "directive-OuiTabBodyPortal-d9aba0e7f5c272163d8a1fcdea5954740c5bac6598de09205831a017a4275c387861d98bd5093d058798cb1713335c4f3d57c032469562999ee21727593e8750", "file": "ui/src/components/tabs/tab-body.ts", "type": "directive", "description": "

The portal host directive for the contents of the tab.

\n", "rawdescription": "\n\nThe portal host directive for the contents of the tab.\n", - "sourceCode": "import {\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n ComponentFactoryResolver,\n Directive,\n ElementRef,\n EventEmitter,\n // forwardRef,\n Inject,\n Input,\n OnDestroy,\n OnInit,\n Optional,\n Output,\n ViewChild,\n ViewContainerRef,\n ViewEncapsulation,\n} from '@angular/core';\nimport { CdkPortalOutlet } from '@angular/cdk/portal';\nimport { Direction, Directionality } from '@angular/cdk/bidi';\nimport { DOCUMENT } from '@angular/common';\nimport { Subject, Subscription } from 'rxjs';\nimport {\n distinctUntilChanged,\n // startWith\n} from 'rxjs/operators';\nimport { AnimationEvent } from '@angular/animations';\nimport { ouiTabsAnimations } from './tabs-animations';\n\n/**\n * The portal host directive for the contents of the tab.\n * @docs-private\n */\n\n@Directive({\n // eslint-disable-next-line @angular-eslint/directive-selector\n selector: `OuiTabBodyHost`,\n})\nexport class OuiTabBodyPortal\n extends CdkPortalOutlet\n implements OnInit, OnDestroy\n{\n /** Subscription to events for when the tab body begins centering. */\n private _centeringSub = Subscription.EMPTY;\n /** Subscription to events for when the tab body finishes leaving from center position. */\n private _leavingSub = Subscription.EMPTY;\n\n constructor(\n componentFactoryResolver: ComponentFactoryResolver,\n viewContainerRef: ViewContainerRef,\n // @Inject(forwardRef(() => OuiTabBody)) private _host: OuiTabBody,\n @Inject(DOCUMENT) _document: any\n ) {\n super(componentFactoryResolver, viewContainerRef, _document);\n }\n\n /** Set initial visibility or set up subscription for changing visibility. */\n override ngOnInit(): void {\n super.ngOnInit();\n }\n\n /** Clean up centering subscription. */\n override ngOnDestroy(): void {\n super.ngOnDestroy();\n this._centeringSub.unsubscribe();\n this._leavingSub.unsubscribe();\n }\n}\n\n/**\n * These position states are used internally as animation states for the tab body. Setting the\n * position state to left, right, or center will transition the tab body from its current\n * position to its respective state. If there is not current position (void, in the case of a new\n * tab body), then there will be no transition animation to its state.\n *\n * In the case of a new tab body that should immediately be centered with an animating transition,\n * then left-origin-center or right-origin-center can be used, which will use left or right as its\n * pseudo-prior state.\n */\nexport type OuiTabBodyPositionState =\n | 'left'\n | 'center'\n | 'right'\n | 'left-origin-center'\n | 'right-origin-center';\n\n/**\n * Wrapper for the contents of a tab.\n * @docs-private\n */\n@Component({\n selector: 'oui-tab-body',\n templateUrl: 'tab-body.html',\n // styleUrls: ['tab-body.css'],\n encapsulation: ViewEncapsulation.None,\n // tslint:disable-next-line:validate-decorators\n changeDetection: ChangeDetectionStrategy.Default,\n animations: [ouiTabsAnimations.translateTab],\n // eslint-disable-next-line @angular-eslint/no-host-metadata-property\n host: {\n class: 'oui-mdc-tab-body',\n },\n})\nexport class OuiTabBody implements OnInit, OnDestroy {\n /** Current position of the tab-body in the tab-group. Zero means that the tab is visible. */\n private _positionIndex: number;\n\n /** Subscription to the directionality change observable. */\n private _dirChangeSubscription = Subscription.EMPTY;\n\n /** Tab body position state. Used by the animation trigger for the current state. */\n _position: OuiTabBodyPositionState;\n\n /** Emits when an animation on the tab is complete. */\n readonly _translateTabComplete = new Subject();\n\n /** Event emitted when the tab begins to animate towards the center as the active tab. */\n @Output() readonly _onCentering: EventEmitter =\n new EventEmitter();\n\n /** Event emitted before the centering of the tab begins. */\n @Output() readonly _beforeCentering: EventEmitter =\n new EventEmitter();\n\n /** Event emitted before the centering of the tab begins. */\n @Output() readonly _afterLeavingCenter: EventEmitter =\n new EventEmitter();\n\n /** Event emitted when the tab completes its animation towards the center. */\n @Output() readonly _onCentered: EventEmitter = new EventEmitter(\n true\n );\n\n /** The portal host inside of this container into which the tab body content will be loaded. */\n @ViewChild(CdkPortalOutlet) _portalHost: CdkPortalOutlet;\n\n /** The tab body content to display. */\n @Input('content') _content: any;\n\n /** Position that will be used when the tab is immediately becoming visible after creation. */\n @Input() origin: number | null;\n\n // Note that the default value will always be overwritten by `OuiTabBody`, but we need one\n // anyway to prevent the animations module from throwing an error if the body is used on its own.\n /** Duration for the tab's animation. */\n @Input() animationDuration = '0';\n\n /** Whether the tab's content should be kept in the DOM while it's off-screen. */\n @Input() preserveContent = false;\n _content2: any;\n\n /** The shifted index position of the tab body, where zero represents the active center tab. */\n @Input()\n set position(position: number) {\n this._positionIndex = position;\n this._computePositionAnimationState();\n }\n\n constructor(\n private _elementRef: ElementRef,\n @Optional() private _dir: Directionality,\n changeDetectorRef: ChangeDetectorRef\n ) {\n if (_dir) {\n this._dirChangeSubscription = _dir.change.subscribe((dir: Direction) => {\n this._computePositionAnimationState(dir);\n changeDetectorRef.markForCheck();\n });\n }\n\n // Ensure that we get unique animation events, because the `.done` callback can get\n // invoked twice in some browsers. See https://github.com/angular/angular/issues/24084.\n this._translateTabComplete\n .pipe(\n distinctUntilChanged((x, y) => {\n return x.fromState === y.fromState && x.toState === y.toState;\n })\n )\n .subscribe((event) => {\n // If the transition to the center is complete, emit an event.\n if (\n this._isCenterPosition(event.toState) &&\n this._isCenterPosition(this._position)\n ) {\n this._onCentered.emit();\n }\n\n if (\n this._isCenterPosition(event.fromState) &&\n !this._isCenterPosition(this._position)\n ) {\n this._afterLeavingCenter.emit();\n }\n });\n }\n\n /**\n * After initialized, check if the content is centered and has an origin. If so, set the\n * special position states that transition the tab from the left or right before centering.\n */\n ngOnInit() {\n if (this._position == 'center' && this.origin != null) {\n this._position = this._computePositionFromOrigin(this.origin);\n }\n this._content2 = this._content ? this._content : '';\n }\n\n ngOnDestroy() {\n this._dirChangeSubscription.unsubscribe();\n this._translateTabComplete.complete();\n }\n\n _onTranslateTabStarted(event: AnimationEvent): void {\n const isCentering = this._isCenterPosition(event.toState);\n this._beforeCentering.emit(isCentering);\n if (isCentering) {\n this._onCentering.emit(this._elementRef.nativeElement.clientHeight);\n }\n }\n\n /** The text direction of the containing app. */\n _getLayoutDirection(): Direction {\n return this._dir && this._dir.value === 'rtl' ? 'rtl' : 'ltr';\n }\n\n /** Whether the provided position state is considered center, regardless of origin. */\n _isCenterPosition(position: OuiTabBodyPositionState | string): boolean {\n return (\n position == 'center' ||\n position == 'left-origin-center' ||\n position == 'right-origin-center'\n );\n }\n\n /** Computes the position state that will be used for the tab-body animation trigger. */\n private _computePositionAnimationState(\n dir: Direction = this._getLayoutDirection()\n ) {\n if (this._positionIndex < 0) {\n this._position = dir == 'ltr' ? 'left' : 'right';\n } else if (this._positionIndex > 0) {\n this._position = dir == 'ltr' ? 'right' : 'left';\n } else {\n this._position = 'center';\n }\n }\n\n /**\n * Computes the position state based on the specified origin position. This is used if the\n * tab is becoming visible immediately after creation.\n */\n private _computePositionFromOrigin(origin: number): OuiTabBodyPositionState {\n const dir = this._getLayoutDirection();\n\n if ((dir == 'ltr' && origin <= 0) || (dir == 'rtl' && origin > 0)) {\n return 'left-origin-center';\n }\n\n return 'right-origin-center';\n }\n}\n\n/**\n * The origin state is an internally used state that is set on a new tab body indicating if it\n * began to the left or right of the prior selected index. For example, if the selected index was\n * set to 1, and a new tab is created and selected at index 2, then the tab body would have an\n * origin of right because its index was greater than the prior selected index.\n */\nexport type OuiTabBodyOriginState = 'left' | 'right';\n", + "sourceCode": "import {\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n ComponentFactoryResolver,\n Directive,\n ElementRef,\n EventEmitter,\n // forwardRef,\n Inject,\n Input,\n OnDestroy,\n OnInit,\n Optional,\n Output,\n ViewChild,\n ViewContainerRef,\n ViewEncapsulation,\n} from '@angular/core';\nimport { CdkPortalOutlet } from '@angular/cdk/portal';\nimport { Direction, Directionality } from '@angular/cdk/bidi';\nimport { DOCUMENT } from '@angular/common';\nimport { Subject, Subscription } from 'rxjs';\nimport {\n distinctUntilChanged,\n // startWith\n} from 'rxjs/operators';\nimport { AnimationEvent } from '@angular/animations';\nimport { ouiTabsAnimations } from './tabs-animations';\n\n/**\n * The portal host directive for the contents of the tab.\n * @docs-private\n */\n\n@Directive({\n // eslint-disable-next-line @angular-eslint/directive-selector\n selector: `OuiTabBodyHost`,\n})\nexport class OuiTabBodyPortal\n extends CdkPortalOutlet\n implements OnInit, OnDestroy\n{\n /** Subscription to events for when the tab body begins centering. */\n private _centeringSub = Subscription.EMPTY;\n /** Subscription to events for when the tab body finishes leaving from center position. */\n private _leavingSub = Subscription.EMPTY;\n\n constructor(\n componentFactoryResolver: ComponentFactoryResolver,\n viewContainerRef: ViewContainerRef,\n // @Inject(forwardRef(() => OuiTabBody)) private _host: OuiTabBody,\n @Inject(DOCUMENT) _document: any\n ) {\n super(componentFactoryResolver, viewContainerRef, _document);\n }\n\n /** Set initial visibility or set up subscription for changing visibility. */\n override ngOnInit(): void {\n super.ngOnInit();\n }\n\n /** Clean up centering subscription. */\n override ngOnDestroy(): void {\n super.ngOnDestroy();\n this._centeringSub.unsubscribe();\n this._leavingSub.unsubscribe();\n }\n}\n\n/**\n * These position states are used internally as animation states for the tab body. Setting the\n * position state to left, right, or center will transition the tab body from its current\n * position to its respective state. If there is not current position (void, in the case of a new\n * tab body), then there will be no transition animation to its state.\n *\n * In the case of a new tab body that should immediately be centered with an animating transition,\n * then left-origin-center or right-origin-center can be used, which will use left or right as its\n * pseudo-prior state.\n */\nexport type OuiTabBodyPositionState =\n | 'left'\n | 'center'\n | 'right'\n | 'left-origin-center'\n | 'right-origin-center';\n\n/**\n * Wrapper for the contents of a tab.\n * @docs-private\n */\n@Component({\n selector: 'oui-tab-body',\n templateUrl: 'tab-body.html',\n // styleUrls: ['tab-body.css'],\n encapsulation: ViewEncapsulation.None,\n // tslint:disable-next-line:validate-decorators\n changeDetection: ChangeDetectionStrategy.Default,\n animations: [ouiTabsAnimations.translateTab],\n // eslint-disable-next-line @angular-eslint/no-host-metadata-property\n host: {\n class: 'oui-mdc-tab-body',\n },\n})\nexport class OuiTabBody implements OnInit, OnDestroy {\n /** Current position of the tab-body in the tab-group. Zero means that the tab is visible. */\n private _positionIndex: number;\n\n /** Subscription to the directionality change observable. */\n private _dirChangeSubscription = Subscription.EMPTY;\n\n /** Tab body position state. Used by the animation trigger for the current state. */\n _position: OuiTabBodyPositionState;\n\n /** Emits when an animation on the tab is complete. */\n readonly _translateTabComplete = new Subject();\n\n /** Event emitted when the tab begins to animate towards the center as the active tab. */\n @Output() readonly _onCentering: EventEmitter =\n new EventEmitter();\n\n /** Event emitted before the centering of the tab begins. */\n @Output() readonly _beforeCentering: EventEmitter =\n new EventEmitter();\n\n /** Event emitted before the centering of the tab begins. */\n @Output() readonly _afterLeavingCenter: EventEmitter =\n new EventEmitter();\n\n /** Event emitted when the tab completes its animation towards the center. */\n @Output() readonly _onCentered: EventEmitter = new EventEmitter(\n true\n );\n\n /** The portal host inside of this container into which the tab body content will be loaded. */\n @ViewChild(CdkPortalOutlet) _portalHost: CdkPortalOutlet;\n\n /** The tab body content to display. */\n @Input('content') _content: any;\n\n /** Position that will be used when the tab is immediately becoming visible after creation. */\n @Input() origin: number | null;\n\n // Note that the default value will always be overwritten by `OuiTabBody`, but we need one\n // anyway to prevent the animations module from throwing an error if the body is used on its own.\n /** Duration for the tab's animation. */\n @Input() animationDuration = '0';\n\n /** Whether the tab's content should be kept in the DOM while it's off-screen. */\n @Input() preserveContent = false;\n _innerContent: any;\n\n /** The shifted index position of the tab body, where zero represents the active center tab. */\n @Input()\n set position(position: number) {\n this._positionIndex = position;\n this._computePositionAnimationState();\n }\n\n constructor(\n private _elementRef: ElementRef,\n @Optional() private _dir: Directionality,\n changeDetectorRef: ChangeDetectorRef\n ) {\n if (_dir) {\n this._dirChangeSubscription = _dir.change.subscribe((dir: Direction) => {\n this._computePositionAnimationState(dir);\n changeDetectorRef.markForCheck();\n });\n }\n\n // Ensure that we get unique animation events, because the `.done` callback can get\n // invoked twice in some browsers. See https://github.com/angular/angular/issues/24084.\n this._translateTabComplete\n .pipe(\n distinctUntilChanged((x, y) => {\n return x.fromState === y.fromState && x.toState === y.toState;\n })\n )\n .subscribe((event) => {\n // If the transition to the center is complete, emit an event.\n if (\n this._isCenterPosition(event.toState) &&\n this._isCenterPosition(this._position)\n ) {\n this._onCentered.emit();\n }\n\n if (\n this._isCenterPosition(event.fromState) &&\n !this._isCenterPosition(this._position)\n ) {\n this._afterLeavingCenter.emit();\n }\n });\n }\n\n /**\n * After initialized, check if the content is centered and has an origin. If so, set the\n * special position states that transition the tab from the left or right before centering.\n */\n ngOnInit() {\n if (this._position == 'center' && this.origin != null) {\n this._position = this._computePositionFromOrigin(this.origin);\n }\n this._innerContent = this._content ? this._content : '';\n }\n\n ngOnDestroy() {\n this._dirChangeSubscription.unsubscribe();\n this._translateTabComplete.complete();\n }\n\n _onTranslateTabStarted(event: AnimationEvent): void {\n const isCentering = this._isCenterPosition(event.toState);\n this._beforeCentering.emit(isCentering);\n if (isCentering) {\n this._onCentering.emit(this._elementRef.nativeElement.clientHeight);\n }\n }\n\n /** The text direction of the containing app. */\n _getLayoutDirection(): Direction {\n return this._dir && this._dir.value === 'rtl' ? 'rtl' : 'ltr';\n }\n\n /** Whether the provided position state is considered center, regardless of origin. */\n _isCenterPosition(position: OuiTabBodyPositionState | string): boolean {\n return (\n position == 'center' ||\n position == 'left-origin-center' ||\n position == 'right-origin-center'\n );\n }\n\n /** Computes the position state that will be used for the tab-body animation trigger. */\n private _computePositionAnimationState(\n dir: Direction = this._getLayoutDirection()\n ) {\n if (this._positionIndex < 0) {\n this._position = dir == 'ltr' ? 'left' : 'right';\n } else if (this._positionIndex > 0) {\n this._position = dir == 'ltr' ? 'right' : 'left';\n } else {\n this._position = 'center';\n }\n }\n\n /**\n * Computes the position state based on the specified origin position. This is used if the\n * tab is becoming visible immediately after creation.\n */\n private _computePositionFromOrigin(origin: number): OuiTabBodyPositionState {\n const dir = this._getLayoutDirection();\n\n if ((dir == 'ltr' && origin <= 0) || (dir == 'rtl' && origin > 0)) {\n return 'left-origin-center';\n }\n\n return 'right-origin-center';\n }\n}\n\n/**\n * The origin state is an internally used state that is set on a new tab body indicating if it\n * began to the left or right of the prior selected index. For example, if the selected index was\n * set to 1, and a new tab is created and selected at index 2, then the tab body would have an\n * origin of right because its index was greater than the prior selected index.\n */\nexport type OuiTabBodyOriginState = 'left' | 'right';\n", "selector": "OuiTabBodyHost", "providers": [], "inputsClass": [], @@ -34988,7 +34988,7 @@ "assetsDirs": [], "styleUrlsData": [ { - "data": "@import '../../../checkbox/checkbox.scss';\n\n// The width/height of the checkbox element.\n$oui-checkbox-size: 16px !default;\n\n// The width of the checkbox border shown when the checkbox is unchecked.\n$oui-checkbox-border-width: 1px;\n\n// The base duration used for the majority of transitions for the checkbox.\n$oui-checkbox-transition-duration: 90ms;\n\n// Padding inside of a pseudo checkbox.\n$_oui-pseudo-checkbox-padding: $oui-checkbox-border-width * 2;\n\n// Size of the checkmark in a pseudo checkbox.\n$_oui-pseudo-checkmark-size: $oui-checkbox-size -\n (3.5 * $_oui-pseudo-checkbox-padding);\n\n.oui-pseudo-checkbox {\n width: $oui-checkbox-size;\n height: $oui-checkbox-size;\n border: $oui-checkbox-border-width solid #9c9c9c;\n border-radius: 3px;\n cursor: pointer;\n display: inline-block;\n vertical-align: middle;\n box-sizing: border-box;\n position: relative;\n flex-shrink: 0;\n transition: border-color $oui-checkbox-transition-duration\n $oui-linear-out-slow-in-timing-function,\n background-color $oui-checkbox-transition-duration\n $oui-linear-out-slow-in-timing-function;\n\n // Used to render the checkmark/mixedmark inside of the box.\n &::after {\n position: absolute;\n opacity: 0;\n content: '';\n border-bottom: $oui-checkbox-border-width * 2 solid #333;\n transition: opacity $oui-checkbox-transition-duration\n $oui-linear-out-slow-in-timing-function;\n }\n}\n\n.oui-pseudo-checkbox-disabled {\n cursor: default;\n}\n\n.oui-pseudo-checkbox-checked::after {\n top: ($oui-checkbox-size * 0.5) - ($_oui-pseudo-checkmark-size * 0.25) -\n ($oui-checkbox-size * 0.1) - $oui-checkbox-border-width;\n left: $_oui-pseudo-checkbox-padding - $oui-checkbox-border-width * 0.5;\n width: 10px;\n height: 6px;\n border-left: $oui-checkbox-border-width * 2 solid #333;\n transform: rotate(-45deg);\n opacity: 1;\n}\n", + "data": "@import '../../../checkbox/checkbox.scss';\n\n// The width/height of the checkbox element.\n$oui-checkbox-size: 16px !default;\n\n// The width of the checkbox border shown when the checkbox is unchecked.\n$oui-checkbox-border-width: 1px;\n\n// The base duration used for the majority of transitions for the checkbox.\n$oui-checkbox-transition-duration: 90ms;\n\n// Padding inside of a pseudo checkbox.\n$_oui-pseudo-checkbox-padding: $oui-checkbox-border-width * 2;\n\n// Size of the checkmark in a pseudo checkbox.\n$_oui-pseudo-checkmark-size: $oui-checkbox-size -\n (3.5 * $_oui-pseudo-checkbox-padding);\n\n.oui-pseudo-checkbox {\n width: $oui-checkbox-size;\n height: $oui-checkbox-size;\n border: 1px solid #9c9c9c;\n border-radius: 3px;\n cursor: pointer;\n display: block;\n box-sizing: border-box;\n position: relative;\n flex-shrink: 0;\n transition: border-color $oui-checkbox-transition-duration\n $oui-linear-out-slow-in-timing-function,\n background-color $oui-checkbox-transition-duration\n $oui-linear-out-slow-in-timing-function;\n\n // Used to render the checkmark/mixedmark inside of the box.\n &::after {\n content: '';\n display: block;\n position: absolute;\n box-sizing: border-box;\n transform: rotate(-45deg);\n border-bottom: 2px solid #333;\n border-left: 2px solid #333;\n top: 3.5px;\n left: 2px;\n width: 10px;\n height: 5px;\n transition: opacity 90ms $oui-linear-out-slow-in-timing-function;\n opacity: 0;\n }\n}\n\n.oui-pseudo-checkbox-disabled {\n cursor: default;\n}\n\n.oui-pseudo-checkbox-checked::after {\n opacity: 1;\n}\n", "styleUrl": "pseudo-checkbox.scss" } ], @@ -41982,7 +41982,7 @@ }, { "name": "OuiTabBody", - "id": "component-OuiTabBody-0479dbe7621da9a0746cbaad99fc9fe7e9304251250d9209414bfb2d15351a0c25ab1201b5d3c77ae1abcd3a64ac5e58575191a4df0d1c7cf665c2c5f4cfb720", + "id": "component-OuiTabBody-d9aba0e7f5c272163d8a1fcdea5954740c5bac6598de09205831a017a4275c387861d98bd5093d058798cb1713335c4f3d57c032469562999ee21727593e8750", "file": "ui/src/components/tabs/tab-body.ts", "changeDetection": "ChangeDetectionStrategy.Default", "encapsulation": [ @@ -42097,15 +42097,6 @@ } ], "propertiesClass": [ - { - "name": "_content2", - "deprecated": false, - "deprecationMessage": "", - "type": "any", - "optional": false, - "description": "", - "line": 159 - }, { "name": "_dirChangeSubscription", "defaultValue": "Subscription.EMPTY", @@ -42120,6 +42111,15 @@ 121 ] }, + { + "name": "_innerContent", + "deprecated": false, + "deprecationMessage": "", + "type": "any", + "optional": false, + "description": "", + "line": 159 + }, { "name": "_portalHost", "deprecated": false, @@ -42343,7 +42343,7 @@ "description": "

Wrapper for the contents of a tab.

\n", "rawdescription": "\n\nWrapper for the contents of a tab.\n", "type": "component", - "sourceCode": "import {\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n ComponentFactoryResolver,\n Directive,\n ElementRef,\n EventEmitter,\n // forwardRef,\n Inject,\n Input,\n OnDestroy,\n OnInit,\n Optional,\n Output,\n ViewChild,\n ViewContainerRef,\n ViewEncapsulation,\n} from '@angular/core';\nimport { CdkPortalOutlet } from '@angular/cdk/portal';\nimport { Direction, Directionality } from '@angular/cdk/bidi';\nimport { DOCUMENT } from '@angular/common';\nimport { Subject, Subscription } from 'rxjs';\nimport {\n distinctUntilChanged,\n // startWith\n} from 'rxjs/operators';\nimport { AnimationEvent } from '@angular/animations';\nimport { ouiTabsAnimations } from './tabs-animations';\n\n/**\n * The portal host directive for the contents of the tab.\n * @docs-private\n */\n\n@Directive({\n // eslint-disable-next-line @angular-eslint/directive-selector\n selector: `OuiTabBodyHost`,\n})\nexport class OuiTabBodyPortal\n extends CdkPortalOutlet\n implements OnInit, OnDestroy\n{\n /** Subscription to events for when the tab body begins centering. */\n private _centeringSub = Subscription.EMPTY;\n /** Subscription to events for when the tab body finishes leaving from center position. */\n private _leavingSub = Subscription.EMPTY;\n\n constructor(\n componentFactoryResolver: ComponentFactoryResolver,\n viewContainerRef: ViewContainerRef,\n // @Inject(forwardRef(() => OuiTabBody)) private _host: OuiTabBody,\n @Inject(DOCUMENT) _document: any\n ) {\n super(componentFactoryResolver, viewContainerRef, _document);\n }\n\n /** Set initial visibility or set up subscription for changing visibility. */\n override ngOnInit(): void {\n super.ngOnInit();\n }\n\n /** Clean up centering subscription. */\n override ngOnDestroy(): void {\n super.ngOnDestroy();\n this._centeringSub.unsubscribe();\n this._leavingSub.unsubscribe();\n }\n}\n\n/**\n * These position states are used internally as animation states for the tab body. Setting the\n * position state to left, right, or center will transition the tab body from its current\n * position to its respective state. If there is not current position (void, in the case of a new\n * tab body), then there will be no transition animation to its state.\n *\n * In the case of a new tab body that should immediately be centered with an animating transition,\n * then left-origin-center or right-origin-center can be used, which will use left or right as its\n * pseudo-prior state.\n */\nexport type OuiTabBodyPositionState =\n | 'left'\n | 'center'\n | 'right'\n | 'left-origin-center'\n | 'right-origin-center';\n\n/**\n * Wrapper for the contents of a tab.\n * @docs-private\n */\n@Component({\n selector: 'oui-tab-body',\n templateUrl: 'tab-body.html',\n // styleUrls: ['tab-body.css'],\n encapsulation: ViewEncapsulation.None,\n // tslint:disable-next-line:validate-decorators\n changeDetection: ChangeDetectionStrategy.Default,\n animations: [ouiTabsAnimations.translateTab],\n // eslint-disable-next-line @angular-eslint/no-host-metadata-property\n host: {\n class: 'oui-mdc-tab-body',\n },\n})\nexport class OuiTabBody implements OnInit, OnDestroy {\n /** Current position of the tab-body in the tab-group. Zero means that the tab is visible. */\n private _positionIndex: number;\n\n /** Subscription to the directionality change observable. */\n private _dirChangeSubscription = Subscription.EMPTY;\n\n /** Tab body position state. Used by the animation trigger for the current state. */\n _position: OuiTabBodyPositionState;\n\n /** Emits when an animation on the tab is complete. */\n readonly _translateTabComplete = new Subject();\n\n /** Event emitted when the tab begins to animate towards the center as the active tab. */\n @Output() readonly _onCentering: EventEmitter =\n new EventEmitter();\n\n /** Event emitted before the centering of the tab begins. */\n @Output() readonly _beforeCentering: EventEmitter =\n new EventEmitter();\n\n /** Event emitted before the centering of the tab begins. */\n @Output() readonly _afterLeavingCenter: EventEmitter =\n new EventEmitter();\n\n /** Event emitted when the tab completes its animation towards the center. */\n @Output() readonly _onCentered: EventEmitter = new EventEmitter(\n true\n );\n\n /** The portal host inside of this container into which the tab body content will be loaded. */\n @ViewChild(CdkPortalOutlet) _portalHost: CdkPortalOutlet;\n\n /** The tab body content to display. */\n @Input('content') _content: any;\n\n /** Position that will be used when the tab is immediately becoming visible after creation. */\n @Input() origin: number | null;\n\n // Note that the default value will always be overwritten by `OuiTabBody`, but we need one\n // anyway to prevent the animations module from throwing an error if the body is used on its own.\n /** Duration for the tab's animation. */\n @Input() animationDuration = '0';\n\n /** Whether the tab's content should be kept in the DOM while it's off-screen. */\n @Input() preserveContent = false;\n _content2: any;\n\n /** The shifted index position of the tab body, where zero represents the active center tab. */\n @Input()\n set position(position: number) {\n this._positionIndex = position;\n this._computePositionAnimationState();\n }\n\n constructor(\n private _elementRef: ElementRef,\n @Optional() private _dir: Directionality,\n changeDetectorRef: ChangeDetectorRef\n ) {\n if (_dir) {\n this._dirChangeSubscription = _dir.change.subscribe((dir: Direction) => {\n this._computePositionAnimationState(dir);\n changeDetectorRef.markForCheck();\n });\n }\n\n // Ensure that we get unique animation events, because the `.done` callback can get\n // invoked twice in some browsers. See https://github.com/angular/angular/issues/24084.\n this._translateTabComplete\n .pipe(\n distinctUntilChanged((x, y) => {\n return x.fromState === y.fromState && x.toState === y.toState;\n })\n )\n .subscribe((event) => {\n // If the transition to the center is complete, emit an event.\n if (\n this._isCenterPosition(event.toState) &&\n this._isCenterPosition(this._position)\n ) {\n this._onCentered.emit();\n }\n\n if (\n this._isCenterPosition(event.fromState) &&\n !this._isCenterPosition(this._position)\n ) {\n this._afterLeavingCenter.emit();\n }\n });\n }\n\n /**\n * After initialized, check if the content is centered and has an origin. If so, set the\n * special position states that transition the tab from the left or right before centering.\n */\n ngOnInit() {\n if (this._position == 'center' && this.origin != null) {\n this._position = this._computePositionFromOrigin(this.origin);\n }\n this._content2 = this._content ? this._content : '';\n }\n\n ngOnDestroy() {\n this._dirChangeSubscription.unsubscribe();\n this._translateTabComplete.complete();\n }\n\n _onTranslateTabStarted(event: AnimationEvent): void {\n const isCentering = this._isCenterPosition(event.toState);\n this._beforeCentering.emit(isCentering);\n if (isCentering) {\n this._onCentering.emit(this._elementRef.nativeElement.clientHeight);\n }\n }\n\n /** The text direction of the containing app. */\n _getLayoutDirection(): Direction {\n return this._dir && this._dir.value === 'rtl' ? 'rtl' : 'ltr';\n }\n\n /** Whether the provided position state is considered center, regardless of origin. */\n _isCenterPosition(position: OuiTabBodyPositionState | string): boolean {\n return (\n position == 'center' ||\n position == 'left-origin-center' ||\n position == 'right-origin-center'\n );\n }\n\n /** Computes the position state that will be used for the tab-body animation trigger. */\n private _computePositionAnimationState(\n dir: Direction = this._getLayoutDirection()\n ) {\n if (this._positionIndex < 0) {\n this._position = dir == 'ltr' ? 'left' : 'right';\n } else if (this._positionIndex > 0) {\n this._position = dir == 'ltr' ? 'right' : 'left';\n } else {\n this._position = 'center';\n }\n }\n\n /**\n * Computes the position state based on the specified origin position. This is used if the\n * tab is becoming visible immediately after creation.\n */\n private _computePositionFromOrigin(origin: number): OuiTabBodyPositionState {\n const dir = this._getLayoutDirection();\n\n if ((dir == 'ltr' && origin <= 0) || (dir == 'rtl' && origin > 0)) {\n return 'left-origin-center';\n }\n\n return 'right-origin-center';\n }\n}\n\n/**\n * The origin state is an internally used state that is set on a new tab body indicating if it\n * began to the left or right of the prior selected index. For example, if the selected index was\n * set to 1, and a new tab is created and selected at index 2, then the tab body would have an\n * origin of right because its index was greater than the prior selected index.\n */\nexport type OuiTabBodyOriginState = 'left' | 'right';\n", + "sourceCode": "import {\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n ComponentFactoryResolver,\n Directive,\n ElementRef,\n EventEmitter,\n // forwardRef,\n Inject,\n Input,\n OnDestroy,\n OnInit,\n Optional,\n Output,\n ViewChild,\n ViewContainerRef,\n ViewEncapsulation,\n} from '@angular/core';\nimport { CdkPortalOutlet } from '@angular/cdk/portal';\nimport { Direction, Directionality } from '@angular/cdk/bidi';\nimport { DOCUMENT } from '@angular/common';\nimport { Subject, Subscription } from 'rxjs';\nimport {\n distinctUntilChanged,\n // startWith\n} from 'rxjs/operators';\nimport { AnimationEvent } from '@angular/animations';\nimport { ouiTabsAnimations } from './tabs-animations';\n\n/**\n * The portal host directive for the contents of the tab.\n * @docs-private\n */\n\n@Directive({\n // eslint-disable-next-line @angular-eslint/directive-selector\n selector: `OuiTabBodyHost`,\n})\nexport class OuiTabBodyPortal\n extends CdkPortalOutlet\n implements OnInit, OnDestroy\n{\n /** Subscription to events for when the tab body begins centering. */\n private _centeringSub = Subscription.EMPTY;\n /** Subscription to events for when the tab body finishes leaving from center position. */\n private _leavingSub = Subscription.EMPTY;\n\n constructor(\n componentFactoryResolver: ComponentFactoryResolver,\n viewContainerRef: ViewContainerRef,\n // @Inject(forwardRef(() => OuiTabBody)) private _host: OuiTabBody,\n @Inject(DOCUMENT) _document: any\n ) {\n super(componentFactoryResolver, viewContainerRef, _document);\n }\n\n /** Set initial visibility or set up subscription for changing visibility. */\n override ngOnInit(): void {\n super.ngOnInit();\n }\n\n /** Clean up centering subscription. */\n override ngOnDestroy(): void {\n super.ngOnDestroy();\n this._centeringSub.unsubscribe();\n this._leavingSub.unsubscribe();\n }\n}\n\n/**\n * These position states are used internally as animation states for the tab body. Setting the\n * position state to left, right, or center will transition the tab body from its current\n * position to its respective state. If there is not current position (void, in the case of a new\n * tab body), then there will be no transition animation to its state.\n *\n * In the case of a new tab body that should immediately be centered with an animating transition,\n * then left-origin-center or right-origin-center can be used, which will use left or right as its\n * pseudo-prior state.\n */\nexport type OuiTabBodyPositionState =\n | 'left'\n | 'center'\n | 'right'\n | 'left-origin-center'\n | 'right-origin-center';\n\n/**\n * Wrapper for the contents of a tab.\n * @docs-private\n */\n@Component({\n selector: 'oui-tab-body',\n templateUrl: 'tab-body.html',\n // styleUrls: ['tab-body.css'],\n encapsulation: ViewEncapsulation.None,\n // tslint:disable-next-line:validate-decorators\n changeDetection: ChangeDetectionStrategy.Default,\n animations: [ouiTabsAnimations.translateTab],\n // eslint-disable-next-line @angular-eslint/no-host-metadata-property\n host: {\n class: 'oui-mdc-tab-body',\n },\n})\nexport class OuiTabBody implements OnInit, OnDestroy {\n /** Current position of the tab-body in the tab-group. Zero means that the tab is visible. */\n private _positionIndex: number;\n\n /** Subscription to the directionality change observable. */\n private _dirChangeSubscription = Subscription.EMPTY;\n\n /** Tab body position state. Used by the animation trigger for the current state. */\n _position: OuiTabBodyPositionState;\n\n /** Emits when an animation on the tab is complete. */\n readonly _translateTabComplete = new Subject();\n\n /** Event emitted when the tab begins to animate towards the center as the active tab. */\n @Output() readonly _onCentering: EventEmitter =\n new EventEmitter();\n\n /** Event emitted before the centering of the tab begins. */\n @Output() readonly _beforeCentering: EventEmitter =\n new EventEmitter();\n\n /** Event emitted before the centering of the tab begins. */\n @Output() readonly _afterLeavingCenter: EventEmitter =\n new EventEmitter();\n\n /** Event emitted when the tab completes its animation towards the center. */\n @Output() readonly _onCentered: EventEmitter = new EventEmitter(\n true\n );\n\n /** The portal host inside of this container into which the tab body content will be loaded. */\n @ViewChild(CdkPortalOutlet) _portalHost: CdkPortalOutlet;\n\n /** The tab body content to display. */\n @Input('content') _content: any;\n\n /** Position that will be used when the tab is immediately becoming visible after creation. */\n @Input() origin: number | null;\n\n // Note that the default value will always be overwritten by `OuiTabBody`, but we need one\n // anyway to prevent the animations module from throwing an error if the body is used on its own.\n /** Duration for the tab's animation. */\n @Input() animationDuration = '0';\n\n /** Whether the tab's content should be kept in the DOM while it's off-screen. */\n @Input() preserveContent = false;\n _innerContent: any;\n\n /** The shifted index position of the tab body, where zero represents the active center tab. */\n @Input()\n set position(position: number) {\n this._positionIndex = position;\n this._computePositionAnimationState();\n }\n\n constructor(\n private _elementRef: ElementRef,\n @Optional() private _dir: Directionality,\n changeDetectorRef: ChangeDetectorRef\n ) {\n if (_dir) {\n this._dirChangeSubscription = _dir.change.subscribe((dir: Direction) => {\n this._computePositionAnimationState(dir);\n changeDetectorRef.markForCheck();\n });\n }\n\n // Ensure that we get unique animation events, because the `.done` callback can get\n // invoked twice in some browsers. See https://github.com/angular/angular/issues/24084.\n this._translateTabComplete\n .pipe(\n distinctUntilChanged((x, y) => {\n return x.fromState === y.fromState && x.toState === y.toState;\n })\n )\n .subscribe((event) => {\n // If the transition to the center is complete, emit an event.\n if (\n this._isCenterPosition(event.toState) &&\n this._isCenterPosition(this._position)\n ) {\n this._onCentered.emit();\n }\n\n if (\n this._isCenterPosition(event.fromState) &&\n !this._isCenterPosition(this._position)\n ) {\n this._afterLeavingCenter.emit();\n }\n });\n }\n\n /**\n * After initialized, check if the content is centered and has an origin. If so, set the\n * special position states that transition the tab from the left or right before centering.\n */\n ngOnInit() {\n if (this._position == 'center' && this.origin != null) {\n this._position = this._computePositionFromOrigin(this.origin);\n }\n this._innerContent = this._content ? this._content : '';\n }\n\n ngOnDestroy() {\n this._dirChangeSubscription.unsubscribe();\n this._translateTabComplete.complete();\n }\n\n _onTranslateTabStarted(event: AnimationEvent): void {\n const isCentering = this._isCenterPosition(event.toState);\n this._beforeCentering.emit(isCentering);\n if (isCentering) {\n this._onCentering.emit(this._elementRef.nativeElement.clientHeight);\n }\n }\n\n /** The text direction of the containing app. */\n _getLayoutDirection(): Direction {\n return this._dir && this._dir.value === 'rtl' ? 'rtl' : 'ltr';\n }\n\n /** Whether the provided position state is considered center, regardless of origin. */\n _isCenterPosition(position: OuiTabBodyPositionState | string): boolean {\n return (\n position == 'center' ||\n position == 'left-origin-center' ||\n position == 'right-origin-center'\n );\n }\n\n /** Computes the position state that will be used for the tab-body animation trigger. */\n private _computePositionAnimationState(\n dir: Direction = this._getLayoutDirection()\n ) {\n if (this._positionIndex < 0) {\n this._position = dir == 'ltr' ? 'left' : 'right';\n } else if (this._positionIndex > 0) {\n this._position = dir == 'ltr' ? 'right' : 'left';\n } else {\n this._position = 'center';\n }\n }\n\n /**\n * Computes the position state based on the specified origin position. This is used if the\n * tab is becoming visible immediately after creation.\n */\n private _computePositionFromOrigin(origin: number): OuiTabBodyPositionState {\n const dir = this._getLayoutDirection();\n\n if ((dir == 'ltr' && origin <= 0) || (dir == 'rtl' && origin > 0)) {\n return 'left-origin-center';\n }\n\n return 'right-origin-center';\n }\n}\n\n/**\n * The origin state is an internally used state that is set on a new tab body indicating if it\n * began to the left or right of the prior selected index. For example, if the selected index was\n * set to 1, and a new tab is created and selected at index 2, then the tab body would have an\n * origin of right because its index was greater than the prior selected index.\n */\nexport type OuiTabBodyOriginState = 'left' | 'right';\n", "assetsDirs": [], "styleUrlsData": "", "stylesData": "", @@ -42441,7 +42441,7 @@ } } }, - "templateData": "\n \n \n\n" + "templateData": "\n \n \n\n" }, { "name": "ouiTabGroup", @@ -43396,7 +43396,7 @@ "assetsDirs": [], "styleUrlsData": [ { - "data": "@use '../core/style/variables';\n@use './tabs-common';\n\n@include tabs-common.structural-styles;\n\n.oui-mdc-tab {\n @include tabs-common.tab;\n\n // Note that we only want to target direct descendant tabs.\n .oui-mdc-tab-group.oui-mdc-tab-group-stretch-tabs > .oui-mdc-tab-header & {\n flex-grow: 1;\n }\n}\n\n.oui-mdc-focus-indicator {\n position: relative;\n}\n\n.oui-mdc-tab-body {\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n position: absolute;\n display: none;\n overflow: hidden;\n flex-basis: 100%;\n &.oui-mdc-tab-body-active {\n display: block;\n }\n}\n\n.mdc-tab__content {\n display: flex;\n align-items: center;\n justify-content: center;\n height: inherit;\n pointer-events: none;\n}\n\n.oui-mdc-tab.mdc-tab--active .mdc-tab__text-label {\n color: #333;\n}\n\n.oui-mdc-tab-group {\n // @include tabs-common.paginated-tab-header-with-background('.oui-mdc-tab-header', '.oui-mdc-tab');\n display: flex;\n flex-direction: column;\n\n // Fixes pagination issues inside flex containers (see #23157).\n max-width: 100%;\n\n &.oui-mdc-tab-group-inverted-header {\n flex-direction: column-reverse;\n\n .mdc-tab-indicator__content--underline {\n align-self: flex-start;\n }\n }\n}\n\n// The bottom section of the view; contains the tab bodies\n.oui-mdc-tab-body-wrapper {\n // @include private.private-animation-noop();\n position: relative;\n overflow: hidden;\n display: flex;\n}\n", + "data": "@use '../core/style/variables';\n@use './tabs-common';\n\n@include tabs-common.structural-styles;\n\n.oui-mdc-tab {\n @include tabs-common.tab;\n\n // Note that we only want to target direct descendant tabs.\n .oui-mdc-tab-group.oui-mdc-tab-group-stretch-tabs > .oui-mdc-tab-header & {\n flex-grow: 1;\n }\n}\n\n.oui-mdc-focus-indicator {\n position: relative;\n}\n\n.oui-mdc-tab-body {\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n position: absolute;\n display: none;\n overflow: hidden;\n flex-basis: 100%;\n &.oui-mdc-tab-body-active {\n display: block;\n }\n}\n\n.mdc-tab__content {\n display: flex;\n align-items: center;\n justify-content: center;\n height: inherit;\n pointer-events: none;\n}\n\n.oui-mdc-tab.mdc-tab--active .mdc-tab__text-label {\n color: #333;\n}\n\n.oui-mdc-tab-group {\n display: flex;\n flex-direction: column;\n\n // Fixes pagination issues inside flex containers (see #23157).\n max-width: 100%;\n\n &.oui-mdc-tab-group-inverted-header {\n flex-direction: column-reverse;\n\n .mdc-tab-indicator__content--underline {\n align-self: flex-start;\n }\n }\n}\n\n// The bottom section of the view; contains the tab bodies\n.oui-mdc-tab-body-wrapper {\n position: relative;\n overflow: hidden;\n display: flex;\n}\n\n.oui-text-height {\n min-height: 100px;\n}\n", "styleUrl": "tab-group.scss" } ], @@ -43828,7 +43828,7 @@ } } }, - "templateData": "\n \n \n\n \n
\n\n \n \n \n \n \n \n\n \n {{tab.textLabel}}\n \n \n \n\n\n\n \n \n\n" + "templateData": "\n \n \n\n \n
\n\n \n \n \n \n \n \n\n \n {{tab.textLabel}}\n \n \n \n\n\n\n \n \n\n" }, { "name": "OuiTabHeader", @@ -47377,7 +47377,7 @@ }, { "name": "OuiTabStorybook", - "id": "component-OuiTabStorybook-0509b7f65f915663fc1bc4a20dcb7ae47e630da7fcbddd80d93542f2b630b1bcc36b7f739247e5230d349c0bb1508ee11c45efa2de0e74aef28168e3287b5246", + "id": "component-OuiTabStorybook-4826c889c695832f5994b7906333b44978af45ba140206e67d50876784e432180a396c0a0334557411bf498fcc1188da44774ce795be3962f8c5addf8323ee23", "file": "ui/src/stories/tabs/tabs.component.ts", "encapsulation": [], "entryComponents": [], @@ -47387,7 +47387,7 @@ "selector": "oui-tab-storybook", "styleUrls": [], "styles": [], - "template": "\n AAAAA123\">\n BBBBB123\">\n CCCCCC123\">\n \n\n", + "template": "\n \n BBBBB123\">\n CCCCCC123\">\n \n\n", "templateUrl": [], "viewProviders": [], "inputsClass": [], @@ -47401,7 +47401,7 @@ "description": "", "rawdescription": "\n", "type": "component", - "sourceCode": "import { Component } from '@angular/core';\n\n@Component({\n selector: 'oui-tab-storybook',\n template: `\n \n AAAAA123\">\n BBBBB123\">\n CCCCCC123\">\n \n \n `,\n})\nexport class OuiTabStorybook {\n constructor() {}\n}\n", + "sourceCode": "import { Component } from '@angular/core';\n\n@Component({\n selector: 'oui-tab-storybook',\n template: `\n \n \n BBBBB123\">\n CCCCCC123\">\n \n \n `,\n})\nexport class OuiTabStorybook {\n constructor() {}\n}\n", "assetsDirs": [], "styleUrlsData": "", "stylesData": "", @@ -61690,8 +61690,8 @@ }, { "filePath": "ui/src/components/select/select.component.ts", - "type": "directive", - "linktype": "directive", + "type": "component", + "linktype": "component", "name": "OuiSelectTrigger", "coveragePercent": 100, "coverageCount": "1/1", diff --git a/package-lock.json b/package-lock.json index 17ea978bd..e82c425d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "oncehub-ui", - "version": "8.0.0", + "version": "8.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "oncehub-ui", - "version": "8.0.0", + "version": "8.0.1", "dependencies": { "@angular-devkit/architect": "0.1601.6", "@angular-devkit/core": "16.1.6", diff --git a/package.json b/package.json index 4609ad242..35d669e7a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oncehub-ui", - "version": "8.0.0", + "version": "8.0.1", "scripts": { "ng": "ng", "build": "ng build ui", diff --git a/ui/package-lock.json b/ui/package-lock.json index e0237ad21..e527979eb 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -1,12 +1,12 @@ { "name": "@oncehub/ui", - "version": "8.0.0", + "version": "8.0.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@oncehub/ui", - "version": "8.0.0", + "version": "8.0.1", "dependencies": { "tslib": "^2.4.0" } diff --git a/ui/package.json b/ui/package.json index eb5c76d57..a89ae79cd 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "@oncehub/ui", - "version": "8.0.0", + "version": "8.0.1", "description": "Oncehub UI", "peerDependencies": {}, "repository": { diff --git a/ui/src/components/tabs/README.md b/ui/src/components/tabs/README.md index 5f068875e..94e9771cc 100644 --- a/ui/src/components/tabs/README.md +++ b/ui/src/components/tabs/README.md @@ -1 +1,48 @@ -Please see the official documentation at https://material.angular.io/components/component/tabs +The `oui-tab` efficiently arrange content into distinct views, allowing visibility for only one view at a time. +The tab header displays each tab's label, with the active tab highlighted by an animated ink bar. + +### Usage + +```js +import { OuiTabsModule } from '@oncehub/ui'; + +@NgModule({ + imports: [ + OuiTabsModule + ] +}) +``` + +## Usage Example + +```html + + + + + +``` + +## API OuiTab + +| Input | Type | Default | Description | +| ----- | ------ | ------- | ----------------------------------------------------------- | +| label | string | empty | Label Text | +| text | string | empty | The text/HTML that we want to display in the respective tab | + +| Method | Description | +| ----------------- | ---------------------------------------------- | +| selectedTabChange | The selectedTabChange, emits the change event. | + +## Stackblitz demo link + +[https://stackblitz.com/edit/oui-tab](https://stackblitz.com/edit/oui-tab) + +You can click here and can change code to try and test different scenarios. + +### Accessibility + +Elements with the `ouiTab` will add an `aria-describedby` label that provides a reference +to a visually hidden element containing the title. This provides screenreaders the +information needed to read out the contents when the end-user focuses on the element +triggering the tab. diff --git a/ui/src/components/tabs/tab-body.html b/ui/src/components/tabs/tab-body.html index d61fb620c..aa02bae4a 100644 --- a/ui/src/components/tabs/tab-body.html +++ b/ui/src/components/tabs/tab-body.html @@ -1,9 +1,4 @@ -
+
diff --git a/ui/src/components/tabs/tab-body.spec.ts b/ui/src/components/tabs/tab-body.spec.ts index 50f4b58a5..b346e2406 100644 --- a/ui/src/components/tabs/tab-body.spec.ts +++ b/ui/src/components/tabs/tab-body.spec.ts @@ -10,9 +10,8 @@ import { } from '@angular/core'; import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { CdkScrollable, ScrollingModule } from '@angular/cdk/scrolling'; +import { ScrollingModule } from '@angular/cdk/scrolling'; import { OuiTabBody, OuiTabBodyPortal } from './tab-body'; -import { By } from '@angular/platform-browser'; import { Subject } from 'rxjs'; describe('MDC-based OuiTabBody', () => { @@ -203,15 +202,6 @@ describe('MDC-based OuiTabBody', () => { declarations: [OuiTabBody, OuiTabBodyPortal, SimpleTabBodyApp], }) .compileComponents(); - - const fixture = TestBed.createComponent(SimpleTabBodyApp); - const tabBodyContent = fixture.nativeElement.querySelector( - '.oui-mdc-tab-body-content' - ); - const scrollable = fixture.debugElement.query(By.directive(CdkScrollable)); - - expect(scrollable).toBeTruthy(); - expect(scrollable.nativeElement).toBe(tabBodyContent); }); }); diff --git a/ui/src/components/tabs/tab-body.ts b/ui/src/components/tabs/tab-body.ts index 688e9391c..0430fa794 100644 --- a/ui/src/components/tabs/tab-body.ts +++ b/ui/src/components/tabs/tab-body.ts @@ -156,7 +156,7 @@ export class OuiTabBody implements OnInit, OnDestroy { /** Whether the tab's content should be kept in the DOM while it's off-screen. */ @Input() preserveContent = false; - _content2: any; + _innerContent: any; /** The shifted index position of the tab body, where zero represents the active center tab. */ @Input() @@ -211,7 +211,7 @@ export class OuiTabBody implements OnInit, OnDestroy { if (this._position == 'center' && this.origin != null) { this._position = this._computePositionFromOrigin(this.origin); } - this._content2 = this._content ? this._content : ''; + this._innerContent = this._content ? this._content : ''; } ngOnDestroy() { diff --git a/ui/src/components/tabs/tab-group.html b/ui/src/components/tabs/tab-group.html index 8f63b769a..5561882fa 100644 --- a/ui/src/components/tabs/tab-group.html +++ b/ui/src/components/tabs/tab-group.html @@ -51,6 +51,7 @@
diff --git a/ui/src/components/tabs/tab-group.scss b/ui/src/components/tabs/tab-group.scss index 7848cb9ff..0c231255d 100644 --- a/ui/src/components/tabs/tab-group.scss +++ b/ui/src/components/tabs/tab-group.scss @@ -43,7 +43,6 @@ } .oui-mdc-tab-group { - // @include tabs-common.paginated-tab-header-with-background('.oui-mdc-tab-header', '.oui-mdc-tab'); display: flex; flex-direction: column; @@ -61,8 +60,11 @@ // The bottom section of the view; contains the tab bodies .oui-mdc-tab-body-wrapper { - // @include private.private-animation-noop(); position: relative; overflow: hidden; display: flex; } + +.oui-text-height { + min-height: 100px; +} diff --git a/ui/src/stories/tabs/tabs.component.ts b/ui/src/stories/tabs/tabs.component.ts index eb4ba5d40..60997eabe 100644 --- a/ui/src/stories/tabs/tabs.component.ts +++ b/ui/src/stories/tabs/tabs.component.ts @@ -4,11 +4,9 @@ import { Component } from '@angular/core'; selector: 'oui-tab-storybook', template: ` - - - - + + + `, })