From de30ac0a9039b0f34f9de0b50175758a1bcc3d49 Mon Sep 17 00:00:00 2001 From: Jeremy Elbourn Date: Thu, 29 Jun 2017 18:47:39 -0700 Subject: [PATCH] feat: move a11y, bidi, platform, rxjs, and portal to cdk (#5386) * feat: move a11y, bidi, platform, and portal to cdk * lint * Fix weird rxjs operator typing issue * Workaround for ngc issue * debug ie11 * debug ios * Add directionaltiy reexport --- src/cdk/a11y/README.md | 30 ++ src/cdk/a11y/activedescendant-key-manager.ts | 41 +++ src/cdk/a11y/fake-mousedown.ts | 18 + src/cdk/a11y/focus-key-manager.ts | 39 +++ src/{lib/core => cdk}/a11y/focus-trap.spec.ts | 0 src/cdk/a11y/focus-trap.ts | 309 ++++++++++++++++++ src/cdk/a11y/index.ts | 30 ++ .../a11y/interactivity-checker.spec.ts | 0 src/cdk/a11y/interactivity-checker.ts | 244 ++++++++++++++ .../a11y/list-key-manager.spec.ts | 0 src/cdk/a11y/list-key-manager.ts | 177 ++++++++++ .../core => cdk}/a11y/live-announcer.spec.ts | 0 src/cdk/a11y/live-announcer.ts | 95 ++++++ src/cdk/bidi/dir.ts | 66 ++++ .../core => cdk}/bidi/directionality.spec.ts | 0 src/cdk/bidi/directionality.ts | 65 ++++ src/cdk/bidi/index.ts | 31 ++ src/cdk/keyboard/keycodes.ts | 22 ++ src/cdk/platform/features.ts | 64 ++++ src/cdk/platform/index.ts | 20 ++ src/cdk/platform/platform.ts | 53 +++ src/cdk/portal/README.md | 72 ++++ src/cdk/portal/dom-portal-host.ts | 107 ++++++ src/cdk/portal/index.ts | 11 + src/cdk/portal/portal-directives.ts | 138 ++++++++ src/{lib/core => cdk}/portal/portal-errors.ts | 0 src/{lib/core => cdk}/portal/portal.spec.ts | 0 src/cdk/portal/portal.ts | 236 +++++++++++++ src/cdk/public_api.ts | 6 + src/cdk/rxjs/index.ts | 10 + src/cdk/rxjs/rx-chain.spec.ts | 41 +++ src/cdk/rxjs/rx-chain.ts | 55 ++++ src/cdk/rxjs/rx-operators.ts | 124 +++++++ src/cdk/testing/dispatch-events.ts | 30 ++ src/cdk/testing/event-objects.ts | 62 ++++ src/lib/core/a11y/README.md | 31 +- src/lib/core/a11y/fake-mousedown.ts | 11 +- src/lib/core/a11y/focus-trap.ts | 302 +---------------- src/lib/core/a11y/index.ts | 15 +- src/lib/core/a11y/interactivity-checker.ts | 237 +------------- src/lib/core/a11y/list-key-manager.ts | 168 +--------- src/lib/core/a11y/live-announcer.ts | 94 +----- src/lib/core/bidi/dir.ts | 58 +--- src/lib/core/bidi/directionality.ts | 64 +--- src/lib/core/bidi/index.ts | 20 +- src/lib/core/keyboard/keycodes.ts | 38 +-- src/lib/core/platform/features.ts | 57 +--- src/lib/core/platform/index.ts | 11 +- src/lib/core/platform/platform.ts | 46 +-- src/lib/core/portal/README.md | 73 +---- src/lib/core/portal/dom-portal-host.ts | 101 +----- src/lib/core/portal/portal-directives.ts | 131 +------- src/lib/core/portal/portal.ts | 227 +------------ src/lib/core/ripple/ripple.spec.ts | 15 +- src/lib/core/rxjs/rx-chain.ts | 48 +-- src/lib/core/rxjs/rx-operators.ts | 147 +++------ .../core/style/focus-origin-monitor.spec.ts | 13 +- src/lib/core/style/focus-origin-monitor.ts | 2 +- src/lib/tabs/tab-label.ts | 5 +- 59 files changed, 2312 insertions(+), 1798 deletions(-) create mode 100644 src/cdk/a11y/README.md create mode 100644 src/cdk/a11y/activedescendant-key-manager.ts create mode 100644 src/cdk/a11y/fake-mousedown.ts create mode 100644 src/cdk/a11y/focus-key-manager.ts rename src/{lib/core => cdk}/a11y/focus-trap.spec.ts (100%) create mode 100644 src/cdk/a11y/focus-trap.ts create mode 100644 src/cdk/a11y/index.ts rename src/{lib/core => cdk}/a11y/interactivity-checker.spec.ts (100%) create mode 100644 src/cdk/a11y/interactivity-checker.ts rename src/{lib/core => cdk}/a11y/list-key-manager.spec.ts (100%) create mode 100644 src/cdk/a11y/list-key-manager.ts rename src/{lib/core => cdk}/a11y/live-announcer.spec.ts (100%) create mode 100644 src/cdk/a11y/live-announcer.ts create mode 100644 src/cdk/bidi/dir.ts rename src/{lib/core => cdk}/bidi/directionality.spec.ts (100%) create mode 100644 src/cdk/bidi/directionality.ts create mode 100644 src/cdk/bidi/index.ts create mode 100644 src/cdk/keyboard/keycodes.ts create mode 100644 src/cdk/platform/features.ts create mode 100644 src/cdk/platform/index.ts create mode 100755 src/cdk/platform/platform.ts create mode 100644 src/cdk/portal/README.md create mode 100644 src/cdk/portal/dom-portal-host.ts create mode 100644 src/cdk/portal/index.ts create mode 100644 src/cdk/portal/portal-directives.ts rename src/{lib/core => cdk}/portal/portal-errors.ts (100%) rename src/{lib/core => cdk}/portal/portal.spec.ts (100%) create mode 100644 src/cdk/portal/portal.ts create mode 100644 src/cdk/rxjs/index.ts create mode 100644 src/cdk/rxjs/rx-chain.spec.ts create mode 100644 src/cdk/rxjs/rx-chain.ts create mode 100644 src/cdk/rxjs/rx-operators.ts create mode 100644 src/cdk/testing/dispatch-events.ts create mode 100644 src/cdk/testing/event-objects.ts diff --git a/src/cdk/a11y/README.md b/src/cdk/a11y/README.md new file mode 100644 index 000000000000..a98419c31666 --- /dev/null +++ b/src/cdk/a11y/README.md @@ -0,0 +1,30 @@ +# LiveAnnouncer +`LiveAnnouncer` is a service, which announces messages to several screenreaders. + +### Methods + +| Name | Description | +| --- | --- | +| `announce(message, politeness)` | This announces a text message to the supported screenreaders.

The politeness parameter sets the `aria-live` attribute on the announcer element | + +### Examples +The service can be injected in a component. +```ts +@Component({ + selector: 'my-component' + providers: [LiveAnnouncer] +}) +export class MyComponent { + + constructor(live: LiveAnnouncer) { + live.announce("Hey Google"); + } + +} +``` + +### Supported Screenreaders +- JAWS (Windows) +- NVDA (Windows) +- VoiceOver (OSX and iOS) +- TalkBack (Android) diff --git a/src/cdk/a11y/activedescendant-key-manager.ts b/src/cdk/a11y/activedescendant-key-manager.ts new file mode 100644 index 000000000000..c0df3831339a --- /dev/null +++ b/src/cdk/a11y/activedescendant-key-manager.ts @@ -0,0 +1,41 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {QueryList} from '@angular/core'; +import {ListKeyManager, CanDisable} from './list-key-manager'; + +/** + * This is the interface for highlightable items (used by the ActiveDescendantKeyManager). + * Each item must know how to style itself as active or inactive and whether or not it is + * currently disabled. + */ +export interface Highlightable extends CanDisable { + setActiveStyles(): void; + setInactiveStyles(): void; +} + +export class ActiveDescendantKeyManager extends ListKeyManager { + + /** + * This method sets the active item to the item at the specified index. + * It also adds active styles to the newly active item and removes active + * styles from the previously active item. + */ + setActiveItem(index: number): void { + Promise.resolve().then(() => { + if (this.activeItem) { + this.activeItem.setInactiveStyles(); + } + super.setActiveItem(index); + if (this.activeItem) { + this.activeItem.setActiveStyles(); + } + }); + } + +} diff --git a/src/cdk/a11y/fake-mousedown.ts b/src/cdk/a11y/fake-mousedown.ts new file mode 100644 index 000000000000..87f6eb92592e --- /dev/null +++ b/src/cdk/a11y/fake-mousedown.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +/** + * Screenreaders will often fire fake mousedown events when a focusable element + * is activated using the keyboard. We can typically distinguish between these faked + * mousedown events and real mousedown events using the "buttons" property. While + * real mousedowns will indicate the mouse button that was pressed (e.g. "1" for + * the left mouse button), faked mousedowns will usually set the property value to 0. + */ +export function isFakeMousedownFromScreenReader(event: MouseEvent): boolean { + return event.buttons === 0; +} diff --git a/src/cdk/a11y/focus-key-manager.ts b/src/cdk/a11y/focus-key-manager.ts new file mode 100644 index 000000000000..8473b84560ea --- /dev/null +++ b/src/cdk/a11y/focus-key-manager.ts @@ -0,0 +1,39 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {QueryList} from '@angular/core'; +import {ListKeyManager, CanDisable} from './list-key-manager'; + +/** + * This is the interface for focusable items (used by the FocusKeyManager). + * Each item must know how to focus itself and whether or not it is currently disabled. + */ +export interface Focusable extends CanDisable { + focus(): void; +} + + +export class FocusKeyManager extends ListKeyManager { + + constructor(items: QueryList) { + super(items); + } + + /** + * This method sets the active item to the item at the specified index. + * It also adds focuses the newly active item. + */ + setActiveItem(index: number): void { + super.setActiveItem(index); + + if (this.activeItem) { + this.activeItem.focus(); + } + } + +} diff --git a/src/lib/core/a11y/focus-trap.spec.ts b/src/cdk/a11y/focus-trap.spec.ts similarity index 100% rename from src/lib/core/a11y/focus-trap.spec.ts rename to src/cdk/a11y/focus-trap.spec.ts diff --git a/src/cdk/a11y/focus-trap.ts b/src/cdk/a11y/focus-trap.ts new file mode 100644 index 000000000000..60cb67ef3dfc --- /dev/null +++ b/src/cdk/a11y/focus-trap.ts @@ -0,0 +1,309 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { + Directive, + ElementRef, + Input, + NgZone, + OnDestroy, + AfterContentInit, + Injectable, +} from '@angular/core'; +import {coerceBooleanProperty} from '../coercion/boolean-property'; +import {InteractivityChecker} from './interactivity-checker'; +import {Platform} from '../platform/platform'; +import {first} from '../rxjs/index'; + + +/** + * Class that allows for trapping focus within a DOM element. + * + * NOTE: This class currently uses a very simple (naive) approach to focus trapping. + * It assumes that the tab order is the same as DOM order, which is not necessarily true. + * Things like tabIndex > 0, flex `order`, and shadow roots can cause to two to misalign. + * This will be replaced with a more intelligent solution before the library is considered stable. + */ +export class FocusTrap { + private _startAnchor: HTMLElement | null; + private _endAnchor: HTMLElement | null; + + /** Whether the focus trap is active. */ + get enabled(): boolean { return this._enabled; } + set enabled(val: boolean) { + this._enabled = val; + + if (this._startAnchor && this._endAnchor) { + this._startAnchor.tabIndex = this._endAnchor.tabIndex = this._enabled ? 0 : -1; + } + } + private _enabled: boolean = true; + + constructor( + private _element: HTMLElement, + private _platform: Platform, + private _checker: InteractivityChecker, + private _ngZone: NgZone, + deferAnchors = false) { + + if (!deferAnchors) { + this.attachAnchors(); + } + } + + /** Destroys the focus trap by cleaning up the anchors. */ + destroy() { + if (this._startAnchor && this._startAnchor.parentNode) { + this._startAnchor.parentNode.removeChild(this._startAnchor); + } + + if (this._endAnchor && this._endAnchor.parentNode) { + this._endAnchor.parentNode.removeChild(this._endAnchor); + } + + this._startAnchor = this._endAnchor = null; + } + + /** + * Inserts the anchors into the DOM. This is usually done automatically + * in the constructor, but can be deferred for cases like directives with `*ngIf`. + */ + attachAnchors(): void { + // If we're not on the browser, there can be no focus to trap. + if (!this._platform.isBrowser) { + return; + } + + if (!this._startAnchor) { + this._startAnchor = this._createAnchor(); + } + + if (!this._endAnchor) { + this._endAnchor = this._createAnchor(); + } + + this._ngZone.runOutsideAngular(() => { + this._startAnchor!.addEventListener('focus', () => this.focusLastTabbableElement()); + this._endAnchor!.addEventListener('focus', () => this.focusFirstTabbableElement()); + + if (this._element.parentNode) { + this._element.parentNode.insertBefore(this._startAnchor!, this._element); + this._element.parentNode.insertBefore(this._endAnchor!, this._element.nextSibling); + } + }); + } + + /** + * Waits for the zone to stabilize, then either focuses the first element that the + * user specified, or the first tabbable element.. + */ + focusInitialElementWhenReady() { + this._executeOnStable(() => this.focusInitialElement()); + } + + /** + * Waits for the zone to stabilize, then focuses + * the first tabbable element within the focus trap region. + */ + focusFirstTabbableElementWhenReady() { + this._executeOnStable(() => this.focusFirstTabbableElement()); + } + + /** + * Waits for the zone to stabilize, then focuses + * the last tabbable element within the focus trap region. + */ + focusLastTabbableElementWhenReady() { + this._executeOnStable(() => this.focusLastTabbableElement()); + } + + /** + * Get the specified boundary element of the trapped region. + * @param bound The boundary to get (start or end of trapped region). + * @returns The boundary element. + */ + private _getRegionBoundary(bound: 'start' | 'end'): HTMLElement | null { + // Contains the deprecated version of selector, for temporary backwards comparability. + let markers = this._element.querySelectorAll(`[cdk-focus-region-${bound}], ` + + `[cdk-focus-${bound}]`) as NodeListOf; + + for (let i = 0; i < markers.length; i++) { + if (markers[i].hasAttribute(`cdk-focus-${bound}`)) { + console.warn(`Found use of deprecated attribute 'cdk-focus-${bound}',` + + ` use 'cdk-focus-region-${bound}' instead.`, markers[i]); + } + } + + if (bound == 'start') { + return markers.length ? markers[0] : this._getFirstTabbableElement(this._element); + } + return markers.length ? + markers[markers.length - 1] : this._getLastTabbableElement(this._element); + } + + /** Focuses the element that should be focused when the focus trap is initialized. */ + focusInitialElement() { + let redirectToElement = this._element.querySelector('[cdk-focus-initial]') as HTMLElement; + if (redirectToElement) { + redirectToElement.focus(); + } else { + this.focusFirstTabbableElement(); + } + } + + /** Focuses the first tabbable element within the focus trap region. */ + focusFirstTabbableElement() { + let redirectToElement = this._getRegionBoundary('start'); + if (redirectToElement) { + redirectToElement.focus(); + } + } + + /** Focuses the last tabbable element within the focus trap region. */ + focusLastTabbableElement() { + let redirectToElement = this._getRegionBoundary('end'); + if (redirectToElement) { + redirectToElement.focus(); + } + } + + /** Get the first tabbable element from a DOM subtree (inclusive). */ + private _getFirstTabbableElement(root: HTMLElement): HTMLElement | null { + if (this._checker.isFocusable(root) && this._checker.isTabbable(root)) { + return root; + } + + // Iterate in DOM order. Note that IE doesn't have `children` for SVG so we fall + // back to `childNodes` which includes text nodes, comments etc. + let children = root.children || root.childNodes; + + for (let i = 0; i < children.length; i++) { + let tabbableChild = children[i].nodeType === Node.ELEMENT_NODE ? + this._getFirstTabbableElement(children[i] as HTMLElement) : + null; + + if (tabbableChild) { + return tabbableChild; + } + } + + return null; + } + + /** Get the last tabbable element from a DOM subtree (inclusive). */ + private _getLastTabbableElement(root: HTMLElement): HTMLElement | null { + if (this._checker.isFocusable(root) && this._checker.isTabbable(root)) { + return root; + } + + // Iterate in reverse DOM order. + let children = root.children || root.childNodes; + + for (let i = children.length - 1; i >= 0; i--) { + let tabbableChild = children[i].nodeType === Node.ELEMENT_NODE ? + this._getLastTabbableElement(children[i] as HTMLElement) : + null; + + if (tabbableChild) { + return tabbableChild; + } + } + + return null; + } + + /** Creates an anchor element. */ + private _createAnchor(): HTMLElement { + let anchor = document.createElement('div'); + anchor.tabIndex = this._enabled ? 0 : -1; + anchor.classList.add('cdk-visually-hidden'); + anchor.classList.add('cdk-focus-trap-anchor'); + return anchor; + } + + /** Executes a function when the zone is stable. */ + private _executeOnStable(fn: () => any): void { + if (this._ngZone.isStable) { + fn(); + } else { + first.call(this._ngZone.onStable).subscribe(fn); + } + } +} + + +/** Factory that allows easy instantiation of focus traps. */ +@Injectable() +export class FocusTrapFactory { + constructor( + private _checker: InteractivityChecker, + private _platform: Platform, + private _ngZone: NgZone) { } + + create(element: HTMLElement, deferAnchors = false): FocusTrap { + return new FocusTrap(element, this._platform, this._checker, this._ngZone, deferAnchors); + } +} + + +/** + * Directive for trapping focus within a region. + * @deprecated + */ +@Directive({ + selector: 'cdk-focus-trap', +}) +export class FocusTrapDeprecatedDirective implements OnDestroy, AfterContentInit { + focusTrap: FocusTrap; + + /** Whether the focus trap is active. */ + @Input() + get disabled(): boolean { return !this.focusTrap.enabled; } + set disabled(val: boolean) { + this.focusTrap.enabled = !coerceBooleanProperty(val); + } + + constructor(private _elementRef: ElementRef, private _focusTrapFactory: FocusTrapFactory) { + this.focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement, true); + } + + ngOnDestroy() { + this.focusTrap.destroy(); + } + + ngAfterContentInit() { + this.focusTrap.attachAnchors(); + } +} + + +/** Directive for trapping focus within a region. */ +@Directive({ + selector: '[cdkTrapFocus]', + exportAs: 'cdkTrapFocus', +}) +export class FocusTrapDirective implements OnDestroy, AfterContentInit { + focusTrap: FocusTrap; + + /** Whether the focus trap is active. */ + @Input('cdkTrapFocus') + get enabled(): boolean { return this.focusTrap.enabled; } + set enabled(value: boolean) { this.focusTrap.enabled = coerceBooleanProperty(value); } + + constructor(private _elementRef: ElementRef, private _focusTrapFactory: FocusTrapFactory) { + this.focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement, true); + } + + ngOnDestroy() { + this.focusTrap.destroy(); + } + + ngAfterContentInit() { + this.focusTrap.attachAnchors(); + } +} diff --git a/src/cdk/a11y/index.ts b/src/cdk/a11y/index.ts new file mode 100644 index 000000000000..c78f2a4b0104 --- /dev/null +++ b/src/cdk/a11y/index.ts @@ -0,0 +1,30 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {NgModule} from '@angular/core'; +import {FocusTrapDirective, FocusTrapDeprecatedDirective, FocusTrapFactory} from './focus-trap'; +import {LIVE_ANNOUNCER_PROVIDER} from './live-announcer'; +import {InteractivityChecker} from './interactivity-checker'; +import {CommonModule} from '@angular/common'; +import {PlatformModule} from '../platform/index'; + +@NgModule({ + imports: [CommonModule, PlatformModule], + declarations: [FocusTrapDirective, FocusTrapDeprecatedDirective], + exports: [FocusTrapDirective, FocusTrapDeprecatedDirective], + providers: [InteractivityChecker, FocusTrapFactory, LIVE_ANNOUNCER_PROVIDER] +}) +export class A11yModule {} + +export * from './live-announcer'; +export * from './fake-mousedown'; +export * from './focus-trap'; +export * from './interactivity-checker'; +export * from './list-key-manager'; +export * from './activedescendant-key-manager'; +export * from './focus-key-manager'; diff --git a/src/lib/core/a11y/interactivity-checker.spec.ts b/src/cdk/a11y/interactivity-checker.spec.ts similarity index 100% rename from src/lib/core/a11y/interactivity-checker.spec.ts rename to src/cdk/a11y/interactivity-checker.spec.ts diff --git a/src/cdk/a11y/interactivity-checker.ts b/src/cdk/a11y/interactivity-checker.ts new file mode 100644 index 000000000000..035e27001506 --- /dev/null +++ b/src/cdk/a11y/interactivity-checker.ts @@ -0,0 +1,244 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Injectable} from '@angular/core'; +import {Platform} from '../platform/platform'; + +/** + * The InteractivityChecker leans heavily on the ally.js accessibility utilities. + * Methods like `isTabbable` are only covering specific edge-cases for the browsers which are + * supported. + */ + +/** + * Utility for checking the interactivity of an element, such as whether is is focusable or + * tabbable. + */ +@Injectable() +export class InteractivityChecker { + + constructor(private _platform: Platform) {} + + /** + * Gets whether an element is disabled. + * + * @param element Element to be checked. + * @returns Whether the element is disabled. + */ + isDisabled(element: HTMLElement): boolean { + // This does not capture some cases, such as a non-form control with a disabled attribute or + // a form control inside of a disabled form, but should capture the most common cases. + return element.hasAttribute('disabled'); + } + + /** + * Gets whether an element is visible for the purposes of interactivity. + * + * This will capture states like `display: none` and `visibility: hidden`, but not things like + * being clipped by an `overflow: hidden` parent or being outside the viewport. + * + * @returns Whether the element is visible. + */ + isVisible(element: HTMLElement): boolean { + return hasGeometry(element) && getComputedStyle(element).visibility === 'visible'; + } + + /** + * Gets whether an element can be reached via Tab key. + * Assumes that the element has already been checked with isFocusable. + * + * @param element Element to be checked. + * @returns Whether the element is tabbable. + */ + isTabbable(element: HTMLElement): boolean { + // Nothing is tabbable on the the server 😎 + if (!this._platform.isBrowser) { + return false; + } + + let frameElement = getWindow(element).frameElement as HTMLElement; + + if (frameElement) { + + let frameType = frameElement && frameElement.nodeName.toLowerCase(); + + // Frame elements inherit their tabindex onto all child elements. + if (getTabIndexValue(frameElement) === -1) { + return false; + } + + // Webkit and Blink consider anything inside of an element as non-tabbable. + if ((this._platform.BLINK || this._platform.WEBKIT) && frameType === 'object') { + return false; + } + + // Webkit and Blink disable tabbing to an element inside of an invisible frame. + if ((this._platform.BLINK || this._platform.WEBKIT) && !this.isVisible(frameElement)) { + return false; + } + + } + + let nodeName = element.nodeName.toLowerCase(); + let tabIndexValue = getTabIndexValue(element); + + if (element.hasAttribute('contenteditable')) { + return tabIndexValue !== -1; + } + + if (nodeName === 'iframe') { + // The frames may be tabbable depending on content, but it's not possibly to reliably + // investigate the content of the frames. + return false; + } + + if (nodeName === 'audio') { + if (!element.hasAttribute('controls')) { + // By default an