## Themes

A **theme** is a collection of color, typography, and density options. Each theme includes three
palettes that determine component colors:

* A **primary** palette for the color that appears most frequently throughout your application
* An **accent**, or _secondary_, palette used to selectively highlight key parts of your UI
* A **warn**, or _error_, palette used for warnings and error states

You can include the CSS styles for a theme in your application in one of two ways: by defining a
custom theme with Sass, or by importing a pre-built theme CSS file.

### Custom themes with Sass

A **theme file** is a Sass file that calls Angular Material Sass mixins to output color,
typography, and density CSS styles.

#### Defining a theme

Angular Material represents a theme as a Sass map that contains your color, typography, and density
choices, as well as some base design system settings. See
[Angular Material Typography][mat-typography] for an in-depth guide to customizing typography. See
[Customizing density](#customizing-density) below for details on adjusting component density.

Constructing the theme first requires defining your primary and accent palettes, with an optional
warn palette. The `m2-define-palette` Sass function accepts a color palette, described in the
[Palettes](#palettes) section above, as well as four optional hue numbers. These four hues
represent, in order: the "default" hue, a "lighter" hue, a "darker" hue, and a "text" hue.
Components use these hues to choose the most appropriate color for different parts of
themselves.

```scss
@use '@angular/material' as mat;

$my-primary: mat.m2-define-palette(mat.$m2-indigo-palette, 500);
$my-accent: mat.m2-define-palette(mat.$m2-pink-palette, A200, A100, A400);

// The "warn" palette is optional and defaults to red if not specified.
$my-warn: mat.m2-define-palette(mat.$m2-red-palette);
```

You can construct a theme by calling either `m2-define-light-theme` or `m2-define-dark-theme` with
the result from `m2-define-palette`. The choice of a light versus a dark theme determines the
background and foreground colors used throughout the components.

```scss
@use '@angular/material' as mat;

$my-primary: mat.m2-define-palette(mat.$m2-indigo-palette, 500);
$my-accent: mat.m2-define-palette(mat.$m2-pink-palette, A200, A100, A400);

// The "warn" palette is optional and defaults to red if not specified.
$my-warn: mat.m2-define-palette(mat.$m2-red-palette);

$my-theme: mat.m2-define-light-theme((
 color: (
   primary: $my-primary,
   accent: $my-accent,
   warn: $my-warn,
 ),
 typography: mat.m2-define-typography-config(),
 density: 0,
));
```

#### Applying a theme to components

The `core-theme` Sass mixin emits prerequisite styles for common features used by multiple
components, such as ripples. This mixin must be included once per theme.

Each Angular Material component has a mixin for each [theming dimension](#theming-dimensions): base,
color, typography, and density. For example, `MatButton` declares `button-base`, `button-color`,
`button-typography`, and `button-density`. Each mixin emits only the styles corresponding to that
dimension of customization.

Additionally, each component has a "theme" mixin that emits all styles that depend on the theme
config. This theme mixin only emits color, typography, or density styles if you provided a
corresponding configuration to `m2-define-light-theme` or `m2-define-dark-theme`, and it always emits the
base styles.

Apply the styles for each of the components used in your application by including each of their
theme Sass mixins.

```scss
@use '@angular/material' as mat;

$my-primary: mat.m2-define-palette(mat.$m2-indigo-palette, 500);
$my-accent: mat.m2-define-palette(mat.$m2-pink-palette, A200, A100, A400);

$my-theme: mat.m2-define-light-theme((
 color: (
   primary: $my-primary,
   accent: $my-accent,
 ),
 typography: mat.m2-define-typography-config(),
 density: 0,
));

// Emit theme-dependent styles for common features used across multiple components.
@include mat.core-theme($my-theme);

// Emit styles for MatButton based on `$my-theme`. Because the configuration
// passed to `m2-define-light-theme` omits typography, `button-theme` will not
// emit any typography styles.
@include mat.button-theme($my-theme);

// Include the theme mixins for other components you use here.
```

As an alternative to listing every component that your application uses, Angular Material offers
Sass mixins that includes styles for all components in the library: `all-component-bases`,
`all-component-colors`, `all-component-typographies`, `all-component-densities`, and
`all-component-themes`. These mixins behave the same as individual component mixins, except they
emit styles for `core-theme` and _all_ 35+ components in Angular Material. Unless your application
uses every single component, this will produce unnecessary CSS.

```scss
@use '@angular/material' as mat;

$my-primary: mat.m2-define-palette(mat.$m2-indigo-palette, 500);
$my-accent: mat.m2-define-palette(mat.$m2-pink-palette, A200, A100, A400);

$my-theme: mat.m2-define-light-theme((
 color: (
   primary: $my-primary,
   accent: $my-accent,
 ),
 typography: mat.m2-define-typography-config(),
 density: 0,
));

@include mat.all-component-themes($my-theme);
```

To include the emitted styles in your application, [add your theme file to the `styles` array of
your project's `angular.json` file][adding-styles].

[adding-styles]: https://angular.dev/reference/configs/workspace-config#styles-and-scripts-configuration

#### Theming dimensions

Angular Material themes are divided along four dimensions: base, color, typography, and density.

##### Base

Common base styles for the design system. These styles don't change based on your configured
colors, typography, or density, so they only need to be included once per application. These
mixins include structural styles such as border-radius, border-width, etc. All components have a base
mixin that can be used to include its base styles. (For example,
`@include mat.checkbox-base($theme)`)

##### Color

Styles related to the colors in your application. These style should be included at least once in
your application. Depending on your needs, you may need to include these styles multiple times
with different configurations. (For example, if your app supports light and dark theme colors.)
All components have a color mixin that can be used to include its color styles. (For example,
`@include mat.checkbox-color($theme)`)

##### Typography

Styles related to the fonts used in your application, including the font family, size, weight,
line-height, and letter-spacing. These style should be included at least once in your application.
Depending on your needs, you may need to include these styles multiple times with different
configurations. (For example, if your app supports reading content in either a serif or sans-serif
font.) All components  have a typography mixin that can be used to include its typography
styles. (For example, `@include mat.checkbox-typography($theme)`)

##### Density

Styles related to the size and spacing of elements in your application. These style should be
included at least once in your application. Depending on your needs, you may need to include these
styles multiple times with different configurations. (For example, if your app supports both a
normal and compact mode). All components have a density mixin that can be used to include its
density styles. (For example, `@include mat.checkbox-density($theme)`)

##### Theme mixin

All components also support a theme mixin that can be used to include the component's styles for all
theme dimensions at once. (For example, `@include mat.checkbox-theme($theme)`).

The recommended approach is to rely on the `theme` mixins to lay down your base styles, and if
needed use the single dimension mixins to override particular aspects for parts of your app (see the
section on [Multiple themes in one file](#multiple-themes-in-one-file).)


## Theming your components

### Reading values from a theme

As described in this guide, a theme is a Sass map that contains style values to
customize components. Angular Material provides APIs for reading values from this data structure.

#### Reading color values

To read color values from a theme, you can use the `get-theme-color` Sass function. This function
supports reading colors for both the app color palettes (primary, accent, and warn), as well as the
foreground and background palettes. `get-theme-color` takes three arguments: The theme to read from,
the name of the palette, and the name of the color.

Each of the color palettes (primary, accent, and warn) supports reading the following named colors:

| Color Name       | Description                                                                                |
|------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| default          | The default color from this palette                                                                                |
| lighter          | A lighter version of the color for this palette                                                                                |
| darker           | A darker version of the color for this palette                                                                                |
| text             | The text color for this palette                                                                                |
| default-contrast | A color that stands out against the this palette's default color                                                                                |
| lighter-contrast | A color that stands out against the this palette's lighter color                                                                                |
| darker-contrast  | A color that stands out against the this palette's darker color                                                                                |
| [hue]            | The [hue] color for the palette.
[hue] can be one of: 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, A100, A200, A400, A700                                 |
| [hue]-contrast   | A color that stands out against the [hue] color for the palette.
[hue] can be one of: 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, A100, A200, A400, A700 |

The background palette supports reading the following named colors:

| Color Name               | Description                                        |
|--------------------------|----------------------------------------------------|
| status-bar               | The background color for a status bar              |
| app-bar                  | The background color for an app bar                |
| background               | The background color for the app                   |
| hover                    | The background color of a hover overlay            |
| card                     | The background color of a card                     |
| dialog                   | The background color of a dialog                   |
| raised-button            | The background color of a raised button            |
| selected-button          | The background color of a selected button          |
| selected-disabled-button | The background color of a selected disabled button |
| disabled-button          | The background color of a disabled button          |
| focused-button           | The background color of a focused button           |
| disabled-button-toggle   | The background color of a disabled button toggle   |
| unselected-chip          | The background color of an unselected chip         |
| disabled-list-option     | The background color of a disabled list option     |
| tooltip                  | The background color of a tooltip                  |

The foreground palette supports reading the following named colors:

| Color name        | Description                                                                                |
|-------------------|----------------------------------------------------------------------------------------------------------|
| base              | The base foreground color, can be used to for color mixing or creating a custom opacity foreground color |
| divider           | The color of a divider                                                                                |
| dividers          | (Alternate name for divider)                                                                             |
| disabled-text     | The color for disabled text                                                                              |
| disabled          | (Alternate name for disabled-text)                                                                       |
| disabled-button   | The color for disabled button text                                                                       |
| elevation         | The color elevation shadows                                                                              |
| hint-text         | The color for hint text                                                                                |
| secondary-text    | The color for secondary text                                                                             |
| icon              | The color for icons                                                                                |
| icons             | (Alternate name for icon)                                                                                |
| text              | The color for text                                                                                |                                                                         |

In addition to reading particular colors, you can use the `get-theme-type` Sass function to
determine the type of theme (either light or dark). This function takes a single argument, the
theme, and returns either `light` or `dark`.

See the below example of reading some colors from a theme:

```scss
$theme: mat.m2-define-dark-theme(...);

$primary-default: mat.get-theme-color($theme, primary, default);
$accent-a100: mat.get-theme-color($theme, accent, A100);
$warn-500-contrast: mat.get-theme-color($theme, warn, 500-contrast);
$foreground-text: mat.get-theme-color($theme, foreground, text);
$background-card: mat.get-theme-color($theme, background, card);
$type: mat.get-theme-type($theme);
$custom-background: if($type == dark, #030, #dfd);
```

#### Reading typography values

To read typography values from a theme, you can use the `get-theme-typography` Sass function. This
function supports reading typography properties from the typography levels defined in the theme.
There are two ways to call the function.

The first way to call it is by passing just the theme and the typography level to get a shorthand
`font` property based on the settings for that level. (Note: `letter-spacing` cannot be expressed in
the `font` shorthand, so it must be applied separately).

The second way to call it is by passing the theme, typography level, and the specific font property
you want: `font-family`, `font-size`, `font-weight`, `line-height`, or `letter-spacing`.

The available typography levels are:

| Name       | Description                                                          |
|------------|----------------------------------------------------------------------|
| headline-1 | One-off header, usually at the top of the page (e.g. a hero header). |
| headline-2 | One-off header, usually at the top of the page (e.g. a hero header). |
| headline-3 | One-off header, usually at the top of the page (e.g. a hero header). |
| headline-4 | One-off header, usually at the top of the page (e.g. a hero header). |
| headline-5 | Section heading corresponding to the `<h1>` tag.                     |
| headline-6 | Section heading corresponding to the `<h2>` tag.                     |
| subtitle-1 | Section heading corresponding to the `<h3>` tag.                     |
| subtitle-2 | Section heading corresponding to the `<h4>` tag.                     |
| body-1     | Base body text.                                                      |
| body-2     | Secondary body text.                                                 |
| caption    | Smaller body and hint text.                                          |
| button     | Buttons and anchors.                                                 |

See the below example of reading some typography settings from a theme:

```scss
$theme: mat.m2-define-dark-theme(...);

body {
  font: mat.get-theme-typography($theme, body-1);
  letter-spacing: mat.get-theme-typography($theme, body-1, letter-spacing);
}
```

#### Reading density values

To read the density scale from a theme, you can use the `get-theme-density` Sass function. This
function takes a theme and returns the density scale (0, -1, -2, -3, -4, or -5).

See the below example of reading the density scale from a theme:

```scss
$theme: mat.m2-define-dark-theme(...);

$density-scale: mat.get-theme-density($theme);
```

### Checking what dimensions are configured for a theme

Depending on how a theme was created, it may not have configuration data for all theming dimensions
(base, color, typography, density). You can check if a theme has a configuration for a particular
dimension by calling the `theme-has` Sass function, passing the theme and the dimension to check.

See the below example of checking the configured dimensions for a theme:

```scss
$theme: mat.m2-define-dark-theme(...);

$has-base: mat.theme-has($theme, base);
$has-color: mat.theme-has($theme, color);
$has-typography: mat.theme-has($theme, typography);
$has-density: mat.theme-has($theme, density);
```

### Separating theme styles

Angular Material components each have a Sass file that defines mixins for customizing
that component's color and typography. For example, `MatButton` has mixins for `button-color` and
`button-typography`. Each mixin emits all color and typography styles for that component,
respectively.

You can mirror this structure in your components by defining your own mixins. These mixins
should accept an Angular Material theme, from which they can read color and typography values. You
can then include these mixins in your application along with Angular Material's own mixins.


## How to migrate an app from Material 2 to Material 3

Angular Material does not offer an automated migration from M2 to M3 because the design of your app
is subjective. Material Design offers general principles and constraints to guide you, but
ultimately it is up to you to decide how to apply those in your app. That said, Angular Material's M3 themes were designed with maximum compatibility in mind.

### Update your components that use Angular Material themes to be compatible with Material 3

In order to facilitate a smooth transition from M2 to M3, it may make sense to have your components
support both M2 and M3 themes. Once the entire app is migrated, the support for M2 themes can be
removed.

The simplest way to accomplish this is by checking the theme version and emitting different styles
for M2 vs M3. You can check the theme version using the `get-theme-version` function from
`@angular/material`. The function will return `0` for an M2 theme or `1` for an M3 theme (see
[theme your own components using a Material 3 theme](https://material.angular.io/guide/theming#theme-your-own-components-using-a-material-3-theme)
for how to read values from an M3 theme).

```scss
@use '@angular/material' as mat;

@mixin my-comp-theme($theme) {
  @if (mat.get-theme-version($theme) == 1) {
    // Add your new M3 styles here.
  } @else {
    // Keep your old M2 styles here.
  }
}
```

### Pass a new M3 theme in your global theme styles

Create a new M3 theme object using `define-theme` and pass it everywhere you were previously passing
your M2 theme. All Angular Material mixins that take an M2 theme are compatible with M3 themes as
well.

### Update usages of APIs that are not supported for Material 3 themes

Because Material 3 is implemented as an alternate theme for the same components used for Material 2,
the APIs for both are largely the same. However, there are a few differences to be aware of:

- M3 expects that any `@include` of the `-theme`, `-color`, `-typography`, `-density`, or `-base`
  mixins should be wrapped in a selector. If your app includes such an `@include` at the root level,
  we recommend wrapping it in `html { ... }`
- M3 has a different API for setting the color variant of a component (see
  [using component color variants](https://material.angular.io/guide/theming#using-component-color-variants)
  for more).
- The `backgroundColor` property of `<mat-tab-group>` is not supported, and should not be used with
  M3 themes.
- The `appearance="legacy"` variant of `<mat-button-toggle>` is not supported, and should not be
  used with M3 themes.
- For M3 themes, calling `all-component-typographies` does _not_ emit the `typography-hierarchy`
  styles, as this would violate M3's guarantee to not add selectors. Instead, the
  `typography-hierarchy` mixin must be explicitly called if you want these styles in your app.
- The `typography-hierarchy` mixin outputs CSS class names that correspond to the names of M3
  typescale levels rather than M2 typescale levels. If you were relying on the M2 classes to style
  your app, you may need to update the class names.

### (Optional) add backwards compatibility styles for color variants

We recommend _not_ relying on the `color="primary"`, `color="accent"`, or `color="warn"` options
that are offered by a number of Angular Material components for M2 themes. However, if you want to
quickly update to M3 and are willing to accept the extra CSS generated for these variants, you can
enable backwards compatibility styles that restore the behavior of this API. Call the
`color-variants-backwards-compatibility` mixin from `@angular/material` with the M3 theme you want
to generate color variant styles for.

<!-- TODO(mmalerba): Upgrade to embedded example -->

```scss
@use '@angular/material' as mat;

$theme: mat.define-theme();

html {
  @include mat.all-component-themes($theme);
  @include mat.color-variants-backwards-compatibility($theme);
}
```

### (Optional) add backwards compatibility styles for typography hierarchy

Calling the `typography-hierarchy` mixin with an M3 theme generates styles for CSS classes that
match the names of M3 typescale names (e.g. `.mat-headline-large`) rather than ones that match M2
typescale names (`.mat-headline-1`). If you were using the M2 class names in your app we recommend
updating all usages to one of the new class names. However, to make migration easier, the
`typography-hierarchy` mixin does support emitting the old class names in addition to the new ones.
We have made a best effort to map the M2 classes to reasonable equivalents in M3. To enable these
styles, pass an additional argument `$back-compat: true` to the mixin.

<!-- TODO(mmalerba): Upgrade to embedded example -->

```scss
@use '@angular/material' as mat;

$theme: mat.define-theme();

@include mat.typography-hierarchy($theme, $back-compat: true);
```


### Implementing the methods and properties of MatFormFieldControl

#### `value`

This property allows someone to set or get the value of our control. Its type should be the same
type we used for the type parameter when we implemented `MatFormFieldControl`. Since our component
already has a value property, we don't need to do anything for this one.

#### `stateChanges`

Because the `<mat-form-field>` uses the `OnPush` change detection strategy, we need to let it know
when something happens in the form field control that may require the form field to run change
detection. We do this via the `stateChanges` property. So far the only thing the form field needs to
know about is when the value changes. We'll need to emit on the stateChanges stream when that
happens, and as we continue flushing out these properties we'll likely find more places we need to
emit. We should also make sure to complete `stateChanges` when our component is destroyed.

```ts
stateChanges = new Subject<void>();

set value(tel: MyTel | null) {
  ...
  this.stateChanges.next();
}

ngOnDestroy() {
  this.stateChanges.complete();
}
```

#### `id`

This property should return the ID of an element in the component's template that we want the
`<mat-form-field>` to associate all of its labels and hints with. In this case, we'll use the host
element and just generate a unique ID for it.

```ts
static nextId = 0;

@HostBinding() id = `example-tel-input-${MyTelInput.nextId++}`;
```

#### `placeholder`

This property allows us to tell the `<mat-form-field>` what to use as a placeholder. In this
example, we'll do the same thing as `matInput` and `<mat-select>` and allow the user to specify it
via an `@Input()`. Since the value of the placeholder may change over time, we need to make sure to
trigger change detection in the parent form field by emitting on the `stateChanges` stream when the
placeholder changes.

```ts
@Input()
get placeholder() {
  return this._placeholder;
}
set placeholder(plh) {
  this._placeholder = plh;
  this.stateChanges.next();
}
private _placeholder: string;
```

#### `ngControl`

This property allows the form field control to specify the `@angular/forms` control that is bound
to this component. Since we haven't set up our component to act as a `ControlValueAccessor`, we'll
just set this to `null` in our component.

```ts
ngControl: NgControl = null;
```

It is likely you will want to implement `ControlValueAccessor` so that your component can work with
`formControl` and `ngModel`. If you do implement `ControlValueAccessor` you will need to get a
reference to the `NgControl` associated with your control and make it publicly available.

The easy way is to add it as a public property to your constructor and let dependency injection
handle it:

```ts
constructor(
  ...,
  @Optional() @Self() public ngControl: NgControl,
  ...,
) { }
```

Note that if your component implements `ControlValueAccessor`, it may already be set up to provide
`NG_VALUE_ACCESSOR` (in the `providers` part of the component's decorator, or possibly in a module
declaration). If so, you may get a *cannot instantiate cyclic dependency* error.

To resolve this, remove the `NG_VALUE_ACCESSOR` provider and instead set the value accessor directly:

```ts
@Component({
  ...,
  providers: [
    ...,
    // Remove this.
    // {
    //   provide: NG_VALUE_ACCESSOR,
    //   useExisting: forwardRef(() => MatFormFieldControl),
    //   multi: true,
    // },
  ],
})
export class MyTelInput implements MatFormFieldControl<MyTel>, ControlValueAccessor {
  constructor(
    ...,
    @Optional() @Self() public ngControl: NgControl,
    ...,
  ) {

    // Replace the provider from above with this.
    if (this.ngControl != null) {
      // Setting the value accessor directly (instead of using
      // the providers) to avoid running into a circular import.
      this.ngControl.valueAccessor = this;
    }
  }
}
```

For additional information about `ControlValueAccessor` see the [API docs](https://angular.dev/api/forms/ControlValueAccessor).

#### `focused`

This property indicates whether the form field control should be considered to be in a
focused state. When it is in a focused state, the form field is displayed with a solid color
underline. For the purposes of our component, we want to consider it focused if any of the part
inputs are focused. We can use the `focusin` and `focusout` events to easily check this. We also
need to remember to emit on the `stateChanges` when the focused stated changes stream so change
detection can happen.

In addition to updating the focused state, we use the `focusin` and `focusout` methods to update the
internal touched state of our component, which we'll use to determine the error state.

```ts
focused = false;

onFocusIn(event: FocusEvent) {
  if (!this.focused) {
    this.focused = true;
    this.stateChanges.next();
  }
}

onFocusOut(event: FocusEvent) {
  if (!this._elementRef.nativeElement.contains(event.relatedTarget as Element)) {
    this.touched = true;
    this.focused = false;
    this.onTouched();
    this.stateChanges.next();
  }
}
```

#### `empty`

This property indicates whether the form field control is empty. For our control, we'll consider it
empty if all the parts are empty.

```ts
get empty() {
  let n = this.parts.value;
  return !n.area && !n.exchange && !n.subscriber;
}
```

#### `shouldLabelFloat`

This property is used to indicate whether the label should be in the floating position. We'll
use the same logic as `matInput` and float the placeholder when the input is focused or non-empty.
Since the placeholder will be overlapping our control when it's not floating, we should hide
the `–` characters when it's not floating.

```ts
@HostBinding('class.floating')
get shouldLabelFloat() {
  return this.focused || !this.empty;
}
```
```css
span {
  opacity: 0;
  transition: opacity 200ms;
}
:host.floating span {
  opacity: 1;
}
```

#### `required`

This property is used to indicate whether the input is required. `<mat-form-field>` uses this
information to add a required indicator to the placeholder. Again, we'll want to make sure we run
change detection if the required state changes.

```ts
@Input()
get required() {
  return this._required;
}
set required(req: BooleanInput) {
  this._required = coerceBooleanProperty(req);
  this.stateChanges.next();
}
private _required = false;
```

#### `disabled`

This property tells the form field when it should be in the disabled state. In addition to reporting
the right state to the form field, we need to set the disabled state on the individual inputs that
make up our component.

```ts
@Input()
get disabled(): boolean { return this._disabled; }
set disabled(value: BooleanInput) {
  this._disabled = coerceBooleanProperty(value);
  this._disabled ? this.parts.disable() : this.parts.enable();
  this.stateChanges.next();
}
private _disabled = false;
```

#### `errorState`

This property indicates whether the associated `NgControl` is in an error state. For example,
we can show an error if the input is invalid and our component has been touched.

```ts
get errorState(): boolean {
  return this.parts.invalid && this.touched;
}
```

However, there are some error triggers that we can't subscribe to (e.g. parent form submissions),
to handle such cases we should re-evaluate `errorState` on every change detection cycle.

```ts
/** Whether the component is in an error state. */
errorState: boolean = false;

constructor(
  ...,
  @Optional() private _parentForm: NgForm,
  @Optional() private _parentFormGroup: FormGroupDirective
) {
...
}

ngDoCheck() {
  if (this.ngControl) {
    this.updateErrorState();
  }
}

private updateErrorState() {
  const parent = this._parentFormGroup || this.parentForm;

  const oldState = this.errorState;
  const newState = (this.ngControl?.invalid || this.parts.invalid) && (this.touched || parent.submitted);

  if (oldState !== newState) {
    this.errorState = newState;
    this.stateChanges.next();
  }
}
```

Keep in mind that `updateErrorState()` must have minimal logic to avoid performance issues.

##


/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import {inject, Injectable, PLATFORM_ID} from '@angular/core';
import {isPlatformBrowser} from '@angular/common';

// Whether the current platform supports the V8 Break Iterator. The V8 check
// is necessary to detect all Blink based browsers.
let hasV8BreakIterator: boolean;

// We need a try/catch around the reference to `Intl`, because accessing it in some cases can
// cause IE to throw. These cases are tied to particular versions of Windows and can happen if
// the consumer is providing a polyfilled `Map`. See:
// https://github.com/Microsoft/ChakraCore/issues/3189
// https://github.com/angular/components/issues/15687
try {
  hasV8BreakIterator = typeof Intl !== 'undefined' && (Intl as any).v8BreakIterator;
} catch {
  hasV8BreakIterator = false;
}

/**
 * Service to detect the current platform by comparing the userAgent strings and
 * checking browser-specific global properties.
 */
@Injectable({providedIn: 'root'})
export class Platform {
  private _platformId = inject(PLATFORM_ID);

  // We want to use the Angular platform check because if the Document is shimmed
  // without the navigator, the following checks will fail. This is preferred because
  // sometimes the Document may be shimmed without the user's knowledge or intention
  /** Whether the Angular application is being rendered in the browser. */
  isBrowser: boolean = this._platformId
    ? isPlatformBrowser(this._platformId)
    : typeof document === 'object' && !!document;

  /** Whether the current browser is Microsoft Edge. */
  EDGE: boolean = this.isBrowser && /(edge)/i.test(navigator.userAgent);

  /** Whether the current rendering engine is Microsoft Trident. */
  TRIDENT: boolean = this.isBrowser && /(msie|trident)/i.test(navigator.userAgent);

  // EdgeHTML and Trident mock Blink specific things and need to be excluded from this check.
  /** Whether the current rendering engine is Blink. */
  BLINK: boolean =
    this.isBrowser &&
    !!((window as any).chrome || hasV8BreakIterator) &&
    typeof CSS !== 'undefined' &&
    !this.EDGE &&
    !this.TRIDENT;

  // Webkit is part of the userAgent in EdgeHTML, Blink and Trident. Therefore we need to
  // ensure that Webkit runs standalone and is not used as another engine's base.
  /** Whether the current rendering engine is WebKit. */
  WEBKIT: boolean =
    this.isBrowser &&
    /AppleWebKit/i.test(navigator.userAgent) &&
    !this.BLINK &&
    !this.EDGE &&
    !this.TRIDENT;

  /** Whether the current platform is Apple iOS. */
  IOS: boolean =
    this.isBrowser && /iPad|iPhone|iPod/.test(navigator.userAgent) && !('MSStream' in window);

  // It's difficult to detect the plain Gecko engine, because most of the browsers identify
  // them self as Gecko-like browsers and modify the userAgent's according to that.
  // Since we only cover one explicit Firefox case, we can simply check for Firefox
  // instead of having an unstable check for Gecko.
  /** Whether the current browser is Firefox. */
  FIREFOX: boolean = this.isBrowser && /(firefox|minefield)/i.test(navigator.userAgent);

  /** Whether the current platform is Android. */
  // Trident on mobile adds the android platform to the userAgent to trick detections.
  ANDROID: boolean = this.isBrowser && /android/i.test(navigator.userAgent) && !this.TRIDENT;

  // Safari browsers will include the Safari keyword in their userAgent. Some browsers may fake
  // this and just place the Safari keyword in the userAgent. To be more safe about Safari every
  // Safari browser should also use Webkit as its layout engine.
  /** Whether the current browser is Safari. */
  SAFARI: boolean = this.isBrowser && /safari/i.test(navigator.userAgent) && this.WEBKIT;

  /** Backwards-compatible constructor. */
  constructor(..._args: unknown[]);

  constructor() {}
}


The `@angular/cdk/menu` module provides directives to help create custom menu
interactions based on the [WAI ARIA specification][aria].

By using `@angular/cdk/menu` you get all of the expected behaviors for an accessible
experience, including bidi layout support, keyboard interaction, and focus management. All
directives apply their associated ARIA roles to their host element.

### Supported ARIA Roles

The directives in `@angular/cdk/menu` set the appropriate roles on their host element.

| Directive           | ARIA Role        |
|---------------------|------------------|
| CdkMenuBar          | menubar          |
| CdkMenu             | menu             |
| CdkMenuGroup        | group            |
| CdkMenuItem         | menuitem         |
| CdkMenuItemRadio    | menuitemradio    |
| CdkMenuItemCheckbox | menuitemcheckbox |
| CdkMenuTrigger      | button           |

### CSS Styles and Classes

The `@angular/cdk/menu` is designed to be highly customizable to your needs. It therefore does not
make any assumptions about how elements should be styled. You are expected to apply any required
CSS styles, but the directives do apply CSS classes to make it easier for you to add custom styles.
The available CSS classes are listed below, by directive.

| Directive             | CSS Class                | Applied...                                     |
|:----------------------|--------------------------|------------------------------------------------|
| `cdkMenu`             | `cdk-menu`               | Always                                         |
| `cdkMenu`             | `cdk-menu-inline`        | If the menu is an [inline menu](#menu-content) |
| `cdkMenuBar`          | `cdk-menu-bar`           | Always                                         |
| `cdkMenuGroup`        | `cdk-menu-group`         | Always                                         |
| `cdkMenuItem`         | `cdk-menu-item`          | Always                                         |
| `cdkMenuItemCheckbox` | `cdk-menu-item`          | Always                                         |
| `cdkMenuItemCheckbox` | `cdk-menu-item-checkbox` | Always                                         |
| `cdkMenuItemRadio`    | `cdk-menu-item`          | Always                                         |
| `cdkMenuItemRadio`    | `cdk-menu-item-radio`    | Always                                         |
| `cdkMenuTriggerFor`   | `cdk-menu-trigger`       | Always                                         |

### Getting started

Import the `CdkMenuModule` into the `NgModule` in which you want to create menus. You can then apply
menu directives to build your custom menu. A typical menu consists of the following directives:

- `cdkMenuTriggerFor` - links a trigger element to an `ng-template` containing the menu to be opened
- `cdkMenu` - creates the menu content opened by the trigger
- `cdkMenuItem` - added to each item in the menu

<!-- example({
  "example": "cdk-menu-standalone-menu",
  "file": "cdk-menu-standalone-menu-example.html"
  }) -->

Most menu interactions consist of two parts: a trigger and a menu panel.

#### Triggers

You can add `cdkMenuTriggerFor` to any button to make it a trigger for the given menu, or any menu
item to make it a trigger for a submenu. When adding this directive, be sure to pass a reference to
the template containing the menu it should open. Users can toggle the associated menu using a mouse
or keyboard.

<!-- example({"example":"cdk-menu-standalone-menu",
              "file":"cdk-menu-standalone-menu-example.html",
              "region":"trigger"}) -->

When creating a submenu trigger, add both `cdkMenuItem` and `cdkMenuTriggerFor` like so,

<!-- example({"example":"cdk-menu-menubar",
              "file":"cdk-menu-menubar-example.html",
              "region":"file-trigger"}) -->

#### Menu content

There are two types of menus:
* _inline menus_ are always present on the page
* _pop-up menus_ can be toggled to hide or show by the user

You can create menus by marking their content element with the `cdkMenu` or `cdkMenuBar`
directives. You can create several types of menu interaction which are discussed below.

All type of menus should exclusively contain elements with role `menuitem`, `menuitemcheckbox`,
`menuitemradio`, or `group`. Supporting directives that automatically apply these roles are
discussed below.

Note that Angular CDK provides no styles; you must add styles as part of building your custom menu.

### Inline Menus

An _inline menu_ is a menu that lives directly on the page rather than in a pop-up associated with a
trigger. You can use an inline menu when you want a persistent menu interaction on a page. Menu
items within an inline menus are logically grouped together, and you can navigate through them
using your keyboard. You can create an inline menu by adding the `cdkMenu` directive to the element
you want to serve as the menu content.

<!-- example({
  "example": "cdk-menu-inline",
  "file": "cdk-menu-inline-example.html"
  }) -->

### Pop-up Menus

You can create pop-up menus using the `cdkMenu` directive as well. Add this directive to the
element you want to serve as the content for your pop-up menu. Then wrap the content element in an
`ng-template` and reference the template from the `cdkMenuTriggerFor` property of the trigger. This
will allow the trigger to show and hide the menu content as needed.

<!-- example({
  "example": "cdk-menu-standalone-menu",
  "file": "cdk-menu-standalone-menu-example.html"
  }) -->

### Menu Bars

Menu bars are a type of inline menu that you can create using the `cdkMenuBar` directive. They
follow the [ARIA menubar][menubar] spec and behave similarly to a desktop application menubar. Each
bar consists of at least one `cdkMenuItem` that triggers a submenu.

<!-- example({
  "example": "cdk-menu-menubar",
  "file": "cdk-menu-menubar-example.html"
  }) -->

### Context Menus

A context menus is a type of pop-up menu that doesn't have a traditional trigger element, instead
it is triggered when a user right-clicks within some container element. You can mark a
container element with the `cdkContextMenuTriggerFor`, which behaves like `cdkMenuTriggerFor` except
that it responds to the browser's native `contextmenu` event. Custom context menus appear next to
the cursor, similarly to native context menus.

<!-- example({
  "example": "cdk-menu-context",
  "file": "cdk-menu-context-example.html"
  }) -->

You can nest context menu container elements. Upon right-click, the menu associated with the closest
container element will open.

<!-- example({
  "example": "cdk-menu-nested-context",
  "file": "cdk-menu-nested-context-example.html",
  "region": "triggers"
  }) -->

In the example above, right-clicking on "Inner context menu" will open up the "inner" menu and
right-clicking inside "Outer context menu" will open up the "outer" menu.


### Menu Items

The `cdkMenuItem` directive allows users to navigate menu items via keyboard.
You can add a custom action to a menu item with the `cdkMenuItemTriggered` output.

<!-- example({"example":"cdk-menu-standalone-stateful-menu",
              "file":"cdk-menu-standalone-stateful-menu-example.html",
              "region":"reset-item"}) -->

You can create nested menus by using a menu item as the trigger for another menu.

<!-- example({"example":"cdk-menu-menubar",
              "file":"cdk-menu-menubar-example.html",
              "region":"file-trigger"}) -->

#### Menu Item Checkboxes

A `cdkMenuItemCheckbox` is a special type of menu item that behaves as a checkbox. You can use this
type of menu item to toggle items on and off. An element with the `cdkMenuItemCheckbox` directive
does not need the additional `cdkMenuItem` directive.

Checkbox items do not track their own state. You must bind the checked state using the
`cdkMenuItemChecked` input and listen to `cdkMenuItemTriggered` to know when it is toggled. If you
don't bind the state it will reset when the menu is closed and re-opened.

<!-- example({"example":"cdk-menu-standalone-stateful-menu",
              "file":"cdk-menu-standalone-stateful-menu-example.html",
              "region":"bold-item"}) -->

#### Menu Item Radios

A `cdkMenuItemRadio` is a special type of menu item that behaves as a radio button. You can use this
type of menu item for menus with exclusively selectable items. An element with the `cdkMenuItemRadio`
directive does not need the additional `cdkMenuItem` directive.

As with checkbox items, radio items do not track their own state, but you can track it by binding
`cdkMenuItemChecked` and listening for `cdkMenuItemTriggered`. If you do not bind the state the
selection will reset when the menu is closed and reopened.

<!-- example({"example":"cdk-menu-standalone-stateful-menu",
              "file":"cdk-menu-standalone-stateful-menu-example.html",
              "region":"size-items"}) -->

#### Groups

By default `cdkMenu` acts as a group for `cdkMenuItemRadio` elements. Elements with
`cdkMenuItemRadio` added as children of a `cdkMenu` will be logically grouped and only a single item
can have the checked state.

If you would like to have unrelated groups of radio buttons within a single menu you should use the
`cdkMenuGroup` directive.

<!-- example({
  "example": "cdk-menu-standalone-stateful-menu",
  "file": "cdk-menu-standalone-stateful-menu-example.html"
  }) -->

### Smart Menu Aim

`@angular/cdk/menu` is capable of intelligently predicting when a user intends to navigate to an
open submenu and preventing premature closeouts. This functionality prevents users from having to
hunt through the open menus in a maze-like fashion to reach their destination. To enable this
feature for a menu and its sub-menus, add the `cdkTargetMenuAim` directive to the `cdkMenu` or
`cdkMenuBar` element.

![menu aim diagram][diagram]

As demonstrated in the diagram above we first track the user's mouse movements within a menu. Next,
when a user mouses into a sibling menu item (e.g. Share button) the sibling item asks the Menu Aim
service if it can perform its close actions. In order to determine if the current submenu can be
closed out, the Menu Aim service calculates the slope between a selected target coordinate in the
submenu and the previous mouse point, and the slope between the target and the current mouse point.
If the slope of the current mouse point is greater than the slope of the previous that means the
user is moving towards the submenu, so we shouldn't close out. Users however may sometimes stop
short in a sibling item after moving towards the submenu. The service is intelligent enough to
detect this intention and will trigger the next menu.

### Accessibility

The set of directives defined in `@angular/cdk/menu` follow accessibility best practices as defined
in the [ARIA spec][menubar]. Specifically, the menus are aware of left-to-right and right-to-left
layouts and opened appropriately. You should however add any necessary CSS styles. Menu items should
always have meaningful labels, whether through text content, `aria-label`, or `aria-labelledby`.
Finally, keyboard interaction is supported as defined in the [ARIA menubar keyboard interaction spec][keyboard].

<!-- links -->

[aria]: https://www.w3.org/TR/wai-aria-1.1/ 'ARIA Spec'
[menubar]: https://www.w3.org/TR/wai-aria-practices-1.1/#menu 'ARIA Menubar Pattern'
[keyboard]:
  https://www.w3.org/TR/wai-aria-practices-1.1/#keyboard-interaction-12
  'ARIA Menubar Keyboard Interaction'
[diagram]: https://material.angular.io/assets/img/menuaim.png 'Menu Aim Diagram'


import {Component, ElementRef, ViewChild} from '@angular/core';
import {
  ComponentFixture,
  TestBed,
  fakeAsync,
  inject,
  tick,
  waitForAsync,
} from '@angular/core/testing';
import {ContentObserver, MutationObserverFactory, ObserversModule} from './observe-content';

describe('Observe content directive', () => {
  describe('basic usage', () => {
    beforeEach(waitForAsync(() => {
      TestBed.configureTestingModule({
        imports: [ObserversModule, ComponentWithTextContent, ComponentWithChildTextContent],
      });
    }));

    it('should trigger the callback when the content of the element changes', done => {
      let fixture = TestBed.createComponent(ComponentWithTextContent);
      fixture.detectChanges();

      // If the hint label is empty, expect no label.
      const spy = spyOn(fixture.componentInstance, 'doSomething').and.callFake(() => {
        expect(spy).toHaveBeenCalled();
        done();
      });

      expect(spy).not.toHaveBeenCalled();

      fixture.componentInstance.text = 'text';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
    });

    it('should trigger the callback when the content of the children changes', done => {
      let fixture = TestBed.createComponent(ComponentWithChildTextContent);
      fixture.detectChanges();

      // If the hint label is empty, expect no label.
      const spy = spyOn(fixture.componentInstance, 'doSomething').and.callFake(() => {
        expect(spy).toHaveBeenCalled();
        done();
      });

      expect(spy).not.toHaveBeenCalled();

      fixture.componentInstance.text = 'text';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
    });

    it('should disconnect the MutationObserver when the directive is disabled', () => {
      const observeSpy = jasmine.createSpy('observe spy');
      const disconnectSpy = jasmine.createSpy('disconnect spy');

      // Note: since we can't know exactly when the native MutationObserver will emit, we can't
      // test this scenario reliably without risking flaky tests, which is why we supply a mock
      // MutationObserver and check that the methods are called at the right time.
      TestBed.overrideProvider(MutationObserverFactory, {
        deps: [],
        useFactory: () => ({
          create: () => ({observe: observeSpy, disconnect: disconnectSpy}),
        }),
      });

      const fixture = TestBed.createComponent(ComponentWithTextContent);
      fixture.detectChanges();

      expect(observeSpy).toHaveBeenCalledTimes(1);
      expect(disconnectSpy).not.toHaveBeenCalled();

      fixture.componentInstance.disabled = true;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(observeSpy).toHaveBeenCalledTimes(1);
      expect(disconnectSpy).toHaveBeenCalledTimes(1);
    });
  });

  describe('debounced', () => {
    let fixture: ComponentFixture<ComponentWithDebouncedListener>;
    let callbacks: Function[];
    let invokeCallbacks = (args?: any) => callbacks.forEach(callback => callback(args));

    beforeEach(waitForAsync(() => {
      callbacks = [];

      TestBed.configureTestingModule({
        imports: [ObserversModule, ComponentWithDebouncedListener],
        providers: [
          {
            provide: MutationObserverFactory,
            useValue: {
              create: function (callback: Function) {
                callbacks.push(callback);
                return {
                  observe: () => {},
                  disconnect: () => {},
                };
              },
            },
          },
        ],
      });

      fixture = TestBed.createComponent(ComponentWithDebouncedListener);
      fixture.detectChanges();
    }));

    it('should debounce the content changes', fakeAsync(() => {
      invokeCallbacks([{type: 'fake'}]);
      invokeCallbacks([{type: 'fake'}]);
      invokeCallbacks([{type: 'fake'}]);

      tick(500);
      expect(fixture.componentInstance.spy).toHaveBeenCalledTimes(1);
    }));
  });
});

describe('ContentObserver injectable', () => {
  describe('basic usage', () => {
    let callbacks: Function[];
    let invokeCallbacks = (args?: any) => callbacks.forEach(callback => callback(args));
    let contentObserver: ContentObserver;

    beforeEach(fakeAsync(() => {
      callbacks = [];

      TestBed.configureTestingModule({
        imports: [ObserversModule, UnobservedComponentWithTextContent],
        providers: [
          {
            provide: MutationObserverFactory,
            useValue: {
              create: function (callback: Function) {
                callbacks.push(callback);
                return {
                  observe: () => {},
                  disconnect: () => {},
                };
              },
            },
          },
        ],
      });
    }));

    beforeEach(inject([ContentObserver], (co: ContentObserver) => {
      contentObserver = co;
    }));

    it('should trigger the callback when the content of the element changes', fakeAsync(() => {
      const spy = jasmine.createSpy('content observer');
      const fixture = TestBed.createComponent(UnobservedComponentWithTextContent);
      fixture.detectChanges();

      contentObserver.observe(fixture.componentInstance.contentEl).subscribe(() => spy());

      expect(spy).not.toHaveBeenCalled();

      fixture.componentInstance.text = 'text';
      invokeCallbacks([{type: 'fake'}]);

      expect(spy).toHaveBeenCalled();
    }));

    it('should only create one MutationObserver when observing the same element twice', fakeAsync(
      inject([MutationObserverFactory], (mof: MutationObserverFactory) => {
        const spy = jasmine.createSpy('content observer');
        spyOn(mof, 'create').and.callThrough();
        const fixture = TestBed.createComponent(UnobservedComponentWithTextContent);
        fixture.detectChanges();

        const sub1 = contentObserver
          .observe(fixture.componentInstance.contentEl)
          .subscribe(() => spy());
        contentObserver.observe(fixture.componentInstance.contentEl).subscribe(() => spy());

        expect(mof.create).toHaveBeenCalledTimes(1);

        fixture.componentInstance.text = 'text';
        invokeCallbacks([{type: 'fake'}]);

        expect(spy).toHaveBeenCalledTimes(2);

        spy.calls.reset();
        sub1.unsubscribe();
        fixture.componentInstance.text = 'text text';
        invokeCallbacks([{type: 'fake'}]);

        expect(spy).toHaveBeenCalledTimes(1);
      }),
    ));
  });

  describe('real behavior', () => {
    let spy: jasmine.Spy;
    let contentEl: HTMLElement;
    let contentObserver: ContentObserver;

    beforeEach(waitForAsync(() => {
      TestBed.configureTestingModule({
        imports: [ObserversModule, UnobservedComponentWithTextContent],
      });

      const fixture = TestBed.createComponent(UnobservedComponentWithTextContent);
      fixture.autoDetectChanges();
      spy = jasmine.createSpy('content observer');
      contentObserver = TestBed.inject(ContentObserver);
      contentEl = fixture.componentInstance.contentEl.nativeElement;
      contentObserver.observe(contentEl).subscribe(spy);
    }));

    it('should ignore addition or removal of comments', waitForAsync(async () => {
      const comment = document.createComment('cool');
      await new Promise(r => setTimeout(r));

      spy.calls.reset();
      contentEl.appendChild(comment);
      await new Promise(r => setTimeout(r));
      expect(spy).not.toHaveBeenCalled();

      comment.remove();
      await new Promise(r => setTimeout(r));
      expect(spy).not.toHaveBeenCalled();
    }));

    it('should not ignore addition or removal of text', waitForAsync(async () => {
      const text = document.createTextNode('cool');
      await new Promise(r => setTimeout(r));

      spy.calls.reset();
      contentEl.appendChild(text);
      await new Promise(r => setTimeout(r));
      expect(spy).toHaveBeenCalled();

      spy.calls.reset();
      text.remove();
      await new Promise(r => setTimeout(r));
      expect(spy).toHaveBeenCalled();
    }));

    it('should ignore comment content change', waitForAsync(async () => {
      const comment = document.createComment('cool');
      contentEl.appendChild(comment);
      await new Promise(r => setTimeout(r));

      spy.calls.reset();
      comment.textContent = 'beans';
      await new Promise(r => setTimeout(r));
      expect(spy).not.toHaveBeenCalled();
    }));

    it('should not ignore text content change', waitForAsync(async () => {
      const text = document.createTextNode('cool');
      contentEl.appendChild(text);
      await new Promise(r => setTimeout(r));

      spy.calls.reset();
      text.textContent = 'beans';
      await new Promise(r => setTimeout(r));
      expect(spy).toHaveBeenCalled();
    }));
  });
});

@Component({
  template: `
    <div
      (cdkObserveContent)="doSomething()"
      [cdkObserveContentDisabled]="disabled">{{text}}</div>
  `,
  standalone: true,
  imports: [ObserversModule],
})
class ComponentWithTextContent {
  text = '';
  disabled = false;
  doSomething() {}
}

@Component({
  template: `<div (cdkObserveContent)="doSomething()"><div>{{text}}</div></div>`,
  standalone: true,
  imports: [ObserversModule],
})
class ComponentWithChildTextContent {
  text = '';
  doSomething() {}
}

@Component({
  template: `<div (cdkObserveContent)="spy($event)" [debounce]="debounce">{{text}}</div>`,
  standalone: true,
  imports: [ObserversModule],
})
class ComponentWithDebouncedListener {
  debounce = 500;
  spy = jasmine.createSpy('MutationObserver callback');
}

@Component({
  template: `<div #contentEl>{{text}}</div>`,
  standalone: true,
  imports: [ObserversModule],
})
class UnobservedComponentWithTextContent {
  @ViewChild('contentEl') contentEl: ElementRef;
  text = '';
}


getPlaybackRate(): number {
    if (this._player) {
      return this._player.getPlaybackRate();
    }

    if (this._pendingPlayerState && this._pendingPlayerState.playbackRate != null) {
      return this._pendingPlayerState.playbackRate;
    }

    return 0;
  }

  /** See https://developers.google.com/youtube/iframe_api_reference#getAvailablePlaybackRates */
  getAvailablePlaybackRates(): number[] {
    return this._player ? this._player.getAvailablePlaybackRates() : [];
  }

  /** See https://developers.google.com/youtube/iframe_api_reference#getVideoLoadedFraction */
  getVideoLoadedFraction(): number {
    return this._player ? this._player.getVideoLoadedFraction() : 0;
  }

  /** See https://developers.google.com/youtube/iframe_api_reference#getPlayerState */
  getPlayerState(): YT.PlayerState | undefined {
    if (!this._isBrowser || !window.YT) {
      return undefined;
    }

    if (this._player) {
      return this._player.getPlayerState();
    }

    if (this._pendingPlayerState && this._pendingPlayerState.playbackState != null) {
      return this._pendingPlayerState.playbackState;
    }

    return PlayerState.UNSTARTED;
  }

  /** See https://developers.google.com/youtube/iframe_api_reference#getCurrentTime */
  getCurrentTime(): number {
    if (this._player) {
      return this._player.getCurrentTime();
    }

    if (this._pendingPlayerState && this._pendingPlayerState.seek) {
      return this._pendingPlayerState.seek.seconds;
    }

    return 0;
  }

  /** See https://developers.google.com/youtube/iframe_api_reference#getPlaybackQuality */
  getPlaybackQuality(): YT.SuggestedVideoQuality {
    return this._player ? this._player.getPlaybackQuality() : 'default';
  }

  /** See https://developers.google.com/youtube/iframe_api_reference#getAvailableQualityLevels */
  getAvailableQualityLevels(): YT.SuggestedVideoQuality[] {
    return this._player ? this._player.getAvailableQualityLevels() : [];
  }

  /** See https://developers.google.com/youtube/iframe_api_reference#getDuration */
  getDuration(): number {
    return this._player ? this._player.getDuration() : 0;
  }

  /** See https://developers.google.com/youtube/iframe_api_reference#getVideoUrl */
  getVideoUrl(): string {
    return this._player ? this._player.getVideoUrl() : '';
  }

  /** See https://developers.google.com/youtube/iframe_api_reference#getVideoEmbedCode */
  getVideoEmbedCode(): string {
    return this._player ? this._player.getVideoEmbedCode() : '';
  }

  /**
   * Loads the YouTube API and sets up the player.
   * @param playVideo Whether to automatically play the video once the player is loaded.
   */
  protected _load(playVideo: boolean) {
    // Don't do anything if we're not in a browser environment.
    if (!this._isBrowser) {
      return;
    }

    if (!window.YT || !window.YT.Player) {
      if (this.loadApi) {
        this._isLoading = true;
        loadApi(this._nonce);
      } else if (this.showBeforeIframeApiLoads && (typeof ngDevMode === 'undefined' || ngDevMode)) {
        throw new Error(
          'Namespace YT not found, cannot construct embedded youtube player. ' +
            'Please install the YouTube Player API Reference for iframe Embeds: ' +
            'https://developers.google.com/youtube/iframe_api_reference',
        );
      }

      this._existingApiReadyCallback = window.onYouTubeIframeAPIReady;

      window.onYouTubeIframeAPIReady = () => {
        this._existingApiReadyCallback?.();
        this._ngZone.run(() => this._createPlayer(playVideo));
      };
    } else {
      this._createPlayer(playVideo);
    }
  }

  /** Loads the player depending on the internal state of the component. */
  private _conditionallyLoad() {
    // If the placeholder isn't shown anymore, we have to trigger a load.
    if (!this._shouldShowPlaceholder()) {
      this._load(false);
    } else if (this.playerVars?.autoplay === 1) {
      // If it's an autoplaying video, we have to hide the placeholder and start playing.
      this._load(true);
    }
  }

  /** Whether to show the placeholder element. */
  protected _shouldShowPlaceholder(): boolean {
    if (this.disablePlaceholder) {
      return false;
    }

    // Since we don't load the API on the server, we show the placeholder permanently.
    if (!this._isBrowser) {
      return true;
    }

    return this._hasPlaceholder && !!this.videoId && !this._player;
  }

  /** Gets an object that should be used to store the temporary API state. */
  private _getPendingState(): PendingPlayerState {
    if (!this._pendingPlayerState) {
      this._pendingPlayerState = {};
    }

    return this._pendingPlayerState;
  }

  /**
   * Determines whether a change in the component state
   * requires the YouTube player to be recreated.
   */
  private _shouldRecreatePlayer(changes: SimpleChanges): boolean {
    const change =
      changes['videoId'] ||
      changes['playerVars'] ||
      changes['disableCookies'] ||
      changes['disablePlaceholder'];
    return !!change && !change.isFirstChange();
  }

  /**
   * Creates a new YouTube player and destroys the existing one.
   * @param playVideo Whether to play the video once it loads.
   */
  private _createPlayer(playVideo: boolean) {
    this._player?.destroy();
    this._pendingPlayer?.destroy();

    // A player can't be created if the API isn't loaded,
    // or there isn't a video or playlist to be played.
    if (typeof YT === 'undefined' || (!this.videoId && !this.playerVars?.list)) {
      return;
    }

    // Important! We need to create the Player object outside of the `NgZone`, because it kicks
    // off a 250ms setInterval which will continually trigger change detection if we don't.
    const player = this._ngZone.runOutsideAngular(
      () =>
        new YT.Player(this.youtubeContainer.nativeElement, {
          videoId: this.videoId,
          host: this.disableCookies ? 'https://www.youtube-nocookie.com' : undefined,
          width: this.width,
          height: this.height,
          // Calling `playVideo` on load doesn't appear to actually play
          // the video so we need to trigger it through `playerVars` instead.
          playerVars: playVideo ? {...(this.playerVars || {}), autoplay: 1} : this.playerVars,
        }),
    );

    const whenReady = () => {
      // Only assign the player once it's ready, otherwise YouTube doesn't expose some APIs.
      this._ngZone.run(() => {
        this._isLoading = false;
        this._hasPlaceholder = false;
        this._player = player;
        this._pendingPlayer = undefined;
        player.removeEventListener('onReady', whenReady);
        this._playerChanges.next(player);
        this._setSize();
        this._setQuality();

        if (this._pendingPlayerState) {
          this._applyPendingPlayerState(player, this._pendingPlayerState);
          this._pendingPlayerState = undefined;
        }

        // Only cue the player when it either hasn't started yet or it's cued,
        // otherwise cuing it can interrupt a player with autoplay enabled.
        const state = player.getPlayerState();
        if (state === PlayerState.UNSTARTED || state === PlayerState.CUED || state == null) {
          this._cuePlayer();
        }

        this._changeDetectorRef.markForCheck();
      });
    };

    this._pendingPlayer = player;
    player.addEventListener('onReady', whenReady);
  }

  /** Applies any state that changed before the player was initialized. */
  private _applyPendingPlayerState(player: YT.Player, pendingState: PendingPlayerState): void {
    const {playbackState, playbackRate, volume, muted, seek} = pendingState;

    switch (playbackState) {
      case PlayerState.PLAYING:
        player.playVideo();
        break;
      case PlayerState.PAUSED:
        player.pauseVideo();
        break;
      case PlayerState.CUED:
        player.stopVideo();
        break;
    }

    if (playbackRate != null) {
      player.setPlaybackRate(playbackRate);
    }

    if (volume != null) {
      player.setVolume(volume);
    }

    if (muted != null) {
      muted ? player.mute() : player.unMute();
    }

    if (seek != null) {
      player.seekTo(seek.seconds, seek.allowSeekAhead);
    }
  }

  /** Cues the player based on the current component state. */
  private _cuePlayer() {
    if (this._player && this.videoId) {
      this._player.cueVideoById({
        videoId: this.videoId,
        startSeconds: this.startSeconds,
        endSeconds: this.endSeconds,
        suggestedQuality: this.suggestedQuality,
      });
    }
  }

  /** Sets the player's size based on the current input values. */


<!doctype html>

  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Angular Material</title>
  <base href="/">

  <link rel="preconnect" href="https://fonts.gstatic.com">
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
  <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
  <link href="theme.css" rel="stylesheet">

  <e2e-app>Loading...</e2e-app>
  I am a sibling!


@use '@angular/material' as mat;

$theme: mat.define-theme((
  color: (
    theme-type: light,
    primary: mat.$azure-palette,
    tertiary: mat.$blue-palette,
  ),
  density: (
    scale: 0,
  )
));

html {
  @include mat.all-component-themes($theme);
}

@include mat.typography-hierarchy($theme);


import {Component} from '@angular/core';
import {DataSource} from '@angular/cdk/collections';
import {NgForm, FormsModule} from '@angular/forms';
import {MatIconModule} from '@angular/material/icon';
import {MatButtonModule} from '@angular/material/button';
import {MatInputModule} from '@angular/material/input';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatPopoverEditModule} from '@angular/material-experimental/popover-edit';
import {CdkPopoverEditModule} from '@angular/cdk-experimental/popover-edit';
import {MatTableModule} from '@angular/material/table';
import {BehaviorSubject, Observable} from 'rxjs';

export interface Person {
  id: number;
  firstName: string;
  middleName: string;
  lastName: string;
}

const PERSON_DATA: Person[] = [
  {id: 1, firstName: 'Terra', middleName: 'Maduin', lastName: 'Branford'},
  {id: 2, firstName: 'Locke', middleName: '', lastName: 'Cole'},
  {id: 3, firstName: 'Celes', middleName: 'Gestahl', lastName: 'Chere'},
  {id: 4, firstName: 'Edgar', middleName: 'Roni', lastName: 'Figaro'},
  {id: 5, firstName: 'Sabin', middleName: 'Rene', lastName: 'Figaro'},
  {id: 6, firstName: 'Clyde', middleName: '"Shadow"', lastName: 'Arrowny'},
  {id: 7, firstName: 'Setzer', middleName: '', lastName: 'Gabbiani'},
  {id: 8, firstName: 'Cid', middleName: 'Del Norte', lastName: 'Marquez'},
  {id: 9, firstName: 'Mog', middleName: '', lastName: 'McMoogle'},
];

/**
 * @title Material Popover Edit spanning multiple columns on a Material data-table
 */
@Component({
  selector: 'popover-edit-cell-span-mat-table-example',
  styleUrl: 'popover-edit-cell-span-mat-table-example.css',
  templateUrl: 'popover-edit-cell-span-mat-table-example.html',
  imports: [
    MatTableModule,
    CdkPopoverEditModule,
    FormsModule,
    MatPopoverEditModule,
    MatFormFieldModule,
    MatInputModule,
    MatButtonModule,
    MatIconModule,
  ],
})
export class PopoverEditCellSpanMatTableExample {
  displayedColumns: string[] = ['id', 'firstName', 'middleName', 'lastName'];
  dataSource = new ExampleDataSource();

  readonly preservedValues = new WeakMap<Person, any>();

  onSubmit(person: Person, f: NgForm) {
    if (!f.valid) {
      return;
    }

    person.firstName = f.value['firstName'];
    person.middleName = f.value['middleName'];
    person.lastName = f.value['lastName'];
  }
}

/**
 * Data source to provide what data should be rendered in the table. Note that the data source
 * can retrieve its data in any way. In this case, the data source is provided a reference
 * to a common data base, ExampleDatabase. It is not the data source's responsibility to manage
 * the underlying data. Instead, it only needs to take the data and send the table exactly what
 * should be rendered.
 */
export class ExampleDataSource extends DataSource<Person> {
  /** Stream of data that is provided to the table. */
  data = new BehaviorSubject<Person[]>(PERSON_DATA);

  /** Connect function called by the table to retrieve one stream containing the data to render. */
  connect(): Observable<Person[]> {
    return this.data;
  }

  disconnect() {}
}


<mat-autocomplete #autocomplete="matAutocomplete">
  @for (state of states; track state) {
    <mat-option [value]="state.code">{{ state.name }}</mat-option>
  }
</mat-autocomplete>

<input id="plain" [matAutocomplete]="autocomplete">
<input id="disabled" disabled [matAutocomplete]="autocomplete">


Control value: {{myControl.value || 'empty'}}

<form class="example-form">
  <mat-form-field class="example-full-width">
    <mat-label>Number</mat-label>
    <input #input
           type="text"
           placeholder="Pick one"
           matInput
           [formControl]="myControl"
           [matAutocomplete]="auto"
           (input)="filter()"
           (focus)="filter()">
    <mat-autocomplete requireSelection #auto="matAutocomplete">
      @for (option of filteredOptions; track option) {
        <mat-option [value]="option">{{option}}</mat-option>
      }
    </mat-autocomplete>
  </mat-form-field>
</form>


import {Component} from '@angular/core';
import {MatInputModule} from '@angular/material/input';
import {MatFormFieldModule} from '@angular/material/form-field';
import {FormsModule} from '@angular/forms';

/**
 * @title Inputs in a form
 */
@Component({
  selector: 'input-form-example',
  templateUrl: 'input-form-example.html',
  styleUrl: 'input-form-example.css',
  imports: [FormsModule, MatFormFieldModule, MatInputModule],
})
export class InputFormExample {}


import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core';
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {provideMomentDateAdapter} from '@angular/material-moment-adapter';
import {MatDatepicker, MatDatepickerModule} from '@angular/material/datepicker';

// Depending on whether rollup is used, moment needs to be imported differently.
// Since Moment.js doesn't have a default export, we normally need to import using the `* as`
// syntax. However, rollup creates a synthetic default module and we thus need to import it using
// the `default as` syntax.
import * as _moment from 'moment';
// tslint:disable-next-line:no-duplicate-imports
import {default as _rollupMoment, Moment} from 'moment';
import {MatInputModule} from '@angular/material/input';
import {MatFormFieldModule} from '@angular/material/form-field';

const moment = _rollupMoment || _moment;

// See the Moment.js docs for the meaning of these formats:
// https://momentjs.com/docs/#/displaying/format/
export const MY_FORMATS = {
  parse: {
    dateInput: 'MM/YYYY',
  },
  display: {
    dateInput: 'MM/YYYY',
    monthYearLabel: 'MMM YYYY',
    dateA11yLabel: 'LL',
    monthYearA11yLabel: 'MMMM YYYY',
  },
};

/** @title Datepicker emulating a Year and month picker */
@Component({
  selector: 'datepicker-views-selection-example',
  templateUrl: 'datepicker-views-selection-example.html',
  styleUrl: 'datepicker-views-selection-example.css',
  providers: [
    // Moment can be provided globally to your app by adding `provideMomentDateAdapter`
    // to your app config. We provide it at the component level here, due to limitations
    // of our example generation script.
    provideMomentDateAdapter(MY_FORMATS),
  ],
  encapsulation: ViewEncapsulation.None,
  imports: [
    MatFormFieldModule,
    MatInputModule,
    MatDatepickerModule,
    FormsModule,
    ReactiveFormsModule,
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DatepickerViewsSelectionExample {
  readonly date = new FormControl(moment());

  setMonthAndYear(normalizedMonthAndYear: Moment, datepicker: MatDatepicker<Moment>) {
    const ctrlValue = this.date.value ?? moment();
    ctrlValue.month(normalizedMonthAndYear.month());
    ctrlValue.year(normalizedMonthAndYear.year());
    this.date.setValue(ctrlValue);
    datepicker.close();
  }
}


import {ChangeDetectionStrategy, Component} from '@angular/core';
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {provideMomentDateAdapter} from '@angular/material-moment-adapter';
import {MatDatepickerModule} from '@angular/material/datepicker';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatInputModule} from '@angular/material/input';
// Depending on whether rollup is used, moment needs to be imported differently.
// Since Moment.js doesn't have a default export, we normally need to import using the `* as`
// syntax. However, rollup creates a synthetic default module and we thus need to import it using
// the `default as` syntax.
import * as _moment from 'moment';
// tslint:disable-next-line:no-duplicate-imports
import {default as _rollupMoment} from 'moment';

const moment = _rollupMoment || _moment;

/** @title Datepicker that uses Moment.js dates */
@Component({
  selector: 'datepicker-moment-example',
  templateUrl: 'datepicker-moment-example.html',
  providers: [
    // Moment can be provided globally to your app by adding `provideMomentDateAdapter`
    // to your app config. We provide it at the component level here, due to limitations
    // of our example generation script.
    provideMomentDateAdapter(),
  ],
  imports: [
    MatFormFieldModule,
    MatInputModule,
    MatDatepickerModule,
    FormsModule,
    ReactiveFormsModule,
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DatepickerMomentExample {
  // Datepicker takes `Moment` objects instead of `Date` objects.
  readonly date = new FormControl(moment([2017, 0, 1]));
}


import {Component} from '@angular/core';
import {MatTableModule} from '@angular/material/table';

export interface PeriodicElement {
  name: string;
  position: number;
  weight: number;
  symbol: string;
}

const ELEMENT_DATA: PeriodicElement[] = [
  {position: 1, name: 'Hydrogen', weight: 1.0079, symbol: 'H'},
  {position: 2, name: 'Helium', weight: 4.0026, symbol: 'He'},
  {position: 3, name: 'Lithium', weight: 6.941, symbol: 'Li'},
  {position: 4, name: 'Beryllium', weight: 9.0122, symbol: 'Be'},
  {position: 5, name: 'Boron', weight: 10.811, symbol: 'B'},
  {position: 6, name: 'Carbon', weight: 12.0107, symbol: 'C'},
  {position: 7, name: 'Nitrogen', weight: 14.0067, symbol: 'N'},
  {position: 8, name: 'Oxygen', weight: 15.9994, symbol: 'O'},
  {position: 9, name: 'Fluorine', weight: 18.9984, symbol: 'F'},
  {position: 10, name: 'Neon', weight: 20.1797, symbol: 'Ne'},
];

/**
 * @title Binding event handlers and properties to the table rows.
 */
@Component({
  selector: 'table-row-binding-example',
  styleUrl: 'table-row-binding-example.css',
  templateUrl: 'table-row-binding-example.html',
  imports: [MatTableModule],
})
export class TableRowBindingExample {
  displayedColumns: string[] = ['position', 'name', 'weight', 'symbol'];
  dataSource = ELEMENT_DATA;
  clickedRows = new Set<PeriodicElement>();
}


import {HttpClient} from '@angular/common/http';
import {Component, ViewChild, AfterViewInit, inject} from '@angular/core';
import {MatPaginator, MatPaginatorModule} from '@angular/material/paginator';
import {MatSort, MatSortModule, SortDirection} from '@angular/material/sort';
import {merge, Observable, of as observableOf} from 'rxjs';
import {catchError, map, startWith, switchMap} from 'rxjs/operators';
import {MatTableModule} from '@angular/material/table';
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
import {DatePipe} from '@angular/common';

/**
 * @title Table retrieving data through HTTP
 */
@Component({
  selector: 'table-http-example',
  styleUrl: 'table-http-example.css',
  templateUrl: 'table-http-example.html',
  imports: [MatProgressSpinnerModule, MatTableModule, MatSortModule, MatPaginatorModule, DatePipe],
})
export class TableHttpExample implements AfterViewInit {
  private _httpClient = inject(HttpClient);

  displayedColumns: string[] = ['created', 'state', 'number', 'title'];
  exampleDatabase: ExampleHttpDatabase | null;
  data: GithubIssue[] = [];

  resultsLength = 0;
  isLoadingResults = true;
  isRateLimitReached = false;

  @ViewChild(MatPaginator) paginator: MatPaginator;
  @ViewChild(MatSort) sort: MatSort;

  ngAfterViewInit() {
    this.exampleDatabase = new ExampleHttpDatabase(this._httpClient);

    // If the user changes the sort order, reset back to the first page.
    this.sort.sortChange.subscribe(() => (this.paginator.pageIndex = 0));

    merge(this.sort.sortChange, this.paginator.page)
      .pipe(
        startWith({}),
        switchMap(() => {
          this.isLoadingResults = true;
          return this.exampleDatabase!.getRepoIssues(
            this.sort.active,
            this.sort.direction,
            this.paginator.pageIndex,
          ).pipe(catchError(() => observableOf(null)));
        }),
        map(data => {
          // Flip flag to show that loading has finished.
          this.isLoadingResults = false;
          this.isRateLimitReached = data === null;

          if (data === null) {
            return [];
          }

          // Only refresh the result length if there is new data. In case of rate
          // limit errors, we do not want to reset the paginator to zero, as that
          // would prevent users from re-triggering requests.
          this.resultsLength = data.total_count;
          return data.items;
        }),
      )
      .subscribe(data => (this.data = data));
  }
}

export interface GithubApi {
  items: GithubIssue[];
  total_count: number;
}

export interface GithubIssue {
  created_at: string;
  number: string;
  state: string;
  title: string;
}

/** An example database that the data source uses to retrieve data for the table. */
export class ExampleHttpDatabase {
  constructor(private _httpClient: HttpClient) {}

  getRepoIssues(sort: string, order: SortDirection, page: number): Observable<GithubApi> {
    const href = 'https://api.github.com/search/issues';
    const requestUrl = `${href}?q=repo:angular/components&sort=${sort}&order=${order}&page=${
      page + 1
    }`;

    return this._httpClient.get<GithubApi>(requestUrl);
  }
}


import {AfterViewInit, Component, ViewChild} from '@angular/core';
import {MatPaginator, MatPaginatorModule} from '@angular/material/paginator';
import {MatSort, MatSortModule} from '@angular/material/sort';
import {MatTableDataSource, MatTableModule} from '@angular/material/table';
import {MatInputModule} from '@angular/material/input';
import {MatFormFieldModule} from '@angular/material/form-field';

export interface UserData {
  id: string;
  name: string;
  progress: string;
  fruit: string;
}

/** Constants used to fill up our data base. */
const FRUITS: string[] = [
  'blueberry',
  'lychee',
  'kiwi',
  'mango',
  'peach',
  'lime',
  'pomegranate',
  'pineapple',
];
const NAMES: string[] = [
  'Maia',
  'Asher',
  'Olivia',
  'Atticus',
  'Amelia',
  'Jack',
  'Charlotte',
  'Theodore',
  'Isla',
  'Oliver',
  'Isabella',
  'Jasper',
  'Cora',
  'Levi',
  'Violet',
  'Arthur',
  'Mia',
  'Thomas',
  'Elizabeth',
];

/**
 * @title Data table with sorting, pagination, and filtering.
 */
@Component({
  selector: 'table-overview-example',
  styleUrl: 'table-overview-example.css',
  templateUrl: 'table-overview-example.html',
  imports: [MatFormFieldModule, MatInputModule, MatTableModule, MatSortModule, MatPaginatorModule],
})
export class TableOverviewExample implements AfterViewInit {
  displayedColumns: string[] = ['id', 'name', 'progress', 'fruit'];
  dataSource: MatTableDataSource<UserData>;

  @ViewChild(MatPaginator) paginator: MatPaginator;
  @ViewChild(MatSort) sort: MatSort;

  constructor() {
    // Create 100 users
    const users = Array.from({length: 100}, (_, k) => createNewUser(k + 1));

    // Assign the data to the data source for the table to render
    this.dataSource = new MatTableDataSource(users);
  }

  ngAfterViewInit() {
    this.dataSource.paginator = this.paginator;
    this.dataSource.sort = this.sort;
  }

  applyFilter(event: Event) {
    const filterValue = (event.target as HTMLInputElement).value;
    this.dataSource.filter = filterValue.trim().toLowerCase();

    if (this.dataSource.paginator) {
      this.dataSource.paginator.firstPage();
    }
  }
}

/** Builds and returns a new User. */
function createNewUser(id: number): UserData {
  const name =
    NAMES[Math.round(Math.random() * (NAMES.length - 1))] +
    ' ' +
    NAMES[Math.round(Math.random() * (NAMES.length - 1))].charAt(0) +
    '.';

  return {
    id: id.toString(),
    name: name,
    progress: Math.round(Math.random() * 100).toString(),
    fruit: FRUITS[Math.round(Math.random() * (FRUITS.length - 1))],
  };
}


<form [formGroup]="form">
  <mat-selection-list #shoesList [formControl]="shoesControl" name="shoes" [multiple]="false">
    @for (shoe of shoes; track shoe) {
      <mat-list-option [value]="shoe.value">{{shoe.name}}</mat-list-option>
    }
  </mat-selection-list>
  <p>
     Option selected: {{shoesControl.value ? shoesControl.value[0] : 'None'}}
  </p>
</form>


<mat-selection-list #shoes>
  @for (shoe of typesOfShoes; track shoe) {
    <mat-list-option>{{shoe}}</mat-list-option>
  }
</mat-selection-list>

<p>
  Options selected: {{shoes.selectedOptions.selected.length}}
</p>


<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Angular Material</title>
  <base href="/">

  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <link rel="preconnect" href="https://fonts.gstatic.com">
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
  <link href="https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined" rel="stylesheet">
  <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">

  <!-- FontAwesome for mat-icon demo. -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
</head>
<body>
  <dev-app>Loading...</dev-app>
  <script src="zone.js/bundles/zone.umd.js"></script>
  <script src="bundles/dev-app/main.js" type="module"></script>
  <script>
    (g=>{var h,a,k,p="The Google Maps JavaScript API",c="google",l="importLibrary",q="__ib__",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement("script"));e.set("libraries",[...r]+"");for(k in g)e.set(k.replace(/[A-Z]/g,t=>"_"+t[0].toLowerCase()),g[k]);e.set("callback",c+".maps."+q);a.src=`https://maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+" could not load."));a.nonce=m.querySelector("script[nonce]")?.nonce||"";m.head.append(a)}));d[l]?console.warn(p+" only loads once. Ignoring:",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))})({
      v: "weekly",
      key: window.GOOGLE_MAPS_KEY || 'invalid'
    });
  </script>
</body>
</html>


@use '@angular/material' as mat;
@use '@angular/material-experimental' as experimental;

// Plus imports for other components in your app.

// Disable legacy API compatibility, since dev-app is fully migrated to theme inspection API.
mat.$theme-legacy-inspection-api-compatibility: false;

// Define the default (light) theme.
$candy-app-primary: mat.m2-define-palette(mat.$m2-indigo-palette);
$candy-app-accent: mat.m2-define-palette(mat.$m2-pink-palette, A200, A100, A400);
$candy-app-theme: mat.m2-define-light-theme((
  // Define the default colors for our app.
  color: (
    primary: $candy-app-primary,
    accent: $candy-app-accent
  ),
  // Define the default typography for our app.
  typography: mat.m2-define-typography-config(),
  // Define the default density level for our app.
  density: 0,
));

@include mat.app-background();
@include mat.elevation-classes();

// Include the default theme styles.
@include mat.all-component-themes($candy-app-theme);
@include experimental.column-resize-theme($candy-app-theme);
@include experimental.popover-edit-theme($candy-app-theme);

@include mat.typography-hierarchy($candy-app-theme);

.demo-strong-focus {
  // Note: we can theme the indicators directly through `strong-focus-indicators` as well.
  // Use the theme so we have some coverage over the entire API surface.
  @include mat.strong-focus-indicators();
  @include mat.strong-focus-indicators-theme($candy-app-theme);
}

// Include the alternative theme styles inside of a block with a CSS class. You can make this
// CSS class whatever you want. In this example, any component inside of an element with
// `.demo-unicorn-dark-theme` will be affected by this alternate dark theme instead of the
// default theme.
.demo-unicorn-dark-theme {
  // Create the color palettes used in our dark theme.
  $dark-primary: mat.m2-define-palette(mat.$m2-blue-grey-palette);
  $dark-accent: mat.m2-define-palette(mat.$m2-amber-palette, A200, A100, A400);
  $dark-warn: mat.m2-define-palette(mat.$m2-deep-orange-palette);
  $dark-colors: mat.m2-define-dark-theme(
      (
        color: (
          primary: $dark-primary,
          accent: $dark-accent,
          warn: $dark-warn
        ),
        density: 0,
        typography: mat.m2-define-typography-config(),
      )
  );

  // Include the dark theme color styles.
  @include mat.all-component-colors($dark-colors);
  @include experimental.column-resize-color($dark-colors);
  @include experimental.popover-edit-color($dark-colors);

  // Include the dark theme colors for focus indicators.
  &.demo-strong-focus {
    @include mat.strong-focus-indicators-color($dark-colors);
  }
}

// Create classes for all density scales which are supported by all MDC-based components.
// The classes are applied conditionally based on the selected density in the dev-app layout
// component.
$density-scales: (-1, -2, -3, -4, minimum, maximum);
@each $density in $density-scales {
  .demo-density-#{$density} {
    @include mat.all-component-densities($density);
  }
}


@use '@angular/material' as mat;

// Plus imports for other components in your app.

// Disable legacy API compatibility, since dev-app is fully migrated to theme inspection API.
mat.$theme-legacy-inspection-api-compatibility: false;

$primary: mat.$azure-palette;
$tertiary: mat.$blue-palette;

// Create a theme with the specified color type and density.
@function create-theme($type: light, $density: 0) {
  @return mat.define-theme((
    color: (
      theme-type: $type,
      primary: $primary,
      tertiary: $tertiary,
      use-system-variables: true,
    ),
    typography: (use-system-variables: true),
    density: (
      scale: $density
    ),
  ));
}

// Define the default (light) theme.
$light-theme: create-theme($type: light);

// Create our dark theme.
$dark-theme: create-theme($type: dark);

// Include the default theme styles.
html {
  color-scheme: light;
  body:not(.demo-experimental-theme) {
    @include mat.all-component-themes($light-theme);
    @include mat.system-level-colors($light-theme);
    @include mat.system-level-typography($light-theme);
    // TODO(mmalerba): Support M3 for experimental components.
    // @include matx.column-resize-theme($light-theme);
    // @include matx.popover-edit-theme($light-theme);
  }

  body.demo-experimental-theme {
    @include mat.theme((
      color: (
        primary: $primary,
        tertiary: $tertiary,
      ),
      typography: Roboto,
      density: 0,
    ));
  }
}

@include mat.typography-hierarchy($light-theme);

.demo-strong-focus {
  // Note: we can theme the indicators directly through `strong-focus-indicators` as well.
  // Use the theme so we have some coverage over the entire API surface.
  @include mat.strong-focus-indicators();
  @include mat.strong-focus-indicators-theme($light-theme);
}

// Include the alternative theme styles inside of a block with a CSS class. You can make this
// CSS class whatever you want. In this example, any component inside of an element with
// `.demo-unicorn-dark-theme` will be affected by this alternate dark theme instead of the
// default theme.
body.demo-unicorn-dark-theme {
  color-scheme: dark;
  &:not(.demo-experimental-theme) {
    // Include the dark theme color styles.
    @include mat.all-component-colors($dark-theme);
    @include mat.system-level-colors($dark-theme);
    // TODO(mmalerba): Support M3 for experimental components.
    // @include matx.column-resize-color($dark-theme);
    // @include matx.popover-edit-color($dark-theme);
  }

  // Include the dark theme colors for focus indicators.
  &.demo-strong-focus {
    @include mat.strong-focus-indicators-color($dark-theme);
  }
}

// Create classes for all density scales which are supported by all MDC-based components.
// The classes are applied conditionally based on the selected density in the dev-app layout
// component.
$density-scales: (-1, -2, -3, -4, minimum, maximum);
@each $scale in $density-scales {
  .demo-density-#{$scale} {
    body:not(.demo-experimental-theme) & {
      $density-theme: create-theme($density: $scale);
      @include mat.all-component-densities($density-theme);
    }

    body.demo-experimental-theme & {
      @include mat.theme((density: $scale));
    }
  }
}

// Enable back-compat CSS for color="..." API & typography hierarchy.
.demo-color-api-back-compat {
  @include mat.color-variants-backwards-compatibility($light-theme);
  @include mat.typography-hierarchy($light-theme, $back-compat: true);

  &.demo-unicorn-dark-theme {
    @include mat.color-variants-backwards-compatibility($dark-theme);
  }
}

// In M3 buttons are smaller than their touch target at zero-density.
.demo-config-buttons button {
  margin: 4px;
}

// In M3 we need some spacing around the list in the sidenav.
mat-nav-list.demo-nav-list {
  margin: 8px;
}


Space above cards: <input type="number" [formControl]="topHeightCtrl">

  <mat-card>
    Reactive length: {{ reactiveStates.length }}

Reactive value: {{ stateCtrl.value | json }}

Reactive dirty: {{ stateCtrl.dirty }}

    <mat-form-field [color]="reactiveStatesTheme">
      <mat-label>State</mat-label>
      <input
        #reactiveInput
        matInput
        [matAutocomplete]="reactiveAuto"
        [formControl]="stateCtrl"
        (input)="reactiveStates = filterStates(reactiveInput.value)"
        (focus)="reactiveStates = filterStates(reactiveInput.value)">
    </mat-form-field>
    <mat-autocomplete #reactiveAuto="matAutocomplete"
      [displayWith]="displayFn"
      [hideSingleSelectionIndicator]="reactiveHideSingleSelectionIndicator"
      [autoActiveFirstOption]="reactiveAutoActiveFirstOption"
      [requireSelection]="reactiveRequireSelection">
      @for (state of reactiveStates; track state; let index = $index) {
        <mat-option [value]="state" [disabled]="reactiveIsStateDisabled(state.index)">
          {{ state.name }}
           ({{ state.code }})
        </mat-option>
      }
    </mat-autocomplete>

      <button mat-button (click)="stateCtrl.reset()">RESET</button>
      <button mat-button (click)="stateCtrl.setValue(states[10])">SET VALUE</button>
      <button mat-button (click)="stateCtrl.enabled ? stateCtrl.disable() : stateCtrl.enable()">
        TOGGLE DISABLED
      </button>

      Disable States
      <select [(ngModel)]="reactiveDisableStateOption" id="reactive-disable-state-options">
        <option value="none">None</option>
        <option value="first-middle-last">Disable First, Middle and Last States</option>
        <option value="all">Disable All States</option>
      </select>

      <mat-checkbox [(ngModel)]="reactiveHideSingleSelectionIndicator">
        Hide Single-Selection Indicator
      </mat-checkbox>

      <mat-checkbox [(ngModel)]="reactiveAutoActiveFirstOption">
        Automatically activate first option
      </mat-checkbox>

      <mat-checkbox [(ngModel)]="reactiveRequireSelection">
        Require Selection
      </mat-checkbox>

  </mat-card>

  <mat-card>

Template-driven value (currentState): {{ currentState }}

Template-driven dirty: {{ modelDir ? modelDir.dirty : false }}

    @if (true) {
      <mat-form-field [color]="templateStatesTheme">
        <mat-label>State</mat-label>
        <input matInput [matAutocomplete]="tdAuto" [(ngModel)]="currentState"
          (ngModelChange)="tdStates = filterStates(currentState)" [disabled]="tdDisabled">
        <mat-autocomplete #tdAuto="matAutocomplete"
          [hideSingleSelectionIndicator]="templateHideSingleSelectionIndicator"
          [autoActiveFirstOption]="templateAutoActiveFirstOption"
          [requireSelection]="templateRequireSelection">
          @for (state of tdStates; track state) {
            <mat-option [value]="state.name"
              [disabled]="templateIsStateDisabled(state.index)">
              {{ state.name }}
            </mat-option>
          }
        </mat-autocomplete>
      </mat-form-field>
    }

      <button mat-button (click)="clearTemplateState()">RESET</button>
      <button mat-button (click)="currentState='California'">SET VALUE</button>
      <button mat-button (click)="tdDisabled=!tdDisabled">
        TOGGLE DISABLED
      </button>
      <select [(ngModel)]="templateStatesTheme">
        @for (theme of availableThemes; track theme) {
          <option [value]="theme.value">{{theme.name}}</option>
        }
      </select>

      <mat-checkbox [(ngModel)]="templateHideSingleSelectionIndicator">
        Hide Single-Selection Indicator
      </mat-checkbox>

      <mat-checkbox [(ngModel)]="templateAutoActiveFirstOption">
        Automatically activate first option
      </mat-checkbox>

      <mat-checkbox [(ngModel)]="templateRequireSelection">
        Require Selection
      </mat-checkbox>

      Disable States
      <select [(ngModel)]="templateDisableStateOption" id="template-disable-state-options">
        <option value="none">None</option>
        <option value="first-middle-last">Disable First, Middle and Last States</option>
        <option value="all">Disable All States</option>
      </select>

  </mat-card>

  <mat-card>

Option groups (currentGroupedState): {{ currentGroupedState }}

    <mat-form-field>
      <mat-label>State</mat-label>
      <input
        matInput
        [matAutocomplete]="groupedAuto"
        [(ngModel)]="currentGroupedState"
        (ngModelChange)="filteredGroupedStates = filterStateGroups(currentGroupedState)">
    </mat-form-field>
  </mat-card>

  <mat-card>
    <mat-card-subtitle>Autocomplete inside a Dialog</mat-card-subtitle>
    <mat-card-content>
      <button mat-button (click)="openDialog()">Open dialog</button>
    </mat-card-content>
  </mat-card>

<mat-autocomplete #groupedAuto="matAutocomplete">
  @for (group of filteredGroupedStates; track group) {
    <mat-optgroup [label]="'States starting with ' + group.letter">
      @for (state of group.states; track state) {
        <mat-option [value]="state.name">{{ state.name }}</mat-option>
      }
    </mat-optgroup>
  }
</mat-autocomplete>


Basic Example

      Pick the video:
      <mat-radio-group aria-label="Select a video" [(ngModel)]="selectedVideo">
        @for (video of videos; track video) {
          <mat-radio-button [value]="video">{{video.name}}</mat-radio-button>
        }
        <mat-radio-button [value]="undefined">Unset</mat-radio-button>
      </mat-radio-group>

      <mat-checkbox [(ngModel)]="disableCookies">Disable cookies</mat-checkbox>
      <mat-checkbox [(ngModel)]="disablePlaceholder">Disable placeholder</mat-checkbox>

    <youtube-player [videoId]="selectedVideoId"
                    [playerVars]="playerVars"
                    [width]="videoWidth"
                    [height]="videoHeight"
                    [disableCookies]="disableCookies"
                    [disablePlaceholder]="disablePlaceholder"
                    [placeholderImageQuality]="placeholderQuality"></youtube-player>

Placeholder quality comparison (high to low)

  <youtube-player
    [videoId]="selectedVideoId"
    [width]="videoWidth"
    [height]="videoHeight"
    placeholderImageQuality="high"/>
  <youtube-player
    [videoId]="selectedVideoId"
    [width]="videoWidth"
    [height]="videoHeight"
    placeholderImageQuality="standard"/>
  <youtube-player
    [videoId]="selectedVideoId"
    [width]="videoWidth"
    [height]="videoHeight"
    placeholderImageQuality="low"/>