{
  protected _viewportRuler = inject(ViewportRuler);
  protected _changeDetectorRef = inject(ChangeDetectorRef);
  readonly _elementRef = inject(ElementRef);
  private _dir = inject(Directionality, {optional: true});
  protected _parentFormField = inject<MatFormField>(MAT_FORM_FIELD, {optional: true});
  ngControl = inject(NgControl, {self: true, optional: true})!;
  private _liveAnnouncer = inject(LiveAnnouncer);

  protected _defaultOptions = inject(MAT_SELECT_CONFIG, {optional: true});

  /** All of the defined select options. */
  @ContentChildren(MatOption, {descendants: true}) options: QueryList<MatOption>;

  // TODO(crisbeto): this is only necessary for the non-MDC select, but it's technically a
  // public API so we have to keep it. It should be deprecated and removed eventually.
  /** All of the defined groups of options. */
  @ContentChildren(MAT_OPTGROUP, {descendants: true}) optionGroups: QueryList<MatOptgroup>;

  /** User-supplied override of the trigger element. */
  @ContentChild(MAT_SELECT_TRIGGER) customTrigger: MatSelectTrigger;

  /**
   * This position config ensures that the top "start" corner of the overlay
   * is aligned with with the top "start" of the origin by default (overlapping
   * the trigger completely). If the panel cannot fit below the trigger, it
   * will fall back to a position above the trigger.
   */
  _positions: ConnectedPosition[] = [
    {
      originX: 'start',
      originY: 'bottom',
      overlayX: 'start',
      overlayY: 'top',
    },
    {
      originX: 'end',
      originY: 'bottom',
      overlayX: 'end',
      overlayY: 'top',
    },
    {
      originX: 'start',
      originY: 'top',
      overlayX: 'start',
      overlayY: 'bottom',
      panelClass: 'mat-mdc-select-panel-above',
    },
    {
      originX: 'end',
      originY: 'top',
      overlayX: 'end',
      overlayY: 'bottom',
      panelClass: 'mat-mdc-select-panel-above',
    },
  ];

  /** Scrolls a particular option into the view. */
  _scrollOptionIntoView(index: number): void {
    const option = this.options.toArray()[index];

    if (option) {
      const panel: HTMLElement = this.panel.nativeElement;
      const labelCount = _countGroupLabelsBeforeOption(index, this.options, this.optionGroups);
      const element = option._getHostElement();

      if (index === 0 && labelCount === 1) {
        // If we've got one group label before the option and we're at the top option,
        // scroll the list to the top. This is better UX than scrolling the list to the
        // top of the option, because it allows the user to read the top group's label.
        panel.scrollTop = 0;
      } else {
        panel.scrollTop = _getOptionScrollPosition(
          element.offsetTop,
          element.offsetHeight,
          panel.scrollTop,
          panel.offsetHeight,
        );
      }
    }
  }

  /** Called when the panel has been opened and the overlay has settled on its final position. */
  private _positioningSettled() {
    this._scrollOptionIntoView(this._keyManager.activeItemIndex || 0);
  }

  /** Creates a change event object that should be emitted by the select. */
  private _getChangeEvent(value: any) {
    return new MatSelectChange(this, value);
  }

  /** Factory function used to create a scroll strategy for this select. */
  private _scrollStrategyFactory = inject(MAT_SELECT_SCROLL_STRATEGY);

  /** Whether or not the overlay panel is open. */
  private _panelOpen = false;

  /** Comparison function to specify which option is displayed. Defaults to object equality. */
  private _compareWith = (o1: any, o2: any) => o1 === o2;

  /** Unique id for this input. */
  private _uid = `mat-select-${nextUniqueId++}`;

  /** Current `aria-labelledby` value for the select trigger. */
  private _triggerAriaLabelledBy: string | null = null;

  /**
   * Keeps track of the previous form control assigned to the select.
   * Used to detect if it has changed.
   */
  private _previousControl: AbstractControl | null | undefined;

  /** Emits whenever the component is destroyed. */
  protected readonly _destroy = new Subject<void>();

  /** Tracks the error state of the select. */
  private _errorStateTracker: _ErrorStateTracker;

  /**
   * Emits whenever the component state changes and should cause the parent
   * form-field to update. Implemented as part of `MatFormFieldControl`.
   * @docs-private
   */
  readonly stateChanges = new Subject<void>();

  /**
   * Disable the automatic labeling to avoid issues like #27241.
   * @docs-private
   */
  readonly disableAutomaticLabeling = true;

  /**
   * Implemented as part of MatFormFieldControl.
   * @docs-private
   */
  @Input('aria-describedby') userAriaDescribedBy: string;

  /** Deals with the selection logic. */
  _selectionModel: SelectionModel<MatOption>;

  /** Manages keyboard events for options in the panel. */
  _keyManager: ActiveDescendantKeyManager<MatOption>;

  /** Ideal origin for the overlay panel. */
  _preferredOverlayOrigin: CdkOverlayOrigin | ElementRef | undefined;

  /** Width of the overlay panel. */
  _overlayWidth: string | number;

  /** `View -> model callback called when value changes` */
  _onChange: (value: any) => void = () => {};

  /** `View -> model callback called when select has been touched` */
  _onTouched = () => {};

  /** ID for the DOM node containing the select's value. */
  _valueId = `mat-select-value-${nextUniqueId++}`;

  /** Emits when the panel element is finished transforming in. */
  readonly _panelDoneAnimatingStream = new Subject<string>();

  /** Strategy that will be used to handle scrolling while the select panel is open. */
  _scrollStrategy: ScrollStrategy;

  _overlayPanelClass: string | string[] = this._defaultOptions?.overlayPanelClass || '';

  /** Whether the select is focused. */
  get focused(): boolean {
    return this._focused || this._panelOpen;
  }
  private _focused = false;

  /** A name for this control that can be used by `mat-form-field`. */
  controlType = 'mat-select';

  /** Trigger that opens the select. */
  @ViewChild('trigger') trigger: ElementRef;

  /** Panel containing the select options. */
  @ViewChild('panel') panel: ElementRef;

  /** Overlay pane containing the options. */
  @ViewChild(CdkConnectedOverlay)
  protected _overlayDir: CdkConnectedOverlay;

  /** Classes to be passed to the select panel. Supports the same syntax as `ngClass`. */
  @Input() panelClass: string | string[] | Set<string> | {[key: string]: any};

  /** Whether the select is disabled. */
  @Input({transform: booleanAttribute})
  disabled: boolean = false;

  /** Whether ripples in the select are disabled. */
  @Input({transform: booleanAttribute})
  disableRipple: boolean = false;

  /** Tab index of the select. */
  @Input({
    transform: (value: unknown) => (value == null ? 0 : numberAttribute(value)),
  })
  tabIndex: number = 0;

  /** Whether checkmark indicator for single-selection options is hidden. */
  @Input({transform: booleanAttribute})
  get hideSingleSelectionIndicator(): boolean {
    return this._hideSingleSelectionIndicator;
  }
  set hideSingleSelectionIndicator(value: boolean) {
    this._hideSingleSelectionIndicator = value;
    this._syncParentProperties();
  }
  private _hideSingleSelectionIndicator: boolean =
    this._defaultOptions?.hideSingleSelectionIndicator ?? false;

  /** Placeholder to be shown if no value has been selected. */
  @Input()
  get placeholder(): string {
    return this._placeholder;
  }
  set placeholder(value: string) {
    this._placeholder = value;
    this.stateChanges.next();
  }
  private _placeholder: string;

  /** Whether the component is required. */
  @Input({transform: booleanAttribute})
  get required(): boolean {
    return this._required ?? this.ngControl?.control?.hasValidator(Validators.required) ?? false;
  }
  set required(value: boolean) {
    this._required = value;
    this.stateChanges.next();
  }
  private _required: boolean | undefined;

  /** Whether the user should be allowed to select multiple options. */
  @Input({transform: booleanAttribute})
  get multiple(): boolean {
    return this._multiple;
  }
  set multiple(value: boolean) {
    if (this._selectionModel && (typeof ngDevMode === 'undefined' || ngDevMode)) {
      throw getMatSelectDynamicMultipleError();
    }

    this._multiple = value;
  }
  private _multiple: boolean = false;


`<mat-select>` is a form control for selecting a value from a set of options, similar to the native
`<select>` element. You can read more about selects in the
[Material Design spec](https://material.io/design/components/menus.html). It is designed to work
inside of a [`<mat-form-field>`](https://material.angular.io/components/form-field/overview)
element.

To add options to the select, add `<mat-option>` elements to the `<mat-select>`. Each `<mat-option>`
has a `value` property that can be used to set the value that will be selected if the user chooses
this option. The content of the `<mat-option>` is what will be shown to the user.

Angular Material also supports use of the native `<select>` element inside of
`<mat-form-field>`. The native control has several performance, accessibility,
and usability advantages. See [the documentation for
form-field](https://material.angular.io/components/form-field) for more information.

To use a native select inside `<mat-form-field>`, import `MatInputModule` and add the
`matNativeControl` attribute to the `<select>` element.

### Getting and setting the select value

The `<mat-select>` supports 2-way binding to the `value` property without the need for Angular
forms.

Both`<mat-select>` and `<select>` support all of the form directives from the core `FormsModule` (`NgModel`) and
`ReactiveFormsModule` (`FormControl`, `FormGroup`, etc.) As with native `<select>`, `<mat-select>`
also supports a `compareWith` function. (Additional information about using a custom `compareWith`
function can be found in the
[Angular forms documentation](https://angular.dev/api/forms/SelectControlValueAccessor#compareWith)).

### Form field features

There are a number of `<mat-form-field>` features that can be used with both `<select>` and `<mat-select>`. These
include error messages, hint text, prefix & suffix, and theming. For additional information about
these features, see the
[form field documentation](https://material.angular.io/components/form-field/overview).

### Setting a static placeholder

The placeholder is text shown when the `<mat-form-field>` label is floating but the `<mat-select>`
is empty. It is used to give the user an additional hint about the value they should select. The
placeholder can be specified by setting the `placeholder` attribute on the `<mat-select>` element.
In some cases that `<mat-form-field>` may use the placeholder as the label (see the
[form field label documentation](https://material.angular.io/components/form-field/overview#floating-label)).

### Disabling the select or individual options

It is possible to disable the entire select or individual options in the select by using the
disabled property on the `<select>` or `<mat-select>` and the `<option>` or `<mat-option>` elements respectively.
When working with Reactive Forms, the select component can be disabled/enabled via form controls.
This can be accomplished by creating a `FormControl` with the disabled property
`FormControl({value: '', disabled: true})` or using `FormControl.enable()`, `FormControl.disable()`.

### Resetting the select value

If you want one of your options to reset the select's value, you can omit specifying its value.

### Creating groups of options

The `<mat-optgroup>` element can be used to group common options under a subheading. The name of the
group can be set using the `label` property of `<mat-optgroup>`. Like individual `<mat-option>`
elements, an entire `<mat-optgroup>` can be disabled or enabled by setting the `disabled` property
on the group.

### Multiple selection

`<mat-select>` defaults to single-selection mode, but can be configured to allow multiple selection
by setting the `multiple` property. This will allow the user to select multiple values at once. When
using the `<mat-select>` in multiple selection mode, its value will be a sorted list of all selected
values rather than a single value.

Using multiple selection with a native select element (`<select multiple>`) is discouraged
inside `<mat-form-field>`, as the inline listbox appearance is inconsistent with other
Material Design components.

### Customizing the trigger label

If you want to display a custom trigger label inside a `<mat-select>`, you can use the
`<mat-select-trigger>` element.

### Disabling the ripple effect

By default, when a user clicks on a `<mat-option>`, a ripple animation is shown. This can be disabled
by setting the `disableRipple` property on `<mat-select>`.

### Adding custom styles to the dropdown panel

In order to facilitate easily styling the dropdown panel, `<mat-select>` has a `panelClass` property
which can be used to apply additional CSS classes to the dropdown panel.

### Changing when error messages are shown

The `<mat-form-field>` allows you to
[associate error messages](https://material.angular.io/components/form-field/overview#error-messages)
with your `<select>` or `<mat-select>`. By default, these error messages are shown when the control is invalid and
either the user has interacted with (touched) the element or the parent form has been submitted. If
you wish to override this behavior (e.g. to show the error as soon as the invalid control is dirty
or when a parent form group is invalid), you can use the `errorStateMatcher` property of the
`<mat-select>`. The property takes an instance of an `ErrorStateMatcher` object. An
`ErrorStateMatcher` must implement a single method `isErrorState` which takes the `FormControl` for
this `<mat-select>` as well as the parent form and returns a boolean indicating whether errors
should be shown. (`true` indicating that they should be shown, and `false` indicating that they
should not.)

A global error state matcher can be specified by setting the `ErrorStateMatcher` provider. This
applies to all inputs. For convenience, `ShowOnDirtyErrorStateMatcher` is available in order to
globally cause input errors to show when the input is dirty and invalid.

```ts
@NgModule({
  providers: [
    {provide: ErrorStateMatcher, useClass: ShowOnDirtyErrorStateMatcher}
  ]
})
```

### Keyboard interaction
| Keyboard shortcut                      | Action                                                                |
|----------------------------------------|-----------------------------------------------------------------------|
| Down Arrow                  | Navigate to the next option.                                          |
| Up Arrow                    | Navigate to the previous option.                                      |
| Enter                       | If closed, open the select panel. If open, selects the active option. |
| Escape                      | Close the select panel.                                               |
| Alt + Up Arrow   | Close the select panel.                                               |
| Alt + Down Arrow | Open the select panel if there are any matching options.              |

### Accessibility
When possible, prefer a native `<select>` element over `MatSelect`. The native control
provides the most accessible experience across the widest range of platforms.

`MatSelect` implements the combobox pattern detailed in the [1.2 version of the ARIA
specification](https://www.w3.org/TR/wai-aria-1.2). The combobox trigger controls a `role="listbox"`
element opened in a pop-up. Previous versions of the ARIA specification
required that `role="combobox"` apply to a text input control, but the 1.2 version of the
specification supports a wider variety of interaction patterns. This newer usage of ARIA works
in all browser and screen-reader combinations supported by Angular Material.

Because the pop-up uses the `role="listbox"` pattern, you should _not_ put other interactive
controls, such as buttons or checkboxes, inside a select option. Nesting interactive controls like
this interferes with most assistive technology.

Always provide an accessible label for the select. This can be done by adding a `<mat-label>`
inside of `<mat-form-field>`, the `aria-label` attribute, or the `aria-labelledby` attribute.

By default, `MatSelect` displays a checkmark to identify selected items. While you can hide the
checkmark indicator for single-selection via `hideSingleSelectionIndicator`, this makes the
component less accessible by making it harder or impossible for users to visually identify selected
items.


Please see the official documentation at https://material.angular.io/components/component/select


/**
 * @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 {
  animate,
  animateChild,
  AnimationTriggerMetadata,
  query,
  state,
  style,
  transition,
  trigger,
} from '@angular/animations';

/**
 * The following are all the animations for the mat-select component, with each
 * const containing the metadata for one animation.
 *
 * The values below match the implementation of the AngularJS Material mat-select animation.
 * @docs-private
 */
export const matSelectAnimations: {
  /**
   * @deprecated No longer being used. To be removed.
   * @breaking-change 12.0.0
   */
  readonly transformPanelWrap: AnimationTriggerMetadata;
  readonly transformPanel: AnimationTriggerMetadata;
} = {
  /**
   * This animation ensures the select's overlay panel animation (transformPanel) is called when
   * closing the select.
   * This is needed due to https://github.com/angular/angular/issues/23302
   */
  transformPanelWrap: trigger('transformPanelWrap', [
    transition('* => void', query('@transformPanel', [animateChild()], {optional: true})),
  ]),

  /** This animation transforms the select's overlay panel on and off the page. */
  transformPanel: trigger('transformPanel', [
    state(
      'void',
      style({
        opacity: 0,
        transform: 'scale(1, 0.8)',
      }),
    ),
    transition(
      'void => showing',
      animate(
        '120ms cubic-bezier(0, 0, 0.2, 1)',
        style({
          opacity: 1,
          transform: 'scale(1, 1)',
        }),
      ),
    ),
    transition('* => void', animate('100ms linear', style({opacity: 0}))),
  ]),
};


it('should not throw when toggling an icon that has a binding in IE11', () => {
      iconRegistry.addSvgIcon('fluffy', trustUrl('cat.svg'));

      const fixture = TestBed.createComponent(IconWithBindingAndNgIf);

      fixture.detectChanges();
      http.expectOne('cat.svg').flush(FAKE_SVGS.cat);

      expect(() => {
        fixture.componentInstance.showIcon = false;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        fixture.componentInstance.showIcon = true;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();
      }).not.toThrow();
    });

    it('should be able to configure the viewBox for the icon set', () => {
      iconRegistry.addSvgIconSet(trustUrl('arrow-set.svg'), {viewBox: '0 0 43 43'});

      const fixture = TestBed.createComponent(IconFromSvgName);
      const testComponent = fixture.componentInstance;
      const matIconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');
      let svgElement: SVGElement;

      testComponent.iconName = 'left-arrow';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      http.expectOne('arrow-set.svg').flush(FAKE_SVGS.arrows);
      svgElement = verifyAndGetSingleSvgChild(matIconElement);

      expect(svgElement.getAttribute('viewBox')).toBe('0 0 43 43');
    });

    it('should remove the SVG element from the DOM when the binding is cleared', () => {
      iconRegistry.addSvgIconSet(trustUrl('arrow-set.svg'));

      const fixture = TestBed.createComponent(IconFromSvgName);

      const testComponent = fixture.componentInstance;
      const icon = fixture.debugElement.nativeElement.querySelector('mat-icon');

      testComponent.iconName = 'left-arrow';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      http.expectOne('arrow-set.svg').flush(FAKE_SVGS.arrows);

      expect(icon.querySelector('svg')).toBeTruthy();

      testComponent.iconName = undefined;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(icon.querySelector('svg')).toBeFalsy();
    });

    it('should keep non-SVG user content inside the icon element', fakeAsync(() => {
      iconRegistry.addSvgIcon('fido', trustUrl('dog.svg'));

      const fixture = TestBed.createComponent(SvgIconWithUserContent);
      const testComponent = fixture.componentInstance;
      const iconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');

      testComponent.iconName = 'fido';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      http.expectOne('dog.svg').flush(FAKE_SVGS.dog);

      const userDiv = iconElement.querySelector('div');

      expect(userDiv).toBeTruthy();
      expect(iconElement.textContent.trim()).toContain('Hello');

      tick();
    }));

    it('should cancel in-progress fetches if the icon changes', fakeAsync(() => {
      // Register an icon that will resolve immediately.
      iconRegistry.addSvgIconLiteral('fluffy', trustHtml(FAKE_SVGS.cat));

      // Register a different icon that takes some time to resolve.
      iconRegistry.addSvgIcon('fido', trustUrl('dog.svg'));

      const fixture = TestBed.createComponent(IconFromSvgName);
      const iconElement = fixture.debugElement.nativeElement.querySelector('mat-icon');

      // Assign the slow icon first.
      fixture.componentInstance.iconName = 'fido';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      // Assign the quick icon while the slow one is still in-flight.
      fixture.componentInstance.iconName = 'fluffy';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      // Expect for the in-flight request to have been cancelled.
      expect(http.expectOne('dog.svg').cancelled).toBe(true);

      // Expect the last icon to have been assigned.
      verifyPathChildElement(verifyAndGetSingleSvgChild(iconElement), 'meow');
    }));

    it('should cancel in-progress fetches if the component is destroyed', fakeAsync(() => {
      iconRegistry.addSvgIcon('fido', trustUrl('dog.svg'));

      const fixture = TestBed.createComponent(IconFromSvgName);
      fixture.componentInstance.iconName = 'fido';
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      fixture.destroy();

      expect(http.expectOne('dog.svg').cancelled).toBe(true);
    }));
  });


it('should allow manually dismissing with an action', fakeAsync(() => {
    const dismissCompleteSpy = jasmine.createSpy('dismiss complete spy');
    const actionCompleteSpy = jasmine.createSpy('action complete spy');
    const snackBarRef = snackBar.open('Some content');
    viewContainerFixture.detectChanges();

    snackBarRef.afterDismissed().subscribe({complete: dismissCompleteSpy});
    snackBarRef.onAction().subscribe({complete: actionCompleteSpy});

    snackBarRef.dismissWithAction();
    viewContainerFixture.detectChanges();
    flush();

    expect(dismissCompleteSpy).toHaveBeenCalled();
    expect(actionCompleteSpy).toHaveBeenCalled();
  }));

  it('should indicate in `afterClosed` whether it was dismissed by an action', fakeAsync(() => {
    const dismissSpy = jasmine.createSpy('dismiss spy');
    const snackBarRef = snackBar.open('Some content');
    viewContainerFixture.detectChanges();

    snackBarRef.afterDismissed().subscribe(dismissSpy);
    snackBarRef.dismissWithAction();
    viewContainerFixture.detectChanges();
    flush();

    expect(dismissSpy).toHaveBeenCalledWith(jasmine.objectContaining({dismissedByAction: true}));
  }));

  it('should complete the onAction stream when not closing via an action', fakeAsync(() => {
    const actionCompleteSpy = jasmine.createSpy('action complete spy');
    const snackBarRef = snackBar.open('Some content');
    viewContainerFixture.detectChanges();

    snackBarRef.onAction().subscribe({complete: actionCompleteSpy});
    snackBarRef.dismiss();
    viewContainerFixture.detectChanges();
    flush();

    expect(actionCompleteSpy).toHaveBeenCalled();
  }));

  it('should dismiss automatically after a specified timeout', fakeAsync(() => {
    const config = new MatSnackBarConfig();
    config.duration = 250;
    const snackBarRef = snackBar.open('content', 'test', config);
    const afterDismissSpy = jasmine.createSpy('after dismiss spy');
    snackBarRef.afterDismissed().subscribe(afterDismissSpy);

    viewContainerFixture.detectChanges();
    tick();
    expect(afterDismissSpy).not.toHaveBeenCalled();

    tick(1000);
    viewContainerFixture.detectChanges();
    tick();
    expect(afterDismissSpy).toHaveBeenCalled();
  }));

  it('should add extra classes to the container', () => {
    snackBar.open(simpleMessage, simpleActionLabel, {panelClass: ['one', 'two']});
    viewContainerFixture.detectChanges();

    let containerClasses =
      overlayContainerElement.querySelector('mat-snack-bar-container')!.classList;

    expect(containerClasses).toContain('one');
    expect(containerClasses).toContain('two');
  });

  it('should set the layout direction', () => {
    snackBar.open(simpleMessage, simpleActionLabel, {direction: 'rtl'});
    viewContainerFixture.detectChanges();

    let pane = overlayContainerElement.querySelector('.cdk-global-overlay-wrapper')!;

    expect(pane.getAttribute('dir'))
      .withContext('Expected the pane to be in RTL mode.')
      .toBe('rtl');
  });

  it('should be able to override the default config', fakeAsync(() => {
    viewContainerFixture.destroy();

    TestBed.resetTestingModule()
      .overrideProvider(MAT_SNACK_BAR_DEFAULT_OPTIONS, {
        deps: [],
        useFactory: () => ({panelClass: 'custom-class'}),
      })
      .configureTestingModule({imports: [MatSnackBarModule, NoopAnimationsModule]});

    snackBar = TestBed.inject(MatSnackBar);
    overlayContainerElement = TestBed.inject(OverlayContainer).getContainerElement();

    snackBar.open(simpleMessage);
    flush();

    expect(overlayContainerElement.querySelector('mat-snack-bar-container')!.classList)
      .withContext('Expected class applied through the defaults to be applied.')
      .toContain('custom-class');
  }));

  it('should dismiss the open snack bar on destroy', fakeAsync(() => {
    snackBar.open(simpleMessage);
    viewContainerFixture.detectChanges();
    expect(overlayContainerElement.childElementCount).toBeGreaterThan(0);

    snackBar.ngOnDestroy();
    viewContainerFixture.detectChanges();
    flush();

    expect(overlayContainerElement.childElementCount).toBe(0);
  }));

  it('should cap the timeout to the maximum accepted delay in setTimeout', fakeAsync(() => {
    const config = new MatSnackBarConfig();
    config.duration = Infinity;
    snackBar.open('content', 'test', config);
    viewContainerFixture.detectChanges();
    spyOn(window, 'setTimeout').and.callThrough();
    tick(100);

    expect(window.setTimeout).toHaveBeenCalledWith(jasmine.any(Function), Math.pow(2, 31) - 1);

    flush();
  }));

  it('should only keep one snack bar in the DOM if multiple are opened at the same time', fakeAsync(() => {
    for (let i = 0; i < 10; i++) {
      snackBar.open('Snack time!', 'Chew');
      viewContainerFixture.detectChanges();
    }

    flush();
    expect(overlayContainerElement.querySelectorAll('mat-snack-bar-container').length).toBe(1);
  }));

  describe('with custom component', () => {
    it('should open a custom component', () => {
      const snackBarRef = snackBar.openFromComponent(BurritosNotification);

      expect(snackBarRef.instance instanceof BurritosNotification)
        .withContext('Expected the snack bar content component to be BurritosNotification')
        .toBe(true);
      expect(overlayContainerElement.textContent!.trim())
        .withContext('Expected component to have the proper text.')
        .toBe('Burritos are on the way.');
    });

    it('should inject the snack bar reference into the component', () => {
      const snackBarRef = snackBar.openFromComponent(BurritosNotification);

      expect(snackBarRef.instance.snackBarRef)
        .withContext('Expected component to have an injected snack bar reference.')
        .toBe(snackBarRef);
    });

    it('should have exactly one MDC label element', () => {
      snackBar.openFromComponent(BurritosNotification);
      viewContainerFixture.detectChanges();
      expect(overlayContainerElement.querySelectorAll('.mdc-snackbar__label').length).toBe(1);
    });

    it('should be able to inject arbitrary user data', () => {
      const snackBarRef = snackBar.openFromComponent(BurritosNotification, {
        data: {
          burritoType: 'Chimichanga',
        },
      });

      expect(snackBarRef.instance.data)
        .withContext('Expected component to have a data object.')
        .toBeTruthy();
      expect(snackBarRef.instance.data.burritoType)
        .withContext('Expected the injected data object to be the one the user provided.')
        .toBe('Chimichanga');
    });

    it('should allow manually dismissing with an action', fakeAsync(() => {
      const dismissCompleteSpy = jasmine.createSpy('dismiss complete spy');
      const actionCompleteSpy = jasmine.createSpy('action complete spy');
      const snackBarRef = snackBar.openFromComponent(BurritosNotification);
      viewContainerFixture.detectChanges();

      snackBarRef.afterDismissed().subscribe({complete: dismissCompleteSpy});
      snackBarRef.onAction().subscribe({complete: actionCompleteSpy});

      snackBarRef.dismissWithAction();
      viewContainerFixture.detectChanges();
      flush();

      expect(dismissCompleteSpy).toHaveBeenCalled();
      expect(actionCompleteSpy).toHaveBeenCalled();
    }));
  });

  describe('with TemplateRef', () => {
    let templateFixture: ComponentFixture<ComponentWithTemplateRef>;

    beforeEach(() => {
      templateFixture = TestBed.createComponent(ComponentWithTemplateRef);
      templateFixture.detectChanges();
    });

    it('should be able to open a snack bar using a TemplateRef', () => {
      templateFixture.componentInstance.localValue = 'Pizza';
      templateFixture.changeDetectorRef.markForCheck();
      snackBar.openFromTemplate(templateFixture.componentInstance.templateRef);
      templateFixture.detectChanges();

      const containerElement = overlayContainerElement.querySelector('mat-snack-bar-container')!;

      expect(containerElement.textContent).toContain('Fries');
      expect(containerElement.textContent).toContain('Pizza');

      templateFixture.componentInstance.localValue = 'Pasta';
      templateFixture.changeDetectorRef.markForCheck();
      templateFixture.detectChanges();

      expect(containerElement.textContent).toContain('Pasta');
    });

    it('should be able to pass in contextual data when opening with a TemplateRef', () => {
      snackBar.openFromTemplate(templateFixture.componentInstance.templateRef, {
        data: {value: 'Oranges'},
      });
      templateFixture.detectChanges();

      const containerElement = overlayContainerElement.querySelector('mat-snack-bar-container')!;

      expect(containerElement.textContent).toContain('Oranges');
    });
  });
});


class MatSnackBarContainer extends BasePortalOutlet implements OnDestroy {
  private _ngZone = inject(NgZone);
  private _elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
  private _changeDetectorRef = inject(ChangeDetectorRef);
  private _platform = inject(Platform);
  snackBarConfig = inject(MatSnackBarConfig);

  private _document = inject(DOCUMENT);
  private _trackedModals = new Set<Element>();

  /** The number of milliseconds to wait before announcing the snack bar's content. */
  private readonly _announceDelay: number = 150;

  /** The timeout for announcing the snack bar's content. */
  private _announceTimeoutId: number;

  /** Whether the component has been destroyed. */
  private _destroyed = false;

  /** The portal outlet inside of this container into which the snack bar content will be loaded. */
  @ViewChild(CdkPortalOutlet, {static: true}) _portalOutlet: CdkPortalOutlet;

  /** Subject for notifying that the snack bar has announced to screen readers. */
  readonly _onAnnounce: Subject<void> = new Subject();

  /** Subject for notifying that the snack bar has exited from view. */
  readonly _onExit: Subject<void> = new Subject();

  /** Subject for notifying that the snack bar has finished entering the view. */
  readonly _onEnter: Subject<void> = new Subject();

  /** The state of the snack bar animations. */
  _animationState = 'void';

  /** aria-live value for the live region. */
  _live: AriaLivePoliteness;

  /**
   * Element that will have the `mdc-snackbar__label` class applied if the attached component
   * or template does not have it. This ensures that the appropriate structure, typography, and
   * color is applied to the attached view.
   */
  @ViewChild('label', {static: true}) _label: ElementRef;

  /**
   * Role of the live region. This is only for Firefox as there is a known issue where Firefox +
   * JAWS does not read out aria-live message.
   */
  _role?: 'status' | 'alert';

  /** Unique ID of the aria-live element. */
  readonly _liveElementId = `mat-snack-bar-container-live-${uniqueId++}`;

  constructor(...args: unknown[]);

  constructor() {
    super();
    const config = this.snackBarConfig;

    // Use aria-live rather than a live role like 'alert' or 'status'
    // because NVDA and JAWS have show inconsistent behavior with live roles.
    if (config.politeness === 'assertive' && !config.announcementMessage) {
      this._live = 'assertive';
    } else if (config.politeness === 'off') {
      this._live = 'off';
    } else {
      this._live = 'polite';
    }

    // Only set role for Firefox. Set role based on aria-live because setting role="alert" implies
    // aria-live="assertive" which may cause issues if aria-live is set to "polite" above.
    if (this._platform.FIREFOX) {
      if (this._live === 'polite') {
        this._role = 'status';
      }
      if (this._live === 'assertive') {
        this._role = 'alert';
      }
    }
  }

  /** Attach a component portal as content to this snack bar container. */
  attachComponentPortal<T>(portal: ComponentPortal<T>): ComponentRef<T> {
    this._assertNotAttached();
    const result = this._portalOutlet.attachComponentPortal(portal);
    this._afterPortalAttached();
    return result;
  }

  /** Attach a template portal as content to this snack bar container. */
  attachTemplatePortal<C>(portal: TemplatePortal<C>): EmbeddedViewRef<C> {
    this._assertNotAttached();
    const result = this._portalOutlet.attachTemplatePortal(portal);
    this._afterPortalAttached();
    return result;
  }

  /**
   * Attaches a DOM portal to the snack bar container.
   * @deprecated To be turned into a method.
   * @breaking-change 10.0.0
   */
  override attachDomPortal = (portal: DomPortal) => {
    this._assertNotAttached();
    const result = this._portalOutlet.attachDomPortal(portal);
    this._afterPortalAttached();
    return result;
  };

  /** Handle end of animations, updating the state of the snackbar. */
  onAnimationEnd(event: AnimationEvent) {
    const {fromState, toState} = event;

    if ((toState === 'void' && fromState !== 'void') || toState === 'hidden') {
      this._completeExit();
    }

    if (toState === 'visible') {
      // Note: we shouldn't use `this` inside the zone callback,
      // because it can cause a memory leak.
      const onEnter = this._onEnter;

      this._ngZone.run(() => {
        onEnter.next();
        onEnter.complete();
      });
    }
  }

  /** Begin animation of snack bar entrance into view. */
  enter(): void {
    if (!this._destroyed) {
      this._animationState = 'visible';
      // _animationState lives in host bindings and `detectChanges` does not refresh host bindings
      // so we have to call `markForCheck` to ensure the host view is refreshed eventually.
      this._changeDetectorRef.markForCheck();
      this._changeDetectorRef.detectChanges();
      this._screenReaderAnnounce();
    }
  }

  /** Begin animation of the snack bar exiting from view. */
  exit(): Observable<void> {
    // It's common for snack bars to be opened by random outside calls like HTTP requests or
    // errors. Run inside the NgZone to ensure that it functions correctly.
    this._ngZone.run(() => {
      // Note: this one transitions to `hidden`, rather than `void`, in order to handle the case
      // where multiple snack bars are opened in quick succession (e.g. two consecutive calls to
      // `MatSnackBar.open`).
      this._animationState = 'hidden';
      this._changeDetectorRef.markForCheck();

      // Mark this element with an 'exit' attribute to indicate that the snackbar has
      // been dismissed and will soon be removed from the DOM. This is used by the snackbar
      // test harness.
      this._elementRef.nativeElement.setAttribute('mat-exit', '');

      // If the snack bar hasn't been announced by the time it exits it wouldn't have been open
      // long enough to visually read it either, so clear the timeout for announcing.
      clearTimeout(this._announceTimeoutId);
    });

    return this._onExit;
  }

  /** Makes sure the exit callbacks have been invoked when the element is destroyed. */
  ngOnDestroy() {
    this._destroyed = true;
    this._clearFromModals();
    this._completeExit();
  }

  /**
   * Removes the element in a microtask. Helps prevent errors where we end up
   * removing an element which is in the middle of an animation.
   */
  private _completeExit() {
    queueMicrotask(() => {
      this._onExit.next();
      this._onExit.complete();
    });
  }

  /**
   * Called after the portal contents have been attached. Can be
   * used to modify the DOM once it's guaranteed to be in place.
   */
  private _afterPortalAttached() {
    const element: HTMLElement = this._elementRef.nativeElement;
    const panelClasses = this.snackBarConfig.panelClass;

    if (panelClasses) {
      if (Array.isArray(panelClasses)) {
        // Note that we can't use a spread here, because IE doesn't support multiple arguments.
        panelClasses.forEach(cssClass => element.classList.add(cssClass));
      } else {
        element.classList.add(panelClasses);
      }
    }

    this._exposeToModals();

    // Check to see if the attached component or template uses the MDC template structure,
    // specifically the MDC label. If not, the container should apply the MDC label class to this
    // component's label container, which will apply MDC's label styles to the attached view.
    const label = this._label.nativeElement;
    const labelClass = 'mdc-snackbar__label';
    label.classList.toggle(labelClass, !label.querySelector(`.${labelClass}`));
  }

  /**
   * Some browsers won't expose the accessibility node of the live element if there is an
   * `aria-modal` and the live element is outside of it. This method works around the issue by
   * pointing the `aria-owns` of all modals to the live element.
   */


import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing';
import {createTestApp, getFileContent} from '@angular/cdk/schematics/testing';
import {COLLECTION_PATH} from '../../paths';

import {Schema} from './schema';

describe('material-navigation-schematic', () => {
  let runner: SchematicTestRunner;

  const baseOptions: Schema = {
    name: 'foo',
    project: 'material',
  };

  beforeEach(() => {
    runner = new SchematicTestRunner('schematics', COLLECTION_PATH);
  });

  function expectNavigationSchematicModuleImports(tree: UnitTestTree) {
    const moduleContent = getFileContent(tree, '/projects/material/src/app/app.module.ts');
    expect(moduleContent).toMatch(/MatToolbarModule,\s+/);
    expect(moduleContent).toMatch(/MatButtonModule,\s+/);
    expect(moduleContent).toMatch(/MatSidenavModule,\s+/);
    expect(moduleContent).toMatch(/MatIconModule,\s+/);
    expect(moduleContent).toMatch(/MatListModule\s+],/);
    expect(moduleContent).toContain(`import { MatButtonModule } from '@angular/material/button';`);
    expect(moduleContent).toContain(`import { MatIconModule } from '@angular/material/icon';`);
    expect(moduleContent).toContain(`import { MatListModule } from '@angular/material/list';`);
    expect(moduleContent).toContain(
      `import { MatToolbarModule } from '@angular/material/toolbar';`,
    );
    expect(moduleContent).toContain(
      `import { MatSidenavModule } from '@angular/material/sidenav';`,
    );
  }

  it('should create navigation files and add them to module', async () => {
    const app = await createTestApp(runner, {standalone: false});
    const tree = await runner.runSchematic('navigation', baseOptions, app);
    const files = tree.files;

    expect(files).toContain('/projects/material/src/app/foo/foo.component.css');
    expect(files).toContain('/projects/material/src/app/foo/foo.component.html');
    expect(files).toContain('/projects/material/src/app/foo/foo.component.spec.ts');
    expect(files).toContain('/projects/material/src/app/foo/foo.component.ts');

    const moduleContent = getFileContent(tree, '/projects/material/src/app/app.module.ts');
    expect(moduleContent).toMatch(/import.*Foo.*from '.\/foo\/foo.component'/);
    expect(moduleContent).toMatch(/declarations:\s*\[[^\]]+?,\r?\n\s+FooComponent\r?\n/m);
  });

  it('should add navigation imports to module', async () => {
    const app = await createTestApp(runner, {standalone: false});
    const tree = await runner.runSchematic('navigation', baseOptions, app);
    expectNavigationSchematicModuleImports(tree);
  });

  it('should support `nav` as schematic alias', async () => {
    const app = await createTestApp(runner, {standalone: false});
    const tree = await runner.runSchematic('nav', baseOptions, app);
    expectNavigationSchematicModuleImports(tree);
  });

  it('should throw if no name has been specified', async () => {
    const appTree = await createTestApp(runner);

    await expectAsync(
      runner.runSchematic('navigation', {project: 'material'}, appTree),
    ).toBeRejectedWithError(/required property 'name'/);
  });

  describe('standalone option', () => {
    it('should generate a standalone component', async () => {
      const app = await createTestApp(runner, {standalone: false});
      const tree = await runner.runSchematic('navigation', {...baseOptions, standalone: true}, app);
      const module = getFileContent(tree, '/projects/material/src/app/app.module.ts');
      const component = getFileContent(tree, '/projects/material/src/app/foo/foo.component.ts');
      const requiredModules = [
        'MatToolbarModule',
        'MatButtonModule',
        'MatSidenavModule',
        'MatListModule',
        'MatIconModule',
      ];

      requiredModules.forEach(name => {
        expect(module).withContext('Module should not import dependencies').not.toContain(name);
        expect(component).withContext('Component should import dependencies').toContain(name);
      });

      expect(module).not.toContain('FooComponent');
      expect(component).toContain('standalone: true');
      expect(component).toContain('imports: [');
    });

    it('should infer the standalone option from the project structure', async () => {
      const app = await createTestApp(runner, {standalone: true});
      const tree = await runner.runSchematic('navigation', baseOptions, app);
      const componentContent = getFileContent(
        tree,
        '/projects/material/src/app/foo/foo.component.ts',
      );

      expect(tree.exists('/projects/material/src/app/app.module.ts')).toBe(false);
      expect(componentContent).toContain('standalone: true');
      expect(componentContent).toContain('imports: [');
    });
  });

  describe('style option', () => {
    it('should respect the option value', async () => {
      const tree = await runner.runSchematic(
        'navigation',
        {style: 'scss', ...baseOptions},
        await createTestApp(runner),
      );
      expect(tree.files).toContain('/projects/material/src/app/foo/foo.component.scss');
    });

    it('should fall back to the @schematics/angular:component option value', async () => {
      const tree = await runner.runSchematic(
        'navigation',
        baseOptions,
        await createTestApp(runner, {style: 'less'}),
      );
      expect(tree.files).toContain('/projects/material/src/app/foo/foo.component.less');
    });
  });

  describe('inlineStyle option', () => {
    it('should respect the option value', async () => {
      const tree = await runner.runSchematic(
        'navigation',
        {inlineStyle: true, ...baseOptions},
        await createTestApp(runner),
      );
      expect(tree.files).not.toContain('/projects/material/src/app/foo/foo.component.css');
    });

    it('should fall back to the @schematics/angular:component option value', async () => {
      const tree = await runner.runSchematic(
        'navigation',
        baseOptions,
        await createTestApp(runner, {inlineStyle: true}),
      );
      expect(tree.files).not.toContain('/projects/material/src/app/foo/foo.component.css');
    });
  });

  describe('inlineTemplate option', () => {
    it('should respect the option value', async () => {
      const tree = await runner.runSchematic(
        'navigation',
        {inlineTemplate: true, ...baseOptions},
        await createTestApp(runner),
      );
      expect(tree.files).not.toContain('/projects/material/src/app/foo/foo.component.html');
    });

    it('should fall back to the @schematics/angular:component option value', async () => {
      const tree = await runner.runSchematic(
        'navigation',
        baseOptions,
        await createTestApp(runner, {inlineTemplate: true}),
      );
      expect(tree.files).not.toContain('/projects/material/src/app/foo/foo.component.html');
    });
  });

  describe('skipTests option', () => {
    it('should respect the option value', async () => {
      const tree = await runner.runSchematic(
        'navigation',
        {skipTests: true, ...baseOptions},
        await createTestApp(runner),
      );
      expect(tree.files).not.toContain('/projects/material/src/app/foo/foo.component.spec.ts');
    });

    it('should fall back to the @schematics/angular:component option value', async () => {
      const tree = await runner.runSchematic(
        'navigation',
        baseOptions,
        await createTestApp(runner, {skipTests: true}),
      );
      expect(tree.files).not.toContain('/projects/material/src/app/foo/foo.component.spec.ts');
    });
  });
  describe('router option', () => {
    it('should respect the option value if routing true', async () => {
      const tree = await runner.runSchematic(
        'navigation',
        {routing: true, ...baseOptions},
        await createTestApp(runner),
      );
      const template = tree.readContent('/projects/material/src/app/foo/foo.component.html');
      expect(template).toContain('Link 1');
    });
    it('should respect the option value if routing false', async () => {
      const tree = await runner.runSchematic(
        'navigation',
        {routing: false, ...baseOptions},
        await createTestApp(runner),
      );
      const template = tree.readContent('/projects/material/src/app/foo/foo.component.html');
      expect(template).toContain('Link 1');
    });
  });
});


import {SchematicTestRunner} from '@angular-devkit/schematics/testing';
import {createTestApp, getFileContent} from '@angular/cdk/schematics/testing';
import {COLLECTION_PATH} from '../../paths';
import {Schema} from './schema';

describe('material-table-schematic', () => {
  let runner: SchematicTestRunner;

  const baseOptions: Schema = {
    name: 'foo',
    project: 'material',
  };

  beforeEach(() => {
    runner = new SchematicTestRunner('schematics', COLLECTION_PATH);
  });

  it('should create table files and add them to module', async () => {
    const app = await createTestApp(runner, {standalone: false});
    const tree = await runner.runSchematic('table', baseOptions, app);
    const files = tree.files;

    expect(files).toContain('/projects/material/src/app/foo/foo.component.css');
    expect(files).toContain('/projects/material/src/app/foo/foo.component.html');
    expect(files).toContain('/projects/material/src/app/foo/foo.component.spec.ts');
    expect(files).toContain('/projects/material/src/app/foo/foo.component.ts');
    expect(files).toContain('/projects/material/src/app/foo/foo-datasource.ts');

    const moduleContent = getFileContent(tree, '/projects/material/src/app/app.module.ts');
    expect(moduleContent).toMatch(/import.*Foo.*from '.\/foo\/foo.component'/);
    expect(moduleContent).toMatch(/declarations:\s*\[[^\]]+?,\r?\n\s+FooComponent\r?\n/m);

    const datasourceContent = getFileContent(
      tree,
      '/projects/material/src/app/foo/foo-datasource.ts',
    );

    expect(datasourceContent).toContain('FooItem');
    expect(datasourceContent).toContain('FooDataSource');

    const componentContent = getFileContent(
      tree,
      '/projects/material/src/app/foo/foo.component.ts',
    );

    expect(componentContent).toContain('FooDataSource');
  });

  it('should add table imports to module', async () => {
    const app = await createTestApp(runner, {standalone: false});
    const tree = await runner.runSchematic('table', baseOptions, app);
    const moduleContent = getFileContent(tree, '/projects/material/src/app/app.module.ts');

    expect(moduleContent).toContain('MatTableModule');
    expect(moduleContent).toContain('MatPaginatorModule');
    expect(moduleContent).toContain('MatSortModule');

    expect(moduleContent).toContain(`import { MatTableModule } from '@angular/material/table';`);
    expect(moduleContent).toContain(`import { MatSortModule } from '@angular/material/sort';`);
    expect(moduleContent).toContain(
      `import { MatPaginatorModule } from '@angular/material/paginator';`,
    );
  });

  it('should throw if no name has been specified', async () => {
    const appTree = await createTestApp(runner);

    await expectAsync(
      runner.runSchematic('table', {project: 'material'}, appTree),
    ).toBeRejectedWithError(/required property 'name'/);
  });

  describe('standalone option', () => {
    it('should generate a standalone component', async () => {
      const app = await createTestApp(runner, {standalone: false});
      const tree = await runner.runSchematic('table', {...baseOptions, standalone: true}, app);
      const module = getFileContent(tree, '/projects/material/src/app/app.module.ts');
      const component = getFileContent(tree, '/projects/material/src/app/foo/foo.component.ts');
      const requiredModules = ['MatTableModule', 'MatPaginatorModule', 'MatSortModule'];

      requiredModules.forEach(name => {
        expect(module).withContext('Module should not import dependencies').not.toContain(name);
        expect(component).withContext('Component should import dependencies').toContain(name);
      });

      expect(module).not.toContain('FooComponent');
      expect(component).toContain('standalone: true');
      expect(component).toContain('imports: [');
    });

    it('should infer the standalone option from the project structure', async () => {
      const app = await createTestApp(runner, {standalone: true});
      const tree = await runner.runSchematic('table', baseOptions, app);
      const componentContent = getFileContent(
        tree,
        '/projects/material/src/app/foo/foo.component.ts',
      );

      expect(tree.exists('/projects/material/src/app/app.module.ts')).toBe(false);
      expect(componentContent).toContain('standalone: true');
      expect(componentContent).toContain('imports: [');
    });
  });

  describe('style option', () => {
    it('should respect the option value', async () => {
      const tree = await runner.runSchematic(
        'table',
        {style: 'scss', ...baseOptions},
        await createTestApp(runner),
      );
      expect(tree.files).toContain('/projects/material/src/app/foo/foo.component.scss');
    });

    it('should fall back to the @schematics/angular:component option value', async () => {
      const tree = await runner.runSchematic(
        'table',
        baseOptions,
        await createTestApp(runner, {style: 'less'}),
      );
      expect(tree.files).toContain('/projects/material/src/app/foo/foo.component.less');
    });
  });

  describe('inlineStyle option', () => {
    it('should respect the option value', async () => {
      const tree = await runner.runSchematic(
        'table',
        {inlineStyle: true, ...baseOptions},
        await createTestApp(runner),
      );
      expect(tree.files).not.toContain('/projects/material/src/app/foo/foo.component.css');
    });

    it('should fall back to the @schematics/angular:component option value', async () => {
      const tree = await runner.runSchematic(
        'table',
        baseOptions,
        await createTestApp(runner, {inlineStyle: true}),
      );
      expect(tree.files).not.toContain('/projects/material/src/app/foo/foo.component.css');
    });
  });

  describe('inlineTemplate option', () => {
    it('should respect the option value', async () => {
      const tree = await runner.runSchematic(
        'table',
        {inlineTemplate: true, ...baseOptions},
        await createTestApp(runner),
      );
      expect(tree.files).not.toContain('/projects/material/src/app/foo/foo.component.html');
    });

    it('should fall back to the @schematics/angular:component option value', async () => {
      const tree = await runner.runSchematic(
        'table',
        baseOptions,
        await createTestApp(runner, {inlineTemplate: true}),
      );
      expect(tree.files).not.toContain('/projects/material/src/app/foo/foo.component.html');
    });
  });

  describe('skipTests option', () => {
    it('should respect the option value', async () => {
      const tree = await runner.runSchematic(
        'table',
        {skipTests: true, ...baseOptions},
        await createTestApp(runner),
      );
      expect(tree.files).not.toContain('/projects/material/src/app/foo/foo.component.spec.ts');
    });

    it('should fall back to the @schematics/angular:component option value', async () => {
      const tree = await runner.runSchematic(
        'table',
        baseOptions,
        await createTestApp(runner, {skipTests: true}),
      );
      expect(tree.files).not.toContain('/projects/material/src/app/foo/foo.component.spec.ts');
    });
  });
});


import { AfterViewInit, Component, ViewChild<% if(!!viewEncapsulation) { %>, ViewEncapsulation<% }%><% if(changeDetection !== 'Default') { %>, ChangeDetectionStrategy<% }%> } from '@angular/core';
import { <% if(standalone) { %>MatTableModule, <% } %>MatTable } from '@angular/material/table';
import { <% if(standalone) { %>MatPaginatorModule, <% } %>MatPaginator } from '@angular/material/paginator';
import { <% if(standalone) { %>MatSortModule, <% } %>MatSort } from '@angular/material/sort';
import { <%= classify(name) %>DataSource, <%= classify(name) %>Item } from './<%= dasherize(name) %>-datasource';

@Component({
  selector: '<%= selector %>',<% if(inlineTemplate) { %>
  template: `
    <%= indentTextContent(resolvedFiles.template, 4) %>
  `,<% } else { %>
  templateUrl: './<%= dasherize(name) %>.component.html',<% } if(inlineStyle) { %>
  styles: `
    <%= indentTextContent(resolvedFiles.stylesheet, 4) %>
  `<% } else { %>
  styleUrl: './<%= dasherize(name) %>.component.<%= style %>'<% } %><% if(!!viewEncapsulation) { %>,
  encapsulation: ViewEncapsulation.<%= viewEncapsulation %><% } if (changeDetection !== 'Default') { %>,
  changeDetection: ChangeDetectionStrategy.<%= changeDetection %><% } %><% if(standalone) { %>,
  standalone: true,
  imports: [MatTableModule, MatPaginatorModule, MatSortModule]<% } %>
})
export class <%= classify(name) %>Component implements AfterViewInit {
  @ViewChild(MatPaginator) paginator!: MatPaginator;
  @ViewChild(MatSort) sort!: MatSort;
  @ViewChild(MatTable) table!: MatTable<<%= classify(name) %>Item>;
  dataSource = new <%= classify(name) %>DataSource();

  /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
  displayedColumns = ['id', 'name'];

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


import { DataSource } from '@angular/cdk/collections';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { map } from 'rxjs/operators';
import { Observable, of as observableOf, merge } from 'rxjs';

// TODO: Replace this with your own data model type
export interface <%= classify(name) %>Item {
  name: string;
  id: number;
}

// TODO: replace this with real data from your application
const EXAMPLE_DATA: <%= classify(name) %>Item[] = [
  {id: 1, name: 'Hydrogen'},
  {id: 2, name: 'Helium'},
  {id: 3, name: 'Lithium'},
  {id: 4, name: 'Beryllium'},
  {id: 5, name: 'Boron'},
  {id: 6, name: 'Carbon'},
  {id: 7, name: 'Nitrogen'},
  {id: 8, name: 'Oxygen'},
  {id: 9, name: 'Fluorine'},
  {id: 10, name: 'Neon'},
  {id: 11, name: 'Sodium'},
  {id: 12, name: 'Magnesium'},
  {id: 13, name: 'Aluminum'},
  {id: 14, name: 'Silicon'},
  {id: 15, name: 'Phosphorus'},
  {id: 16, name: 'Sulfur'},
  {id: 17, name: 'Chlorine'},
  {id: 18, name: 'Argon'},
  {id: 19, name: 'Potassium'},
  {id: 20, name: 'Calcium'},
];

/**
 * Data source for the <%= classify(name) %> view. This class should
 * encapsulate all logic for fetching and manipulating the displayed data
 * (including sorting, pagination, and filtering).
 */
export class <%= classify(name) %>DataSource extends DataSource<<%= classify(name) %>Item> {
  data: <%= classify(name) %>Item[] = EXAMPLE_DATA;
  paginator: MatPaginator | undefined;
  sort: MatSort | undefined;

  constructor() {
    super();
  }

  /**
   * Connect this data source to the table. The table will only update when
   * the returned stream emits new items.
   * @returns A stream of the items to be rendered.
   */
  connect(): Observable<<%= classify(name) %>Item[]> {
    if (this.paginator && this.sort) {
      // Combine everything that affects the rendered data into one update
      // stream for the data-table to consume.
      return merge(observableOf(this.data), this.paginator.page, this.sort.sortChange)
        .pipe(map(() => {
          return this.getPagedData(this.getSortedData([...this.data ]));
        }));
    } else {
      throw Error('Please set the paginator and sort on the data source before connecting.');
    }
  }

  /**
   *  Called when the table is being destroyed. Use this function, to clean up
   * any open connections or free any held resources that were set up during connect.
   */
  disconnect(): void {}

  /**
   * Paginate the data (client-side). If you're using server-side pagination,
   * this would be replaced by requesting the appropriate data from the server.
   */
  private getPagedData(data: <%= classify(name) %>Item[]): <%= classify(name) %>Item[] {
    if (this.paginator) {
      const startIndex = this.paginator.pageIndex * this.paginator.pageSize;
      return data.splice(startIndex, this.paginator.pageSize);
    } else {
      return data;
    }
  }

  /**
   * Sort the data (client-side). If you're using server-side sorting,
   * this would be replaced by requesting the appropriate data from the server.
   */
  private getSortedData(data: <%= classify(name) %>Item[]): <%= classify(name) %>Item[] {
    if (!this.sort || !this.sort.active || this.sort.direction === '') {
      return data;
    }

    return data.sort((a, b) => {
      const isAsc = this.sort?.direction === 'asc';
      switch (this.sort?.active) {
        case 'name': return compare(a.name, b.name, isAsc);
        case 'id': return compare(+a.id, +b.id, isAsc);
        default: return 0;
      }
    });
  }
}

/** Simple sort comparator for example ID/Name columns (for client-side sorting). */
function compare(a: string | number, b: string | number, isAsc: boolean): number {
  return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
}


import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing';
import {createTestApp} from '@angular/cdk/schematics/testing';

import {runfiles} from '@bazel/runfiles';
import {compileString} from 'sass';
import * as path from 'path';
import {createLocalAngularPackageImporter} from '../../../../../tools/sass/local-sass-importer';
import {generateSCSSTheme} from './index';
import {Schema} from './schema';

// Note: For Windows compatibility, we need to resolve the directory paths through runfiles
// which are guaranteed to reside in the source tree.
const testDir = runfiles.resolvePackageRelative('../theme-color');
const packagesDir = path.join(runfiles.resolveWorkspaceRelative('src/cdk/_index.scss'), '../..');
const localPackageSassImporter = createLocalAngularPackageImporter(packagesDir);

describe('material-theme-color-schematic', () => {
  let runner: SchematicTestRunner;
  let testM3ThemePalette: Map<string, Map<number, string>>;

  /** Transpiles given Sass content into CSS. */
  function transpileTheme(content: string): string {
    return compileString(
      `
        ${content}

        html {
          @include mat.theme((
            color: (
              primary: $primary-palette,
              tertiary: $tertiary-palette,
              theme-type: light,
            ),
          ));

          @if mixin-exists(high-contrast-overrides) {
            & {
              @include high-contrast-overrides(light);
            }
          }

          &.dark-theme {
            @include mat.theme((
              color: (
                primary: $primary-palette,
                tertiary: $tertiary-palette,
                theme-type: dark,
              ),
            ));

            @if mixin-exists(high-contrast-overrides) {
              & {
                @include high-contrast-overrides(dark);
              }
            }
          }
        }
        `,
      {
        loadPaths: [testDir],
        importers: [localPackageSassImporter],
      },
    ).css.toString();
  }

  async function runM3ThemeSchematic(
    runner: SchematicTestRunner,
    options: Schema,
  ): Promise<UnitTestTree> {
    const app = await createTestApp(runner, {standalone: true});
    return runner.runSchematic('theme-color', options, app);
  }

  beforeEach(() => {
    testM3ThemePalette = getPaletteMap();
    runner = new SchematicTestRunner(
      '@angular/material',
      runfiles.resolveWorkspaceRelative('src/material/schematics/collection.json'),
    );
  });

  it('should throw error if given an incorrect color', async () => {
    try {
      await runM3ThemeSchematic(runner, {
        primaryColor: '#fffff',
      });
    } catch (e) {
      expect((e as Error).message).toBe(
        'Cannot parse the specified color #fffff. Please verify it is a hex color (ex. #ffffff or ffffff).',
      );
    }
  });

  it('should generate m3 theme file', async () => {
    const tree = await runM3ThemeSchematic(runner, {
      primaryColor: '#984061',
    });
    expect(tree.exists('_theme-colors.scss')).toBe(true);
  });

  it('should generate m3 theme file at specified path', async () => {
    const tree = await runM3ThemeSchematic(runner, {
      primaryColor: '#984061',
      directory: 'projects/',
    });
    expect(tree.exists('projects/_theme-colors.scss')).toBe(true);
  });

  it('should generate m3 theme file with correct indentation and formatting', async () => {
    const tree = await runM3ThemeSchematic(runner, {
      primaryColor: '#984061',
    });
    expect(tree.readText('_theme-colors.scss')).toEqual(getTestTheme());
  });

  it('should generate themes when provided a primary color', async () => {
    const tree = await runM3ThemeSchematic(runner, {
      primaryColor: '#984061',
    });

    const generatedSCSS = tree.readText('_theme-colors.scss');
    const testSCSS = generateSCSSTheme(
      testM3ThemePalette,
      'Color palettes are generated from primary: #984061',
    );

    expect(generatedSCSS).toBe(testSCSS);
    expect(transpileTheme(generatedSCSS)).toBe(transpileTheme(testSCSS));
  });

  it('should generate themes when provided primary and secondary colors', async () => {
    const tree = await runM3ThemeSchematic(runner, {
      primaryColor: '#984061',
      secondaryColor: '#984061',
    });

    const generatedSCSS = tree.readText('_theme-colors.scss');

    // Change test theme palette so that secondary is the same source color as
    // primary to match schematic inputs
    let testPalette = testM3ThemePalette;
    testPalette.set('secondary', testM3ThemePalette.get('primary')!);

    const testSCSS = generateSCSSTheme(
      testPalette,
      'Color palettes are generated from primary: #984061, secondary: #984061',
    );

    expect(generatedSCSS).toBe(testSCSS);
    expect(transpileTheme(generatedSCSS)).toBe(transpileTheme(testSCSS));
  });

  it('should generate themes when provided primary, secondary, and tertiary colors', async () => {
    const tree = await runM3ThemeSchematic(runner, {
      primaryColor: '#984061',
      secondaryColor: '#984061',
      tertiaryColor: '#984061',
    });

    const generatedSCSS = tree.readText('_theme-colors.scss');

    // Change test theme palette so that secondary and tertiary are the same
    // source color as primary to match schematic inputs
    let testPalette = testM3ThemePalette;
    testPalette.set('secondary', testM3ThemePalette.get('primary')!);
    testPalette.set('tertiary', testM3ThemePalette.get('primary')!);

    const testSCSS = generateSCSSTheme(
      testPalette,
      'Color palettes are generated from primary: #984061, secondary: #984061, tertiary: #984061',
    );

    expect(generatedSCSS).toBe(testSCSS);
    expect(transpileTheme(generatedSCSS)).toBe(transpileTheme(testSCSS));
  });

  it('should generate themes when provided a primary, secondary, tertiary, and neutral colors', async () => {
    const tree = await runM3ThemeSchematic(runner, {
      primaryColor: '#984061',
      secondaryColor: '#984061',
      tertiaryColor: '#984061',
      neutralColor: '#984061',
    });

    const generatedSCSS = tree.readText('_theme-colors.scss');

    // Change test theme palette so that secondary, tertiary, and neutral are
    // the same source color as primary to match schematic inputs
    let testPalette = testM3ThemePalette;
    testPalette.set('secondary', testM3ThemePalette.get('primary')!);
    testPalette.set('tertiary', testM3ThemePalette.get('primary')!);

    // Neutral's tonal palette has additional tones as opposed to the other color palettes.
    let neutralPalette = new Map(testM3ThemePalette.get('primary')!);
    neutralPalette.set(4, '#26000f');
    neutralPalette.set(6, '#2f0015');
    neutralPalette.set(12, '#460022');
    neutralPalette.set(17, '#55082c');
    neutralPalette.set(22, '#631637');
    neutralPalette.set(24, '#691a3c');
    neutralPalette.set(87, '#ffcdda');
    neutralPalette.set(92, '#ffe1e8');
    neutralPalette.set(94, '#ffe8ed');
    neutralPalette.set(96, '#fff0f2');
    testPalette.set('neutral', neutralPalette);

    const testSCSS = generateSCSSTheme(
      testPalette,
      'Color palettes are generated from primary: #984061, secondary: #984061, tertiary: #984061, neutral: #984061',
    );

    expect(generatedSCSS).toBe(testSCSS);
    expect(transpileTheme(generatedSCSS)).toBe(transpileTheme(testSCSS));
  });

  it('should be able to generate high contrast overrides mixin', async () => {
    const tree = await runM3ThemeSchematic(runner, {
      primaryColor: '#984061',
      includeHighContrast: true,
    });

    const generatedSCSS = tree.readText('_theme-colors.scss');

    expect(generatedSCSS).toContain(`@mixin high-contrast-overrides`);
  });


function getTestTheme() {
  return `// This file was generated by running 'ng generate @angular/material:theme-color'.
// Proceed with caution if making changes to this file.

@use 'sass:map';
@use '@angular/material' as mat;

// Note: Color palettes are generated from primary: #984061
$_palettes: (
  primary: (
    0: #000000,
    10: #3e001d,
    20: #5e1133,
    25: #6c1d3e,
    30: #7b2949,
    35: #893455,
    40: #984061,
    50: #b6587a,
    60: #d57194,
    70: #f48bae,
    80: #ffb0c8,
    90: #ffd9e2,
    95: #ffecf0,
    98: #fff8f8,
    99: #fffbff,
    100: #ffffff,
  ),
  secondary: (
    0: #000000,
    10: #31101d,
    20: #4a2531,
    25: #56303c,
    30: #633b48,
    35: #704653,
    40: #7e525f,
    50: #996a78,
    60: #b58392,
    70: #d29dac,
    80: #efb8c7,
    90: #ffd9e2,
    95: #ffecf0,
    98: #fff8f8,
    99: #fffbff,
    100: #ffffff,
  ),
  tertiary: (
    0: #000000,
    10: #331200,
    20: #532200,
    25: #642a00,
    30: #763300,
    35: #883d03,
    40: #974810,
    50: #b66028,
    60: #d6783e,
    70: #f69256,
    80: #ffb68e,
    90: #ffdbc9,
    95: #ffede5,
    98: #fff8f6,
    99: #fffbff,
    100: #ffffff,
  ),
  neutral: (
    0: #000000,
    10: #22191c,
    20: #372e30,
    25: #43393b,
    30: #4f4446,
    35: #5b5052,
    40: #675b5e,
    50: #807477,
    60: #9b8d90,
    70: #b6a8aa,
    80: #d2c3c5,
    90: #efdfe1,
    95: #fdedef,
    98: #fff8f8,
    99: #fffbff,
    100: #ffffff,
    4: #140c0e,
    6: #191113,
    12: #261d20,
    17: #31282a,
    22: #3c3235,
    24: #413739,
    87: #e6d6d9,
    92: #f5e4e7,
    94: #faeaed,
    96: #fff0f2,
  ),
  neutral-variant: (
    0: #000000,
    10: #25181c,
    20: #3c2c31,
    25: #47373b,
    30: #534247,
    35: #604e52,
    40: #6c5a5e,
    50: #867277,
    60: #a18b90,
    70: #bca5ab,
    80: #d9c0c6,
    90: #f6dce2,
    95: #ffecf0,
    98: #fff8f8,
    99: #fffbff,
    100: #ffffff,
  ),
  error: (
    0: #000000,
    10: #410002,
    20: #690005,
    25: #7e0007,
    30: #93000a,
    35: #a80710,
    40: #ba1a1a,
    50: #de3730,
    60: #ff5449,
    70: #ff897d,
    80: #ffb4ab,
    90: #ffdad6,
    95: #ffedea,
    98: #fff8f7,
    99: #fffbff,
    100: #ffffff,
  ),
);

$_rest: (
  secondary: map.get($_palettes, secondary),
  neutral: map.get($_palettes, neutral),
  neutral-variant: map.get($_palettes,  neutral-variant),
  error: map.get($_palettes, error),
);

$primary-palette: map.merge(map.get($_palettes, primary), $_rest);
$tertiary-palette: map.merge(map.get($_palettes, tertiary), $_rest);`;
}


# Material 3 Custom Theme schematic

```shell
ng generate @angular/material:theme-color
```

This schematic allows users to create new Material 3 theme palettes based
on custom colors by using [Material Color Utilities](https://github.com/material-foundation/material-color-utilities).

The generated [color palettes](https://m3.material.io/styles/color/roles) are
optimized to have enough contrast to be more accessible. See [Science of Color Design](https://material.io/blog/science-of-color-design) for more information about Material's color design.

For more customization, custom colors can be also be provided for the
secondary, tertiary, and neutral palette colors. It is recommended to choose colors that
are contrastful, Material has more detailed guidance for [accessible design](https://m3.material.io/foundations/accessible-design/patterns).

The output of the schematic will create a file named `_theme-colors.scss` at the
specified directory or the project root with the generated palettes. The exported
palettes (`$primary-palette` and `$tertiary-palette`) can be provided to the `theme` mixin within your theme file to use the custom colors.

```scss
@use '@angular/material' as mat;
@use './path/to/my-theme'; // location of generated file

html {
  @include mat.theme(
    color: (
      primary: my-theme.$primary-palette,
      tertiary: my-theme.$tertiary-palette,
    ),
    typography: Roboto,
    density: 0,
  )
}
```

## High contrast override mixins
High contrast override theme mixins are also generated in the file if specified. These mixins
override the system level variables with high contrast equivalent values from your theme. This is
helpful for users who prefer more contrastful colors for either preference or accessibility reasons.

### Creating one theme for light and dark mode
As of v19, the `theme` mixin can create one theme that detects and adapts to a user if they have
light or dark theme with the [`light-dark` function](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/light-dark).

Apply the `high-contrast-overrides(color-scheme)` mixin wrapped inside `@media (prefers-contrast: more)`.

```scss
@use '@angular/material';
@use './path/to/my-theme'; // location of generated file

html {
  // Must specify color-scheme for theme mixin to automatically work
  color-scheme: light;

  // Create one theme that works automatically for light and dark theme
  @include material.theme((
    color: (
      primary: my-theme.$primary-palette,
      tertiary: my-theme.$tertiary-palette,
    ),
    typography: Roboto,
    density: 0,
  ));

  // Use high contrast values when users prefer contrast
  @media (prefers-contrast: more) {
    @include my-theme.high-contrast-overrides(color-scheme);
  }
}
```

### Creating separate themes for light and dark mode
You can manually define the light theme and dark theme separately. This is recommended if you need
granular control over when to show each specific theme in your application. Prior to v19, this was
the only way to create light and dark themes.

```scss
@use '@angular/material';
@use './path/to/my-theme'; // location of generated file

html {
  // Apply the light theme by default
  @include material.theme((
    color: (
      primary: my-theme.$primary-palette,
      tertiary: my-theme.$tertiary-palette,
      theme-type: light,
    ),
    typography: Roboto,
    density: 0,
  ));

  // Use high contrast light theme colors when users prefer contrast
  @media (prefers-contrast: more) {
    @include my-theme.high-contrast-overrides(light);
  }

  // Apply dark theme when users prefer a dark color scheme
  @media (prefers-color-scheme: dark) {
    @include material.theme((
      color: (
        primary: my-theme.$primary-palette,
        tertiary: my-theme.$tertiary-palette,
        theme-type: dark,
      ),
    ));

    // Use high contrast dark theme colors when users prefers a dark color scheme and contrast
    @media (prefers-contrast: more) {
      @include my-theme.high-contrast-overrides(dark);
    }
  }
}
```

## Options

### Required

* `primaryColor` - Color to use for app's primary color palette (Note: the other
palettes described in the Material 3 spec will be automatically chosen based on
your primary palette unless specified, to ensure a harmonious color combination).

### Optional

* `secondaryColor` - Color to use for app's secondary color palette. Defaults to
secondary color generated from Material based on the primary.
* `tertiaryColor` - Color to use for app's tertiary color palette. Defaults to
tertiary color generated from Material based on the primary.
* `neutralColor` - Color to use for app's neutral color palette. Defaults to
neutral color generated from Material based on the primary.
* `includeHighContrast` - Whether to add high contrast override mixins to generated
theme file. Developers can call the mixin when they want to show a high contrast version of their
theme. Defaults to false.
* `directory` - Relative path to a directory within the project that the
generated theme file should be created in. Defaults to the project root.


/**
 * @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 {Rule, SchematicContext, Tree} from '@angular-devkit/schematics';
import {Schema} from './schema';
import {
  argbFromHex,
  hexFromArgb,
  TonalPalette,
  Hct,
  DynamicScheme,
  DislikeAnalyzer,
  TemperatureCache,
} from '@material/material-color-utilities';

// For each color tonal palettes are created using the following hue tones. The
// tonal palettes then get used to create the different color roles (ex.
// on-primary) https://m3.material.io/styles/color/system/how-the-system-works
const HUE_TONES = [0, 10, 20, 25, 30, 35, 40, 50, 60, 70, 80, 90, 95, 98, 99, 100];
// Map of neutral hues to the previous/next hues that
// can be used to estimate them, in case they're missing.
const NEUTRAL_HUES = new Map<number, {prev: number; next: number}>([
  [4, {prev: 0, next: 10}],
  [6, {prev: 0, next: 10}],
  [12, {prev: 10, next: 20}],
  [17, {prev: 10, next: 20}],
  [22, {prev: 20, next: 25}],
  [24, {prev: 20, next: 25}],
  [87, {prev: 80, next: 90}],
  [92, {prev: 90, next: 95}],
  [94, {prev: 90, next: 95}],
  [96, {prev: 95, next: 98}],
]);

// Note: Some of the color tokens refer to additional hue tones, but this only
// applies for the neutral color palette (ex. surface container is neutral
// palette's 94 tone). https://m3.material.io/styles/color/static/baseline
const NEUTRAL_HUE_TONES = [...HUE_TONES, ...NEUTRAL_HUES.keys()];

/**
 * Gets color tonal palettes generated by Material from the provided color.
 * @param primaryPalette Tonal palette that represents primary.
 * @param secondaryPalette Tonal palette that represents secondary.
 * @param tertiaryPalette Tonal palette that represents tertiary.
 * @param neutralPalette Tonal palette that represents neutral.
 * @param neutralVariantPalette Tonal palette that represents neutral variant.
 * @param isDark Boolean to represent if the scheme is for a dark or light theme.
 * @param contrastLevel Number between -1 and 1 for the contrast level. 0 is the standard contrast
 * and 1 represents high contrast.
 * @returns Dynamic scheme for provided theme and contrast level
 */
function getMaterialDynamicScheme(
  primaryPalette: TonalPalette,
  secondaryPalette: TonalPalette,
  tertiaryPalette: TonalPalette,
  neutralPalette: TonalPalette,
  neutralVariantPalette: TonalPalette,
  isDark: boolean,
  contrastLevel: number,
): DynamicScheme {
  return new DynamicScheme({
    sourceColorArgb: primaryPalette.keyColor.toInt(),
    variant: 6, // Variant.FIDELITY, used number representation since enum is not accessible outside of @material/material-color-utilities
    contrastLevel: contrastLevel,
    isDark: isDark,
    primaryPalette: primaryPalette,
    secondaryPalette: secondaryPalette,
    tertiaryPalette: tertiaryPalette,
    neutralPalette: neutralPalette,
    neutralVariantPalette: neutralVariantPalette,
  });
}

/**
 * Gets the scss representation of the provided color palettes.
 * @param colorPalettes Map of colors and their hue tones and values.
 * @returns String of the color palettes scss.
 */
function getColorPalettesSCSS(colorPalettes: Map<string, Map<number, string>>): string {
  let scss = '(\n';
  for (const [variant, palette] of colorPalettes!.entries()) {
    scss += '  ' + variant + ': (\n';
    for (const [key, value] of palette.entries()) {
      scss += '    ' + key + ': ' + value + ',\n';
    }
    scss += '  ),\n';
  }
  scss += ');';
  return scss;
}

/**
 * Gets map of all the color tonal palettes with their tones and colors from provided palettes.
 * @param primaryPalette Tonal palette that represents primary.
 * @param secondaryPalette Tonal palette that represents secondary.
 * @param tertiaryPalette Tonal palette that represents tertiary.
 * @param neutralPalette Tonal palette that represents neutral.
 * @param neutralVariantPalette Tonal palette that represents neutral variant.
 * @param errorPalette Tonal palette that represents error.
 * @returns Map with the colors and their hue tones and values.
 */
function getMapFromColorTonalPalettes(
  primaryPalette: TonalPalette,
  secondaryPalette: TonalPalette,
  tertiaryPalette: TonalPalette,
  neutralPalette: TonalPalette,
  neutralVariantPalette: TonalPalette,
  errorPalette: TonalPalette,
) {
  const tonalPalettes = {
    primary: primaryPalette,
    secondary: secondaryPalette,
    tertiary: tertiaryPalette,
    neutral: neutralPalette,
    neutralVariant: neutralVariantPalette,
    error: errorPalette,
  };
  const palettes: Map<string, Map<number, string>> = new Map();
  for (const [key, palette] of Object.entries(tonalPalettes)) {
    const paletteKey = key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
    const tones = paletteKey === 'neutral' ? NEUTRAL_HUE_TONES : HUE_TONES;
    const colorPalette: Map<number, string> = new Map();
    for (const tone of tones) {
      const color = hexFromArgb(palette.tone(tone));
      colorPalette.set(tone, color);
    }
    palettes.set(paletteKey, colorPalette);
  }
  return palettes;
}

/**
 * Gets the generated scss from the provided color palettes and theme types.
 * @param colorPalettes Map of colors and their hue tones and values.
 * @param colorComment Comment with original hex colors used to generate palettes.
 * @returns String of the generated theme scss.
 */
export function generateSCSSTheme(
  colorPalettes: Map<string, Map<number, string>>,
  colorComment: string,
): string {
  let scss = [
    "// This file was generated by running 'ng generate @angular/material:theme-color'.",
    '// Proceed with caution if making changes to this file.',
    '',
    "@use 'sass:map';",
    "@use '@angular/material' as mat;",
    '',
    '// Note: ' + colorComment,
    '$_palettes: ' + getColorPalettesSCSS(colorPalettes),
    '',
    '$_rest: (',
    '  secondary: map.get($_palettes, secondary),',
    '  neutral: map.get($_palettes, neutral),',
    '  neutral-variant: map.get($_palettes,  neutral-variant),',
    '  error: map.get($_palettes, error),',
    ');',
    '',
    '$primary-palette: map.merge(map.get($_palettes, primary), $_rest);',
    '$tertiary-palette: map.merge(map.get($_palettes, tertiary), $_rest);',
  ];

  return scss.join('\n');
}

/**
 * Gets map of system variables and their high contrast values.
 * @param primaryPalette Tonal palette that represents primary.
 * @param secondaryPalette Tonal palette that represents secondary.
 * @param tertiaryPalette Tonal palette that represents tertiary.
 * @param neutralPalette Tonal palette that represents neutral.
 * @param neutralVariantPalette Tonal palette that represents neutral variant.
 * @param isDark Boolean to represent if the scheme is for a dark or light theme.
 * @returns Map of system variables names and their high contrast values.
 */


/**
 * @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
 */

/** Create custom theme for the given application configuration. */
export function createCustomTheme(name: string = 'app') {
  return `
// Custom Theming for Angular Material
// For more information: https://material.angular.io/guide/theming
@use '@angular/material' as mat;
// Plus imports for other components in your app.

// Define the theme object.
$${name}-theme: mat.define-theme((
  color: (
    theme-type: light,
    primary: mat.$azure-palette,
    tertiary: mat.$blue-palette,
  ),
  density: (
    scale: 0,
  )
));

// Include theme styles for core and each component used in your app.
// Alternatively, you can import and @include the theme mixins for each component
// that you are using.
:root {
  @include mat.all-component-themes($${name}-theme);
}

// Comment out the line below if you want to use the pre-defined typography utility classes.
// For more information: https://material.angular.io/guide/typography#using-typography-styles-in-your-application.
// @include mat.typography-hierarchy($${name}-theme);

// Comment out the line below if you want to use the deprecated \`color\` inputs.
// @include mat.color-variants-backwards-compatibility($${name}-theme);
`;
}


/**
 * @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 {Rule, SchematicsException, Tree} from '@angular-devkit/schematics';
import {
  appendHtmlElementToHead,
  getProjectFromWorkspace,
  getProjectIndexFiles,
} from '@angular/cdk/schematics';
import {getWorkspace} from '@schematics/angular/utility/workspace';
import {Schema} from '../schema';

/** Adds the Material Design fonts to the index HTML file. */
export function addFontsToIndex(options: Schema): Rule {
  return async (host: Tree) => {
    const workspace = await getWorkspace(host);
    const project = getProjectFromWorkspace(workspace, options.project);
    const projectIndexFiles = getProjectIndexFiles(project);

    if (!projectIndexFiles.length) {
      throw new SchematicsException('No project index HTML file could be found.');
    }

    const fonts = [
      'https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap',
      'https://fonts.googleapis.com/icon?family=Material+Icons',
    ];

    projectIndexFiles.forEach(indexFilePath => {
      fonts.forEach(font => {
        appendHtmlElementToHead(host, indexFilePath, `<link href="${font}" rel="stylesheet">`);
      });
    });
  };
}


extends VBoxContainer

const ROT_SPEED = 0.003
const ZOOM_SPEED = 0.5
const MAIN_BUTTONS = MOUSE_BUTTON_MASK_LEFT | MOUSE_BUTTON_MASK_MIDDLE | MOUSE_BUTTON_MASK_RIGHT

@export var camera: Camera3D
@export var camera_holder: Node3D
@export var rotation_x: Node3D
@export var node_to_move: Node3D
@export var rigid_body: RigidBody3D

@onready var zoom := camera.position.z
var base_height: int = ProjectSettings.get_setting("display/window/size/viewport_height")

@onready var rot_x := rotation_x.rotation.x
@onready var rot_y := camera_holder.rotation.y

func _ready() -> void:
	if OS.has_feature("double"):
		%HelpLabel.text = "Double precision is enabled in this engine build.\nNo shaking should occur at high coordinate levels\n(±65,536 or more on any axis)."
		%HelpLabel.add_theme_color_override("font_color", Color(0.667, 1, 0.667))

func _process(delta: float) -> void:
	%Coordinates.text = "X: [color=#fb9]%f[/color]\nY: [color=#bfa]%f[/color]\nZ: [color=#9cf]%f[/color]" % [node_to_move.position.x, node_to_move.position.y, node_to_move.position.z]
	if %IncrementX.button_pressed:
		node_to_move.position.x += 10_000 * delta
	if %IncrementY.button_pressed:
		node_to_move.position.y += 100_000 * delta
	if %IncrementZ.button_pressed:
		node_to_move.position.z += 1_000_000 * delta

func _input(event: InputEvent) -> void:
	if event is InputEventMouseButton:
		if event.button_index == MOUSE_BUTTON_WHEEL_UP:
			zoom -= ZOOM_SPEED
		if event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
			zoom += ZOOM_SPEED
		zoom = clampf(zoom, 4, 15)
		camera.position.z = zoom

	if event is InputEventMouseMotion and event.button_mask & MAIN_BUTTONS:
		# Compensate motion speed to be resolution-independent (based on the window height).
		var relative_motion: Vector2 = event.relative * DisplayServer.window_get_size().y / base_height
		rot_y -= relative_motion.x * ROT_SPEED
		rot_x -= relative_motion.y * ROT_SPEED
		rot_x = clampf(rot_x, -1.4, 0.16)
		camera_holder.transform.basis = Basis.from_euler(Vector3(0, rot_y, 0))
		rotation_x.transform.basis = Basis.from_euler(Vector3(rot_x, 0, 0))

func _on_go_to_button_pressed(x_position: int) -> void:
	if x_position == 0:
		# Reset all coordinates, not just X.
		node_to_move.position = Vector3.ZERO
	else:
		node_to_move.position.x = x_position

func _on_open_documentation_pressed() -> void:
	OS.shell_open("https://docs.godotengine.org/en/latest/tutorials/physics/large_world_coordinates.html")


# This node converts a 3D position to 2D using a 2.5D transformation matrix.
# The transformation of its 2D form is controlled by its 3D child.
@tool
@icon("res://addons/node25d/icons/node_25d_icon.png")
class_name Node25D
extends Node2D

# SCALE is the number of 2D units in one 3D unit. Ideally, but not necessarily, an integer.
const SCALE = 32

# Exported spatial position for editor usage.
@export var spatial_position: Vector3:
	get:
		# TODO: Manually copy the code from this method.
		return get_spatial_position()
	set(value):
		# TODO: Manually copy the code from this method.
		set_spatial_position(value)

# GDScript throws errors when Basis25D is its own structure.
# There is a broken implementation in a hidden folder.
# https://github.com/godotengine/godot/issues/21461
# https://github.com/godotengine/godot-proposals/issues/279
var _basisX: Vector2
var _basisY: Vector2
var _basisZ: Vector2

# Cache the spatial stuff for internal use.
var _spatial_position: Vector3
var _spatial_node: Node3D

# These are separated in case anyone wishes to easily extend Node25D.
func _ready():
	Node25D_ready()

func _process(_delta):
	Node25D_process()

# Call this method in _ready, or before Node25D_process is first ran.
func Node25D_ready():
	_spatial_node = get_child(0)
	# Changing the values here will change the default for all Node25D instances.
	_basisX = SCALE * Vector2(1, 0)
	_basisY = SCALE * Vector2(0, -0.70710678118)
	_basisZ = SCALE * Vector2(0, 0.70710678118)

# Call this method in _process, or whenever the position of this object changes.
func Node25D_process():
	_check_view_mode()
	if _spatial_node == null:
		return
	_spatial_position = _spatial_node.position

	var flat_pos = _spatial_position.x * _basisX
	flat_pos += _spatial_position.y * _basisY
	flat_pos += _spatial_position.z * _basisZ

	global_position = flat_pos

func get_basis():
	return [_basisX, _basisY, _basisZ]

func get_spatial_position():
	if not _spatial_node:
		_spatial_node = get_child(0)
	return _spatial_node.position

func set_spatial_position(value):
	_spatial_position = value
	if _spatial_node:
		_spatial_node.position = value
	elif get_child_count() > 0:
		_spatial_node = get_child(0)

# Change the basis based on the view_mode_index argument.
# This can be changed or removed in actual games where you only need one view mode.
func set_view_mode(view_mode_index):
	match view_mode_index:
		0:  # 45 Degrees
			_basisX = SCALE * Vector2(1, 0)
			_basisY = SCALE * Vector2(0, -0.70710678118)
			_basisZ = SCALE * Vector2(0, 0.70710678118)
		1:  # Isometric
			_basisX = SCALE * Vector2(0.86602540378, 0.5)
			_basisY = SCALE * Vector2(0, -1)
			_basisZ = SCALE * Vector2(-0.86602540378, 0.5)
		2:  # Top Down
			_basisX = SCALE * Vector2(1, 0)
			_basisY = SCALE * Vector2(0, 0)
			_basisZ = SCALE * Vector2(0, 1)
		3:  # Front Side
			_basisX = SCALE * Vector2(1, 0)
			_basisY = SCALE * Vector2(0, -1)
			_basisZ = SCALE * Vector2(0, 0)
		4:  # Oblique Y
			_basisX = SCALE * Vector2(1, 0)
			_basisY = SCALE * Vector2(-0.70710678118, -0.70710678118)
			_basisZ = SCALE * Vector2(0, 1)
		5:  # Oblique Z
			_basisX = SCALE * Vector2(1, 0)
			_basisY = SCALE * Vector2(0, -1)
			_basisZ = SCALE * Vector2(-0.70710678118, 0.70710678118)

# Check if anyone presses the view mode buttons and change the basis accordingly.
# This can be changed or removed in actual games where you only need one view mode.
func _check_view_mode():
	if not Engine.is_editor_hint():
		if Input.is_action_just_pressed(&"forty_five_mode"):
			set_view_mode(0)
		elif Input.is_action_just_pressed(&"isometric_mode"):
			set_view_mode(1)
		elif Input.is_action_just_pressed(&"top_down_mode"):
			set_view_mode(2)
		elif Input.is_action_just_pressed(&"front_side_mode"):
			set_view_mode(3)
		elif Input.is_action_just_pressed(&"oblique_y_mode"):
			set_view_mode(4)
		elif Input.is_action_just_pressed(&"oblique_z_mode"):
			set_view_mode(5)

# Used by YSort25D
static func y_sort(a: Node25D, b: Node25D):
	return a._spatial_position.y < b._spatial_position.y

static func y_sort_slight_xz(a: Node25D, b: Node25D):
	var a_index = a._spatial_position.y + 0.001 * (a._spatial_position.x + a._spatial_position.z)
	var b_index = b._spatial_position.y + 0.001 * (b._spatial_position.x + b._spatial_position.z)
	return a_index < b_index


@tool
extends Control

var zoom_level := 0
var is_panning = false
var pan_center: Vector2
var viewport_center: Vector2
var view_mode_index := 0

var editor_interface: EditorInterface  # Set in node25d_plugin.gd
var moving = false

@onready var viewport_2d = $Viewport2D
@onready var viewport_overlay = $ViewportOverlay
@onready var view_mode_button_group: ButtonGroup = $"../TopBar/ViewModeButtons/45Degree".button_group
@onready var zoom_label: Label = $"../TopBar/Zoom/ZoomPercent"
@onready var gizmo_25d_scene = preload("res://addons/node25d/main_screen/gizmo_25d.tscn")

func _ready() -> void:
	# Give Godot a chance to fully load the scene. Should take two frames.
	for i in 2:
		await get_tree().process_frame

	var edited_scene_root = get_tree().edited_scene_root
	if not edited_scene_root:
		# Godot hasn't finished loading yet, so try loading the plugin again.
		editor_interface.set_plugin_enabled("node25d", false)
		editor_interface.set_plugin_enabled("node25d", true)
		return
	# Alright, we're loaded up. Now check if we have a valid world and assign it.
	var world_2d = edited_scene_root.get_viewport().world_2d
	if world_2d == get_viewport().world_2d:
		return  # This is the MainScreen25D scene opened in the editor!
	viewport_2d.world_2d = world_2d

func _process(_delta: float) -> void:
	if not editor_interface:  # Something's not right... bail!
		return

	# View mode polling.
	var view_mode_changed_this_frame := false
	var new_view_mode := -1
	if view_mode_button_group.get_pressed_button():
		new_view_mode = view_mode_button_group.get_pressed_button().get_index()
	if view_mode_index != new_view_mode:
		view_mode_index = new_view_mode
		view_mode_changed_this_frame = true
		_recursive_change_view_mode(get_tree().edited_scene_root)

	# Zooming.
	if Input.is_mouse_button_pressed(MOUSE_BUTTON_WHEEL_UP):
		zoom_level += 1
	elif Input.is_mouse_button_pressed(MOUSE_BUTTON_WHEEL_DOWN):
		zoom_level -= 1
	var zoom := _get_zoom_amount()

	# SubViewport size.
	var vp_size := get_global_rect().size
	viewport_2d.size = vp_size
	viewport_overlay.size = vp_size

	# SubViewport transform.
	var viewport_trans := Transform2D.IDENTITY
	viewport_trans.x *= zoom
	viewport_trans.y *= zoom
	viewport_trans.origin = viewport_trans.basis_xform(viewport_center) + size / 2
	viewport_2d.canvas_transform = viewport_trans
	viewport_overlay.canvas_transform = viewport_trans

	# Delete unused gizmos.
	var selection := editor_interface.get_selection().get_selected_nodes()
	var gizmos := viewport_overlay.get_children()
	for gizmo in gizmos:
		var contains := false
		for selected in selection:
			if selected == gizmo.node_25d and not view_mode_changed_this_frame:
				contains = true
		if not contains:
			gizmo.queue_free()
	# Add new gizmos.
	for selected in selection:
		if selected is Node25D:
			_ensure_node25d_has_gizmo(selected, gizmos)
	# Update gizmo zoom.
	for gizmo in gizmos:
		gizmo.set_zoom(zoom)

func _ensure_node25d_has_gizmo(node: Node25D, gizmos: Array[Node]) -> void:
	var new = true
	for gizmo in gizmos:
		if node == gizmo.node_25d:
			return
	var gizmo = gizmo_25d_scene.instantiate()
	viewport_overlay.add_child(gizmo)
	gizmo.setup(node)

# This only accepts input when the mouse is inside of the 2.5D viewport.
func _gui_input(event: InputEvent) -> void:
	if event is InputEventMouseButton:
		if event.is_pressed():
			if event.button_index == MOUSE_BUTTON_WHEEL_UP:
				zoom_level += 1
				accept_event()
			elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
				zoom_level -= 1
				accept_event()
			elif event.button_index == MOUSE_BUTTON_MIDDLE:
				is_panning = true
				pan_center = viewport_center - event.position / _get_zoom_amount()
				accept_event()
			elif event.button_index == MOUSE_BUTTON_LEFT:
				var overlay_children := viewport_overlay.get_children()
				for overlay_child in overlay_children:
					overlay_child.wants_to_move = true
				accept_event()
		elif event.button_index == MOUSE_BUTTON_MIDDLE:
			is_panning = false
			accept_event()
		elif event.button_index == MOUSE_BUTTON_LEFT:
			var overlay_children := viewport_overlay.get_children()
			for overlay_child in overlay_children:
				overlay_child.wants_to_move = false
			accept_event()
	elif event is InputEventMouseMotion:
		if is_panning:
			viewport_center = pan_center + event.position / _get_zoom_amount()
			accept_event()

func _recursive_change_view_mode(current_node: Node) -> void:
	if not current_node:
		return

	if current_node.has_method("set_view_mode"):
		current_node.set_view_mode(view_mode_index)

	for child in current_node.get_children():
		_recursive_change_view_mode(child)

func _get_zoom_amount() -> float:
	const THIRTEENTH_ROOT_OF_2 = 1.05476607648
	var zoom_amount = pow(THIRTEENTH_ROOT_OF_2, zoom_level)
	zoom_label.text = str(round(zoom_amount * 1000) / 10) + "%"
	return zoom_amount

func _on_ZoomOut_pressed() -> void:
	zoom_level -= 1

func _on_ZoomIn_pressed() -> void:
	zoom_level += 1

func _on_ZoomReset_pressed() -> void:
	zoom_level = 0


# Handles Player-specific behavior like moving. We calculate such things with CharacterBody3D.
class_name PlayerMath25D  # No icon necessary
extends CharacterBody3D

var vertical_speed := 0.0
var isometric_controls := true

@onready var _parent_node25d: Node25D = get_parent()

func _physics_process(delta: float) -> void:
	if Input.is_action_pressed(&"exit"):
		get_tree().quit()

	if Input.is_action_just_pressed(&"view_cube_demo"):
		get_tree().change_scene_to_file("res://assets/cube/cube.tscn")
		return

	if Input.is_action_just_pressed(&"toggle_isometric_controls"):
		isometric_controls = not isometric_controls
	if Input.is_action_just_pressed(&"reset_position") or position.y <= -100:
		# Reset player position if the player fell down into the void.
		transform = Transform3D(Basis(), Vector3.UP * 0.5)
		vertical_speed = 0
	else:
		_horizontal_movement(delta)
		_vertical_movement(delta)

# Checks WASD and Shift for horizontal movement via move_and_slide.
func _horizontal_movement(_delta: float) -> void:
	var local_x := Vector3.RIGHT
	var local_z := Vector3.BACK

	if isometric_controls and is_equal_approx(Node25D.SCALE * 0.86602540378, _parent_node25d.get_basis()[0].x):
		local_x = Vector3(0.70710678118, 0, -0.70710678118)
		local_z = Vector3(0.70710678118, 0, 0.70710678118)

	# Gather player input and add directional movement to a Vector3 variable.
	var movement_vec2 := Input.get_vector(&"move_left", &"move_right", &"move_forward", &"move_back")
	var move_dir: Vector3 = local_x * movement_vec2.x + local_z * movement_vec2.y

	velocity = move_dir * 10
	if Input.is_action_pressed(&"movement_modifier"):
		velocity /= 2

	move_and_slide()

# Checks Jump and applies gravity and vertical speed via move_and_collide.
func _vertical_movement(delta: float) -> void:
	if Input.is_action_just_pressed(&"jump"):
		vertical_speed = 60

	vertical_speed -= delta * 240 # Gravity
	var k := move_and_collide(Vector3.UP * vertical_speed * delta)

	if k != null:
		vertical_speed = 0