diff --git a/src/cdk-experimental/popover-edit/popover-edit.spec.ts b/src/cdk-experimental/popover-edit/popover-edit.spec.ts index 789aacfbb033..cd1f666b72d3 100644 --- a/src/cdk-experimental/popover-edit/popover-edit.spec.ts +++ b/src/cdk-experimental/popover-edit/popover-edit.spec.ts @@ -62,6 +62,7 @@ const POPOVER_EDIT_DIRECTIVE_NAME = ` [cdkPopoverEdit]="nameEdit" [cdkPopoverEditColspan]="colspan" [cdkPopoverEditDisabled]="nameEditDisabled" + [cdkPopoverEditAriaLabel]="nameEditAriaLabel" `; const POPOVER_EDIT_DIRECTIVE_WEIGHT = `[cdkPopoverEdit]="weightEdit" cdkPopoverEditTabOut`; @@ -77,6 +78,7 @@ abstract class BaseTestComponent { preservedValues = new FormValueContainer(); nameEditDisabled = false; + nameEditAriaLabel: string | undefined = undefined; ignoreSubmitUnlessValid = true; clickOutBehavior: PopoverEditClickOutBehavior = 'close'; colspan: CdkPopoverEditColspan = {}; @@ -557,6 +559,22 @@ describe('CDK Popover Edit', () => { expect(component.lensIsOpen()).toBe(false); clearLeftoverTimers(); })); + + it('sets aria label and role dialog on the popup', fakeAsync(() => { + component.nameEditAriaLabel = 'Label of name!!'; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + + // Uses Enter to open the lens. + component.openLens(); + fixture.detectChanges(); + + expect(component.lensIsOpen()).toBe(true); + const dialogElem = component.getEditPane()!; + expect(dialogElem.getAttribute('aria-label')).toBe('Label of name!!'); + expect(dialogElem.getAttribute('role')).toBe('dialog'); + clearLeftoverTimers(); + })); }); describe('focus manipulation', () => { diff --git a/src/cdk-experimental/popover-edit/table-directives.ts b/src/cdk-experimental/popover-edit/table-directives.ts index 83ef6479167f..58775ee0f8ed 100644 --- a/src/cdk-experimental/popover-edit/table-directives.ts +++ b/src/cdk-experimental/popover-edit/table-directives.ts @@ -173,6 +173,7 @@ const POPOVER_EDIT_INPUTS = [ {name: 'context', alias: 'cdkPopoverEditContext'}, {name: 'colspan', alias: 'cdkPopoverEditColspan'}, {name: 'disabled', alias: 'cdkPopoverEditDisabled'}, + {name: 'ariaLabel', alias: 'cdkPopoverEditAriaLabel'}, ]; /** @@ -196,6 +197,9 @@ export class CdkPopoverEdit implements AfterViewInit, OnDestroy { */ context?: C; + /** Aria label to set on the popover dialog element. */ + ariaLabel?: string; + /** * Specifies that the popup should cover additional table cells before and/or after * this one. @@ -302,7 +306,10 @@ export class CdkPopoverEdit implements AfterViewInit, OnDestroy { }); this.initFocusTrap(); - this.overlayRef.overlayElement.setAttribute('aria-role', 'dialog'); + this.overlayRef.overlayElement.setAttribute('role', 'dialog'); + if (this.ariaLabel) { + this.overlayRef.overlayElement.setAttribute('aria-label', this.ariaLabel); + } this.overlayRef.detachments().subscribe(() => this.closeEditOverlay()); } diff --git a/src/cdk/dialog/dialog.md b/src/cdk/dialog/dialog.md index fb7e1c95e16b..b5b7ae84841d 100644 --- a/src/cdk/dialog/dialog.md +++ b/src/cdk/dialog/dialog.md @@ -159,6 +159,14 @@ If you're using a `TemplateRef` for your dialog content, the data is available i ``` +If you're using a `TemplateRef` and need to access the `DialogRef`, you can use the following: + +```html + + Hello, {{data.name}} + +``` + ### Accessibility diff --git a/src/cdk/drag-drop/directives/drag.ts b/src/cdk/drag-drop/directives/drag.ts index 8d0f5c120113..f4469556e027 100644 --- a/src/cdk/drag-drop/directives/drag.ts +++ b/src/cdk/drag-drop/directives/drag.ts @@ -30,6 +30,7 @@ import { AfterViewInit, inject, Injector, + numberAttribute, } from '@angular/core'; import {coerceElement, coerceNumberProperty} from '@angular/cdk/coercion'; import {BehaviorSubject, Observable, Observer, Subject, merge} from 'rxjs'; @@ -159,6 +160,13 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { */ @Input('cdkDragPreviewContainer') previewContainer: PreviewContainer; + /** + * If the parent of the dragged element has a `scale` transform, it can throw off the + * positioning when the user starts dragging. Use this input to notify the CDK of the scale. + */ + @Input({alias: 'cdkDragScale', transform: numberAttribute}) + scale: number = 1; + /** Emits when the user starts dragging the item. */ @Output('cdkDragStarted') readonly started: EventEmitter = new EventEmitter(); @@ -261,6 +269,11 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { if (dropContainer) { this._dragRef._withDropContainer(dropContainer._dropListRef); dropContainer.addItem(this); + + // The drop container reads this so we need to sync it here. + dropContainer._dropListRef.beforeStarted.pipe(takeUntil(this._destroyed)).subscribe(() => { + this._dragRef.scale = this.scale; + }); } this._syncInputs(this._dragRef); @@ -448,6 +461,7 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { ref.disabled = this.disabled; ref.lockAxis = this.lockAxis; + ref.scale = this.scale; ref.dragStartDelay = typeof dragStartDelay === 'object' && dragStartDelay ? dragStartDelay diff --git a/src/cdk/drag-drop/directives/drop-list-shared.spec.ts b/src/cdk/drag-drop/directives/drop-list-shared.spec.ts index 4e9a6bf2b32b..fef579b3891a 100644 --- a/src/cdk/drag-drop/directives/drop-list-shared.spec.ts +++ b/src/cdk/drag-drop/directives/drop-list-shared.spec.ts @@ -5006,6 +5006,7 @@ const DROP_ZONE_FIXTURE_TEMPLATE = ` [cdkDragBoundary]="boundarySelector" [cdkDragPreviewClass]="previewClass" [cdkDragPreviewContainer]="previewContainer" + [cdkDragScale]="scale" [style.height.px]="item.height" [style.margin-bottom.px]="item.margin" (cdkDragStarted)="startedSpy($event)" @@ -5041,6 +5042,7 @@ export class DraggableInDropZone implements AfterViewInit { previewContainer: PreviewContainer = 'global'; dropDisabled = signal(false); dropLockAxis = signal(undefined); + scale = 1; constructor(protected _elementRef: ElementRef) {} diff --git a/src/cdk/drag-drop/directives/single-axis-drop-list.spec.ts b/src/cdk/drag-drop/directives/single-axis-drop-list.spec.ts index 324ccc8b8776..28e8938b6c32 100644 --- a/src/cdk/drag-drop/directives/single-axis-drop-list.spec.ts +++ b/src/cdk/drag-drop/directives/single-axis-drop-list.spec.ts @@ -311,4 +311,29 @@ describe('Single-axis drop list', () => { dispatchMouseEvent(document, 'mouseup'); })); + + it('should lay out the elements correctly when scaled', fakeAsync(() => { + const fixture = createComponent(DraggableInDropZone); + fixture.componentInstance.scale = 0.5; + fixture.detectChanges(); + + const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement); + const {top, left} = items[0].getBoundingClientRect(); + + startDraggingViaMouse(fixture, items[0], left, top); + + const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement; + const target = items[1]; + const targetRect = target.getBoundingClientRect(); + + dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.top + 5); + fixture.detectChanges(); + + expect(placeholder.style.transform).toBe(`translate3d(0px, ${ITEM_HEIGHT * 2}px, 0px)`); + expect(target.style.transform).toBe(`translate3d(0px, ${-ITEM_HEIGHT * 2}px, 0px)`); + + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); + flush(); + })); }); diff --git a/src/cdk/drag-drop/directives/standalone-drag.spec.ts b/src/cdk/drag-drop/directives/standalone-drag.spec.ts index 4a7e14a8d9a8..33f0588260cf 100644 --- a/src/cdk/drag-drop/directives/standalone-drag.spec.ts +++ b/src/cdk/drag-drop/directives/standalone-drag.spec.ts @@ -1470,34 +1470,41 @@ describe('Standalone CdkDrag', () => { cleanup(); })); - it( - 'should update the free drag position if the user moves their pointer after the page ' + - 'is scrolled', - fakeAsync(() => { - const fixture = createComponent(StandaloneDraggable); - fixture.detectChanges(); + it('should update the free drag position if the user moves their pointer after the page is scrolled', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); - const cleanup = makeScrollable(); - const dragElement = fixture.componentInstance.dragElement.nativeElement; + const cleanup = makeScrollable(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; - expect(dragElement.style.transform).toBeFalsy(); - startDraggingViaMouse(fixture, dragElement, 0, 0); - dispatchMouseEvent(document, 'mousemove', 50, 100); - fixture.detectChanges(); + expect(dragElement.style.transform).toBeFalsy(); + startDraggingViaMouse(fixture, dragElement, 0, 0); + dispatchMouseEvent(document, 'mousemove', 50, 100); + fixture.detectChanges(); - expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); - scrollTo(0, 500); - dispatchFakeEvent(document, 'scroll'); - fixture.detectChanges(); - dispatchMouseEvent(document, 'mousemove', 50, 200); - fixture.detectChanges(); + scrollTo(0, 500); + dispatchFakeEvent(document, 'scroll'); + fixture.detectChanges(); + dispatchMouseEvent(document, 'mousemove', 50, 200); + fixture.detectChanges(); - expect(dragElement.style.transform).toBe('translate3d(50px, 700px, 0px)'); + expect(dragElement.style.transform).toBe('translate3d(50px, 700px, 0px)'); - cleanup(); - }), - ); + cleanup(); + })); + + it('should account for scale when moving the element', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.scale = 0.5; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(100px, 200px, 0px)'); + })); describe('with a handle', () => { it('should not be able to drag the entire element if it has a handle', fakeAsync(() => { @@ -1718,6 +1725,7 @@ describe('Standalone CdkDrag', () => { [cdkDragFreeDragPosition]="freeDragPosition" [cdkDragDisabled]="dragDisabled()" [cdkDragLockAxis]="dragLockAxis()" + [cdkDragScale]="scale" (cdkDragStarted)="startedSpy($event)" (cdkDragReleased)="releasedSpy($event)" (cdkDragEnded)="endedSpy($event)" @@ -1745,6 +1753,7 @@ class StandaloneDraggable { freeDragPosition?: {x: number; y: number}; dragDisabled = signal(false); dragLockAxis = signal(undefined); + scale = 1; } @Component({ diff --git a/src/cdk/drag-drop/drag-ref.ts b/src/cdk/drag-drop/drag-ref.ts index 2b751e373171..a8ed7bb55f5f 100644 --- a/src/cdk/drag-drop/drag-ref.ts +++ b/src/cdk/drag-drop/drag-ref.ts @@ -288,6 +288,12 @@ export class DragRef { /** Class to be added to the preview element. */ previewClass: string | string[] | undefined; + /** + * If the parent of the dragged element has a `scale` transform, it can throw off the + * positioning when the user starts dragging. Use this input to notify the CDK of the scale. + */ + scale: number = 1; + /** Whether starting to drag this element is disabled. */ get disabled(): boolean { return this._disabled || !!(this._dropContainer && this._dropContainer.disabled); @@ -1288,7 +1294,8 @@ export class DragRef { * @param y New transform value along the Y axis. */ private _applyRootElementTransform(x: number, y: number) { - const transform = getTransform(x, y); + const scale = 1 / this.scale; + const transform = getTransform(x * scale, y * scale); const styles = this._rootElement.style; // Cache the previous transform amount only after the first drag sequence, because diff --git a/src/cdk/drag-drop/sorting/single-axis-sort-strategy.ts b/src/cdk/drag-drop/sorting/single-axis-sort-strategy.ts index 28ed95c6517b..9d3ad3a99702 100644 --- a/src/cdk/drag-drop/sorting/single-axis-sort-strategy.ts +++ b/src/cdk/drag-drop/sorting/single-axis-sort-strategy.ts @@ -128,6 +128,8 @@ export class SingleAxisSortStrategy implements DropListSortStrategy { // Update the offset to reflect the new position. sibling.offset += offset; + const transformAmount = Math.round(sibling.offset * (1 / sibling.drag.scale)); + // Since we're moving the items with a `transform`, we need to adjust their cached // client rects to reflect their new position, as well as swap their positions in the cache. // Note that we shouldn't use `getBoundingClientRect` here to update the cache, because the @@ -136,13 +138,13 @@ export class SingleAxisSortStrategy implements DropListSortStrategy { // Round the transforms since some browsers will // blur the elements, for sub-pixel transforms. elementToOffset.style.transform = combineTransforms( - `translate3d(${Math.round(sibling.offset)}px, 0, 0)`, + `translate3d(${transformAmount}px, 0, 0)`, sibling.initialTransform, ); adjustDomRect(sibling.clientRect, 0, offset); } else { elementToOffset.style.transform = combineTransforms( - `translate3d(0, ${Math.round(sibling.offset)}px, 0)`, + `translate3d(0, ${transformAmount}px, 0)`, sibling.initialTransform, ); adjustDomRect(sibling.clientRect, offset, 0); diff --git a/src/material-experimental/popover-edit/popover-edit.spec.ts b/src/material-experimental/popover-edit/popover-edit.spec.ts index 070c3edc673d..45436648234a 100644 --- a/src/material-experimental/popover-edit/popover-edit.spec.ts +++ b/src/material-experimental/popover-edit/popover-edit.spec.ts @@ -53,6 +53,7 @@ const POPOVER_EDIT_DIRECTIVE_NAME = ` [matPopoverEdit]="nameEdit" [matPopoverEditColspan]="colspan" [matPopoverEditDisabled]="nameEditDisabled" + [matPopoverEditAriaLabel]="nameEditAriaLabel" `; const POPOVER_EDIT_DIRECTIVE_WEIGHT = `[matPopoverEdit]="weightEdit" matPopoverEditTabOut`; @@ -69,6 +70,7 @@ abstract class BaseTestComponent { preservedValues = new FormValueContainer(); nameEditDisabled = false; + nameEditAriaLabel: string | undefined = undefined; ignoreSubmitUnlessValid = true; clickOutBehavior: PopoverEditClickOutBehavior = 'close'; colspan: CdkPopoverEditColspan = {}; @@ -430,6 +432,22 @@ describe('Material Popover Edit', () => { expect(component.lensIsOpen()).toBe(false); clearLeftoverTimers(); })); + + it('sets aria label and role dialog on the popup', fakeAsync(() => { + component.nameEditAriaLabel = 'Label of name!!'; + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + + // Uses Enter to open the lens. + component.openLens(); + fixture.detectChanges(); + + expect(component.lensIsOpen()).toBe(true); + const dialogElem = component.getEditPane()!; + expect(dialogElem.getAttribute('aria-label')).toBe('Label of name!!'); + expect(dialogElem.getAttribute('role')).toBe('dialog'); + clearLeftoverTimers(); + })); }); describe('focus manipulation', () => { diff --git a/src/material-experimental/popover-edit/table-directives.ts b/src/material-experimental/popover-edit/table-directives.ts index d64bcecb7e51..5f5bf1a95d9e 100644 --- a/src/material-experimental/popover-edit/table-directives.ts +++ b/src/material-experimental/popover-edit/table-directives.ts @@ -26,6 +26,7 @@ const POPOVER_EDIT_INPUTS = [ {name: 'context', alias: 'matPopoverEditContext'}, {name: 'colspan', alias: 'matPopoverEditColspan'}, {name: 'disabled', alias: 'matPopoverEditDisabled'}, + {name: 'ariaLabel', alias: 'matPopoverEditAriaLabel'}, ]; const EDIT_PANE_CLASS = 'mat-edit-pane'; diff --git a/src/material/core/internal-form-field/internal-form-field.scss b/src/material/core/internal-form-field/internal-form-field.scss index cb4f7e701cbb..fb2191d05b34 100644 --- a/src/material/core/internal-form-field/internal-form-field.scss +++ b/src/material/core/internal-form-field/internal-form-field.scss @@ -1,12 +1,40 @@ -@use '@material/form-field/form-field' as mdc-form-field; -@use '@material/theme/custom-properties' as mdc-custom-properties; @use '../style/vendor-prefixes'; -@use '../mdc-helpers/mdc-helpers'; - -@include mdc-custom-properties.configure($emit-fallback-values: false, $emit-fallback-vars: false) { - @include mdc-form-field.static-styles($query: mdc-helpers.$mdc-base-styles-query); -} .mat-internal-form-field { @include vendor-prefixes.smooth-font(); + display: inline-flex; + align-items: center; + vertical-align: middle; + + & > label { + margin-left: 0; + margin-right: auto; + padding-left: 4px; + padding-right: 0; + order: 0; + } + + [dir='rtl'] & > label { + margin-left: auto; + margin-right: 0; + padding-left: 0; + padding-right: 4px; + } +} + +.mdc-form-field--align-end { + & > label { + margin-left: auto; + margin-right: 0; + padding-left: 0; + padding-right: 4px; + order: -1; + } + + [dir='rtl'] .mdc-form-field--align-end & label { + margin-left: 0; + margin-right: auto; + padding-left: 4px; + padding-right: 0; + } } diff --git a/src/material/list/BUILD.bazel b/src/material/list/BUILD.bazel index 5bd7a26b1467..ded4cec61b5d 100644 --- a/src/material/list/BUILD.bazel +++ b/src/material/list/BUILD.bazel @@ -42,6 +42,15 @@ sass_library( ], ) +sass_library( + name = "inherited_structure_scss_lib", + srcs = ["_list-inherited-structure.scss"], + deps = [ + "//src/cdk:sass_lib", + "//src/material/core:core_scss_lib", + ], +) + sass_library( name = "list_scss_lib", srcs = glob(["**/_*.scss"]), @@ -59,6 +68,7 @@ sass_binary( src = "list.scss", deps = [ ":hcm_indicator_scss_lib", + ":inherited_structure_scss_lib", "//:mdc_sass_lib", "//src/material/core:core_scss_lib", ], diff --git a/src/material/list/_list-inherited-structure.scss b/src/material/list/_list-inherited-structure.scss new file mode 100644 index 000000000000..f8dea90da71d --- /dev/null +++ b/src/material/list/_list-inherited-structure.scss @@ -0,0 +1,516 @@ +@use '@angular/cdk'; +@use '../core/style/vendor-prefixes'; +@use '../core/tokens/m2/mdc/list' as tokens-mdc-list; +@use '../core/tokens/token-utils'; + +// Includes the structural styles for the list that were inherited from MDC. +@mixin private-list-inherited-structural-styles { + $tokens: (tokens-mdc-list.$prefix, tokens-mdc-list.get-token-slots()); + + .mdc-list { + margin: 0; + padding: 8px 0; + list-style-type: none; + + &:focus { + outline: none; + } + } + + .mdc-list-item { + display: flex; + position: relative; + justify-content: flex-start; + overflow: hidden; + padding: 0; + align-items: stretch; + cursor: pointer; + padding-left: 16px; + padding-right: 16px; + + @include token-utils.use-tokens($tokens...) { + @include token-utils.create-token-slot(background-color, list-item-container-color); + @include token-utils.create-token-slot(border-radius, list-item-container-shape); + + &.mdc-list-item--selected { + @include token-utils.create-token-slot(background-color, + list-item-selected-container-color); + } + } + + &:focus { + outline: 0; + } + + &.mdc-list-item--disabled { + cursor: auto; + } + + &.mdc-list-item--with-one-line { + @include token-utils.use-tokens($tokens...) { + @include token-utils.create-token-slot(height, list-item-one-line-container-height); + } + + .mdc-list-item__start { + align-self: center; + margin-top: 0; + } + + .mdc-list-item__end { + align-self: center; + margin-top: 0; + } + } + + &.mdc-list-item--with-two-lines { + @include token-utils.use-tokens($tokens...) { + @include token-utils.create-token-slot(height, list-item-two-line-container-height); + } + + .mdc-list-item__start { + align-self: flex-start; + margin-top: 16px; + } + + .mdc-list-item__end { + align-self: center; + margin-top: 0; + } + } + + &.mdc-list-item--with-three-lines { + @include token-utils.use-tokens($tokens...) { + @include token-utils.create-token-slot(height, list-item-three-line-container-height); + } + + .mdc-list-item__start { + align-self: flex-start; + margin-top: 16px; + } + + .mdc-list-item__end { + align-self: flex-start; + margin-top: 16px; + } + } + + &.mdc-list-item--selected::before, + &.mdc-list-item--selected:focus::before, + &:not(.mdc-list-item--selected):focus::before { + position: absolute; + box-sizing: border-box; + width: 100%; + height: 100%; + top: 0; + left: 0; + border: 1px solid transparent; + border-radius: inherit; + content: ''; + pointer-events: none; + + @include cdk.high-contrast(active, off) { + border-color: CanvasText; + } + } + + &.mdc-list-item--selected:focus::before, + &.mdc-list-item--selected::before { + border-width: 3px; + border-style: double; + } + } + + a.mdc-list-item { + color: inherit; + text-decoration: none; + } + + .mdc-list-item__start { + fill: currentColor; + flex-shrink: 0; + pointer-events: none; + + @include token-utils.use-tokens($tokens...) { + .mdc-list-item--with-leading-icon & { + @include token-utils.create-token-slot(color, list-item-leading-icon-color); + @include token-utils.create-token-slot(width, list-item-leading-icon-size); + @include token-utils.create-token-slot(height, list-item-leading-icon-size); + margin-left: 16px; + margin-right: 32px; + } + + [dir='rtl'] .mdc-list-item--with-leading-icon & { + margin-left: 32px; + margin-right: 16px; + } + + .mdc-list-item--with-leading-icon:hover & { + @include token-utils.create-token-slot(color, list-item-hover-leading-icon-color); + } + + // This is the same in RTL, but we need the specificity. + .mdc-list-item--with-leading-avatar & { + @include token-utils.create-token-slot(width, list-item-leading-avatar-size); + @include token-utils.create-token-slot(height, list-item-leading-avatar-size); + margin-left: 16px; + margin-right: 16px; + border-radius: 50%; + } + + .mdc-list-item--with-leading-avatar &, + [dir='rtl'] .mdc-list-item--with-leading-avatar & { + margin-left: 16px; + margin-right: 16px; + border-radius: 50%; + } + } + } + + .mdc-list-item__end { + flex-shrink: 0; + pointer-events: none; + + @include token-utils.use-tokens($tokens...) { + .mdc-list-item--with-trailing-meta & { + @include token-utils.create-token-slot(font-family, + list-item-trailing-supporting-text-font); + @include token-utils.create-token-slot(line-height, + list-item-trailing-supporting-text-line-height); + @include token-utils.create-token-slot(font-size, + list-item-trailing-supporting-text-size); + @include token-utils.create-token-slot(font-weight, + list-item-trailing-supporting-text-weight); + @include token-utils.create-token-slot(letter-spacing, + list-item-trailing-supporting-text-tracking); + } + + .mdc-list-item--with-trailing-icon & { + @include token-utils.create-token-slot(color, list-item-trailing-icon-color); + @include token-utils.create-token-slot(width, list-item-trailing-icon-size); + @include token-utils.create-token-slot(height, list-item-trailing-icon-size); + } + + .mdc-list-item--with-trailing-icon:hover & { + @include token-utils.create-token-slot(color, list-item-hover-trailing-icon-color); + } + + // For some reason this has an increased specificity just for the `color`. + // Keeping it in place for now to reduce the amount of screenshot diffs. + .mdc-list-item.mdc-list-item--with-trailing-meta & { + @include token-utils.create-token-slot(color, list-item-trailing-supporting-text-color); + } + + .mdc-list-item--selected.mdc-list-item--with-trailing-icon & { + @include token-utils.create-token-slot(color, list-item-selected-trailing-icon-color); + } + } + } + + .mdc-list-item__content { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + align-self: center; + flex: 1; + pointer-events: none; + + .mdc-list-item--with-two-lines &, + .mdc-list-item--with-three-lines & { + align-self: stretch; + } + } + + .mdc-list-item__primary-text { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + + @include token-utils.use-tokens($tokens...) { + @include token-utils.create-token-slot(color, list-item-label-text-color); + @include token-utils.create-token-slot(font-family, list-item-label-text-font); + @include token-utils.create-token-slot(line-height, list-item-label-text-line-height); + @include token-utils.create-token-slot(font-size, list-item-label-text-size); + @include token-utils.create-token-slot(font-weight, list-item-label-text-weight); + @include token-utils.create-token-slot(letter-spacing, list-item-label-text-tracking); + + .mdc-list-item:hover & { + @include token-utils.create-token-slot(color, list-item-hover-label-text-color); + } + + .mdc-list-item:focus & { + @include token-utils.create-token-slot(color, list-item-focus-label-text-color); + } + } + + .mdc-list-item--with-two-lines &, + .mdc-list-item--with-three-lines & { + display: block; + margin-top: 0; + line-height: normal; + margin-bottom: -20px; + } + + .mdc-list-item--with-two-lines &::before, + .mdc-list-item--with-three-lines &::before { + display: inline-block; + width: 0; + height: 28px; + content: ''; + vertical-align: 0; + } + + .mdc-list-item--with-two-lines &::after, + .mdc-list-item--with-three-lines &::after { + display: inline-block; + width: 0; + height: 20px; + content: ''; + vertical-align: -20px; + } + } + + .mdc-list-item__secondary-text { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + display: block; + margin-top: 0; + + @include token-utils.use-tokens($tokens...) { + @include token-utils.create-token-slot(color, list-item-supporting-text-color); + @include token-utils.create-token-slot(font-family, list-item-supporting-text-font); + @include token-utils.create-token-slot(line-height, list-item-supporting-text-line-height); + @include token-utils.create-token-slot(font-size, list-item-supporting-text-size); + @include token-utils.create-token-slot(font-weight, list-item-supporting-text-weight); + @include token-utils.create-token-slot(letter-spacing, list-item-supporting-text-tracking); + } + + &::before { + display: inline-block; + width: 0; + height: 20px; + content: ''; + vertical-align: 0; + } + + .mdc-list-item--with-three-lines & { + white-space: normal; + line-height: 20px; + } + + .mdc-list-item--with-overline & { + white-space: nowrap; + line-height: auto; + } + } + + .mdc-list-item--with-leading-radio, + .mdc-list-item--with-leading-checkbox, + .mdc-list-item--with-leading-icon, + .mdc-list-item--with-leading-avatar { + &.mdc-list-item { + padding-left: 0; + padding-right: 16px; + + [dir='rtl'] & { + padding-left: 16px; + padding-right: 0; + } + } + + &.mdc-list-item--with-two-lines { + .mdc-list-item__primary-text { + display: block; + margin-top: 0; + line-height: normal; + margin-bottom: -20px; + + // This was used by MDC to set the text baseline. We should figure out a way to + // remove it, because it can introduce unnecessary whitespace at the beginning + // of the element. + &::before { + display: inline-block; + width: 0; + height: 32px; + content: ''; + vertical-align: 0; + } + + &::after { + display: inline-block; + width: 0; + height: 20px; + content: ''; + vertical-align: -20px; + } + } + + &.mdc-list-item--with-trailing-meta { + .mdc-list-item__end { + display: block; + margin-top: 0; + line-height: normal; + + &::before { + display: inline-block; + width: 0; + height: 32px; + content: ''; + vertical-align: 0; + } + } + } + } + } + + .mdc-list-item--with-trailing-icon { + &.mdc-list-item { + // This is the same in RTL, but we need the specificity. + &, [dir='rtl'] & { + padding-left: 0; + padding-right: 0; + } + } + + .mdc-list-item__end { + margin-left: 16px; + margin-right: 16px; + } + } + + .mdc-list-item--with-trailing-meta { + &.mdc-list-item { + padding-left: 16px; + padding-right: 0; + + [dir='rtl'] & { + padding-left: 0; + padding-right: 16px; + } + } + + .mdc-list-item__end { + @include vendor-prefixes.user-select(none); + margin-left: 28px; + margin-right: 16px; + + [dir='rtl'] & { + margin-left: 16px; + margin-right: 28px; + } + } + + &.mdc-list-item--with-three-lines .mdc-list-item__end, + &.mdc-list-item--with-two-lines .mdc-list-item__end { + display: block; + line-height: normal; + align-self: flex-start; + margin-top: 0; + + &::before { + display: inline-block; + width: 0; + height: 28px; + content: ''; + vertical-align: 0; + } + } + } + + .mdc-list-item--with-leading-radio, + .mdc-list-item--with-leading-checkbox { + .mdc-list-item__start { + margin-left: 8px; + margin-right: 24px; + + [dir='rtl'] & { + margin-left: 24px; + margin-right: 8px; + } + } + + &.mdc-list-item--with-two-lines { + .mdc-list-item__start { + align-self: flex-start; + margin-top: 8px; + } + } + } + + .mdc-list-item--with-trailing-radio, + .mdc-list-item--with-trailing-checkbox { + &.mdc-list-item { + padding-left: 16px; + padding-right: 0; + + [dir='rtl'] & { + padding-left: 0; + padding-right: 16px; + } + } + + &.mdc-list-item--with-leading-icon, + &.mdc-list-item--with-leading-avatar { + padding-left: 0; + + [dir='rtl'] & { + padding-right: 0; + } + } + + .mdc-list-item__end { + margin-left: 24px; + margin-right: 8px; + + [dir='rtl'] & { + margin-left: 8px; + margin-right: 24px; + } + } + + &.mdc-list-item--with-three-lines .mdc-list-item__end { + align-self: flex-start; + margin-top: 8px; + } + } + + .mdc-list-group__subheader { + margin: 0.75rem 16px; + } + + .mdc-list-item--disabled { + .mdc-list-item__start, + .mdc-list-item__content, + .mdc-list-item__end { + opacity: 1; + } + + .mdc-list-item__primary-text, + .mdc-list-item__secondary-text { + @include token-utils.use-tokens($tokens...) { + @include token-utils.create-token-slot(opacity, list-item-disabled-label-text-opacity); + } + } + + &.mdc-list-item--with-leading-icon .mdc-list-item__start { + @include token-utils.use-tokens($tokens...) { + @include token-utils.create-token-slot(color, list-item-disabled-leading-icon-color); + @include token-utils.create-token-slot(opacity, list-item-disabled-leading-icon-opacity); + } + } + + &.mdc-list-item--with-trailing-icon .mdc-list-item__end { + @include token-utils.use-tokens($tokens...) { + @include token-utils.create-token-slot(color, list-item-disabled-trailing-icon-color); + @include token-utils.create-token-slot(opacity, list-item-disabled-trailing-icon-opacity); + } + } + } + + .mat-mdc-list-item.mat-mdc-list-item-both-leading-and-trailing { + &, [dir='rtl'] & { + padding-left: 0; + padding-right: 0; + } + } +} diff --git a/src/material/list/_list-item-hcm-indicator.scss b/src/material/list/_list-item-hcm-indicator.scss index 488064bb58a2..90e80eb13846 100644 --- a/src/material/list/_list-item-hcm-indicator.scss +++ b/src/material/list/_list-item-hcm-indicator.scss @@ -1,5 +1,4 @@ @use '@angular/cdk'; -@use '@material/list/evolution-variables' as mdc-list-variables; // Renders a circle indicator when Windows Hich Constrast mode (HCM) is enabled. In some // situations, such as a selected option, the list item communicates the selected state by changing @@ -12,7 +11,7 @@ content: ''; position: absolute; top: 50%; - right: mdc-list-variables.$side-padding; + right: 16px; transform: translateY(-50%); width: $size; height: 0; @@ -23,7 +22,7 @@ [dir='rtl'] { &::after { right: auto; - left: mdc-list-variables.$side-padding; + left: 16px; } } } diff --git a/src/material/list/_list-option-trailing-avatar-compat.scss b/src/material/list/_list-option-trailing-avatar-compat.scss deleted file mode 100644 index 826c84284ce9..000000000000 --- a/src/material/list/_list-option-trailing-avatar-compat.scss +++ /dev/null @@ -1,59 +0,0 @@ -@use '@material/feature-targeting/feature-targeting'; -@use '@material/list/evolution-mixins' as mdc-list; -@use '../core/mdc-helpers/mdc-helpers'; - -// For compatibility with the non-MDC selection list, we support avatars that are -// shown at the end of the list option. This is not supported by the MDC list as the -// spec only defines avatars at the beginning of a list item. For selection list options, -// we support changing the checkbox position to `before`. This results in the avatar from -// the list start being moved to the end. Similar to MDC's `--trailing-icon` class, we -// implement a `--trailing-avatar` class that is based on the original `--leading-avatar` -// implementation. See: https://github.com/material-components/material-components-web/blob/3f342c3f4715fd3587f327ce4ea6b5dd314c5c55/packages/mdc-list/_evolution-mixins.scss#L198-L217 - -@mixin core-styles($query) { - $feat-structure: feature-targeting.create-target($query, structure); - - @include mdc-helpers.disable-mdc-fallback-declarations { - .mat-mdc-list-option-with-trailing-avatar { - @include mdc-list.item-end-spacing(16px, $query: $query); - @include mdc-list.item-end-size(40px, $query: $query); - - &.mdc-list-item--with-two-lines { - $top: 32px; - $bottom: 20px; - - .mdc-list-item__primary-text { - display: block; - margin-top: 0; - line-height: normal; - margin-bottom: $bottom * -1; - - // This was used by MDC to set the text baseline. We should figure out a way to - // remove it, because it can introduce unnecessary whitespace at the beginning - // of the element. - &::before { - display: inline-block; - width: 0; - height: $top; - content: ''; - vertical-align: 0; - } - - &::after { - display: inline-block; - width: 0; - height: $bottom; - content: ''; - vertical-align: $bottom * -1; - } - } - } - - .mdc-list-item__end { - @include feature-targeting.targets($feat-structure) { - border-radius: 50%; - } - } - } - } -} diff --git a/src/material/list/_list-theme.scss b/src/material/list/_list-theme.scss index af48cc9c0ee9..daa96261b4ab 100644 --- a/src/material/list/_list-theme.scss +++ b/src/material/list/_list-theme.scss @@ -1,6 +1,4 @@ @use 'sass:map'; -@use '@material/list/evolution-mixins'; -@use '@material/list/list-theme' as mdc-list-theme; @use '../core/style/sass-utils'; @use '../core/theming/theming'; @@ -20,7 +18,8 @@ } @else { @include sass-utils.current-selector-or-root() { - @include mdc-list-theme.theme(tokens-mdc-list.get-unthemable-tokens()); + @include token-utils.create-token-values( + tokens-mdc-list.$prefix, tokens-mdc-list.get-unthemable-tokens()); @include token-utils.create-token-values( tokens-mat-list.$prefix, tokens-mat-list.get-unthemable-tokens()); } @@ -32,11 +31,9 @@ @include _theme-from-tokens(inspection.get-theme-tokens($theme, color)); } @else { - $mdc-list-color-tokens: tokens-mdc-list.get-color-tokens($theme); - - // Add values for MDC list tokens. @include sass-utils.current-selector-or-root() { - @include mdc-list-theme.theme($mdc-list-color-tokens); + @include token-utils.create-token-values( + tokens-mdc-list.$prefix, tokens-mdc-list.get-color-tokens($theme)); @include token-utils.create-token-values( tokens-mat-list.$prefix, tokens-mat-list.get-color-tokens($theme)); } @@ -79,8 +76,13 @@ // There is no token for activated color on nav list. // TODO(mmalerba): Add a token to MDC or make a custom one. .mat-mdc-list-base.mat-mdc-list-base { - @include evolution-mixins.list-selected-ink-color( - inspection.get-theme-color($theme, primary)); + .mdc-list-item--selected, + .mdc-list-item--activated { + .mdc-list-item__primary-text, + .mdc-list-item__start { + color: inspection.get-theme-color($theme, primary); + } + } } // TODO(mmalerba): Leaking styles from the old MDC list mixins used in other components can @@ -102,11 +104,10 @@ } @else { $density-scale: inspection.get-theme-density($theme); - $mdc-list-density-tokens: tokens-mdc-list.get-density-tokens($theme); - // Add values for MDC list tokens. @include sass-utils.current-selector-or-root() { - @include mdc-list-theme.theme($mdc-list-density-tokens); + @include token-utils.create-token-values( + tokens-mdc-list.$prefix, tokens-mdc-list.get-density-tokens($theme)); @include token-utils.create-token-values( tokens-mat-list.$prefix, tokens-mat-list.get-density-tokens($theme)); } @@ -155,11 +156,9 @@ @include _theme-from-tokens(inspection.get-theme-tokens($theme, typography)); } @else { - $mdc-list-typography-tokens: tokens-mdc-list.get-typography-tokens($theme); - - // Add values for MDC list tokens. @include sass-utils.current-selector-or-root() { - @include mdc-list-theme.theme($mdc-list-typography-tokens); + @include token-utils.create-token-values( + tokens-mdc-list.$prefix, tokens-mdc-list.get-typography-tokens($theme)); @include token-utils.create-token-values( tokens-mat-list.$prefix, tokens-mat-list.get-typography-tokens($theme)); } @@ -204,8 +203,8 @@ @mixin _theme-from-tokens($tokens) { @include validation.selector-defined( 'Calls to Angular Material theme mixins with an M3 theme must be wrapped in a selector'); - @include mdc-list-theme.theme(token-utils.get-tokens-for($tokens, tokens-mdc-list.$prefix)); - + $mdc-list-tokens: token-utils.get-tokens-for($tokens, tokens-mdc-list.$prefix); $mat-list-tokens: token-utils.get-tokens-for($tokens, tokens-mat-list.$prefix); + @include token-utils.create-token-values(tokens-mdc-list.$prefix, $mdc-list-tokens); @include token-utils.create-token-values(tokens-mat-list.$prefix, $mat-list-tokens); } diff --git a/src/material/list/list-option.scss b/src/material/list/list-option.scss index f4ea5f27bf75..80f5b6fe7ac8 100644 --- a/src/material/list/list-option.scss +++ b/src/material/list/list-option.scss @@ -1,12 +1,63 @@ @use '../checkbox/checkbox-common'; @use '../radio/radio-common'; -@use '../core/mdc-helpers/mdc-helpers'; -@use './list-option-trailing-avatar-compat'; @use './list-item-hcm-indicator'; -// For compatibility with the non-MDC list, we support avatars that are shown at the end -// of the list option. We create a class similar to MDC's `--trailing-icon` one. -@include list-option-trailing-avatar-compat.core-styles($query: mdc-helpers.$mdc-base-styles-query); +// For compatibility with the non-MDC selection list, we support avatars that are +// shown at the end of the list option. This is not supported by the MDC list as the +// spec only defines avatars at the beginning of a list item. For selection list options, +// we support changing the checkbox position to `before`. This results in the avatar from +// the list start being moved to the end. Similar to MDC's `--trailing-icon` class, we +// implement a `--trailing-avatar` class that is based on the original `--leading-avatar` +// implementation. See: https://github.com/material-components/material-components-web/blob/3f342c3f4715fd3587f327ce4ea6b5dd314c5c55/packages/mdc-list/_evolution-mixins.scss#L198-L217 +.mat-mdc-list-option-with-trailing-avatar { + &.mdc-list-item, + [dir='rtl'] &.mdc-list-item { + padding-left: 0; + padding-right: 0; + } + + .mdc-list-item__end { + margin-left: 16px; + margin-right: 16px; + width: 40px; + height: 40px; + } + + &.mdc-list-item--with-two-lines { + $top: 32px; + $bottom: 20px; + + .mdc-list-item__primary-text { + display: block; + margin-top: 0; + line-height: normal; + margin-bottom: $bottom * -1; + + // This was used by MDC to set the text baseline. We should figure out a way to + // remove it, because it can introduce unnecessary whitespace at the beginning + // of the element. + &::before { + display: inline-block; + width: 0; + height: $top; + content: ''; + vertical-align: 0; + } + + &::after { + display: inline-block; + width: 0; + height: $bottom; + content: ''; + vertical-align: $bottom * -1; + } + } + } + + .mdc-list-item__end { + border-radius: 50%; + } +} .mat-mdc-list-option { // We can't use the MDC checkbox here directly, because this checkbox is purely diff --git a/src/material/list/list-option.ts b/src/material/list/list-option.ts index 8c4c7049fb66..7e900e6e557b 100644 --- a/src/material/list/list-option.ts +++ b/src/material/list/list-option.ts @@ -83,6 +83,10 @@ export interface SelectionList extends MatListBase { '[class.mdc-list-item--with-trailing-checkbox]': '_hasCheckboxAt("after")', '[class.mdc-list-item--with-leading-radio]': '_hasRadioAt("before")', '[class.mdc-list-item--with-trailing-radio]': '_hasRadioAt("after")', + + // Utility class that makes it easier to target the case where there's both a leading + // and a trailing icon. Avoids having to write out all the combinations. + '[class.mat-mdc-list-item-both-leading-and-trailing]': '_hasBothLeadingAndTrailing()', '[class.mat-accent]': 'color !== "primary" && color !== "warn"', '[class.mat-warn]': 'color === "warn"', '[class._mat-animation-noopable]': '_noopAnimations', @@ -337,4 +341,18 @@ export class MatListOption extends MatListItemBase implements ListOption, OnInit _setTabindex(value: number) { this._hostElement.setAttribute('tabindex', value + ''); } + + protected _hasBothLeadingAndTrailing(): boolean { + const hasLeading = + this._hasProjected('avatars', 'before') || + this._hasProjected('icons', 'before') || + this._hasCheckboxAt('before') || + this._hasRadioAt('before'); + const hasTrailing = + this._hasProjected('icons', 'after') || + this._hasProjected('avatars', 'after') || + this._hasCheckboxAt('after') || + this._hasRadioAt('after'); + return hasLeading && hasTrailing; + } } diff --git a/src/material/list/list.scss b/src/material/list/list.scss index 61e246175f42..fbbf4c4d5896 100644 --- a/src/material/list/list.scss +++ b/src/material/list/list.scss @@ -1,87 +1,61 @@ -@use 'sass:map'; -@use '@material/list/list' as mdc-list; -@use '@material/list/list-theme' as mdc-list-theme; -@use '@material/theme/custom-properties' as mdc-custom-properties; @use '../core/style/layout-common'; -@use '../core/tokens/m2/mat/list' as m2-mat-list; -@use '../core/tokens/m2/mdc/list' as m2-mdc-list; +@use '../core/tokens/m2/mat/list' as tokens-mat-list; +@use '../core/tokens/m2/mdc/list' as tokens-mdc-list; @use '../core/tokens/token-utils'; @use './list-item-hcm-indicator'; +@use './list-inherited-structure'; -// The slots for tokens that will be configured in the theme can be emitted with no fallback. -@include mdc-custom-properties.configure($emit-fallback-values: false, $emit-fallback-vars: false) { - $mdc-list-token-slots: m2-mdc-list.get-token-slots(); - - // Add the MDC list static styles. - @include mdc-list.static-styles(); - - // Add the official slots for the MDC list. - @include mdc-list-theme.theme-styles(map.merge($mdc-list-token-slots, ( - // We structure the avatar differently from how MDC expects, so we add these slots ourselves. - list-item-leading-avatar-shape: null, - list-item-leading-avatar-color: null, - // We add this slot ourselves with more specificity, so we don't need MDC to emit it. - list-item-disabled-label-text-color: null, - // We don't use MDC's state layers, so we add these slots ourselves instead. - list-item-hover-state-layer-color: null, - list-item-hover-state-layer-opacity: null, - list-item-focus-state-layer-color: null, - list-item-focus-state-layer-opacity: null, - list-item-disabled-state-layer-color: null, - list-item-disabled-state-layer-opacity: null, - ))); - - // Add additional slots for the MDC list tokens, needed in Angular Material. - @include token-utils.use-tokens(m2-mdc-list.$prefix, $mdc-list-token-slots) { - // MDC allows focus and hover colors to take precedence over disabled color. We add the disabled - // color here with higher specificity so that the disabled color takes precedence. - // TODO(mmalerba): Dicuss with MDC whether to change this in their code. - .mdc-list-item.mdc-list-item--disabled .mdc-list-item__primary-text { - @include token-utils.create-token-slot(color, list-item-disabled-label-text-color); - } +@include list-inherited-structure.private-list-inherited-structural-styles; - // We don't use MDC's state layer since it's tied in with their ripple. Instead we emit slots - // for our own state layer. - // TODO(mmalerba): Consider using MDC's ripple & state layer. - .mdc-list-item:hover::before { - @include token-utils.create-token-slot(background-color, list-item-hover-state-layer-color); - @include token-utils.create-token-slot(opacity, list-item-hover-state-layer-opacity); - } - .mdc-list-item.mdc-list-item--disabled::before { - @include token-utils.create-token-slot( - background-color, list-item-disabled-state-layer-color); - @include token-utils.create-token-slot(opacity, list-item-disabled-state-layer-opacity); - } - .mdc-list-item:focus::before { - @include token-utils.create-token-slot(background-color, list-item-focus-state-layer-color); - @include token-utils.create-token-slot(opacity, list-item-focus-state-layer-opacity); - } +// Add additional slots for the MDC list tokens, needed in Angular Material. +@include token-utils.use-tokens(tokens-mdc-list.$prefix, tokens-mdc-list.get-token-slots()) { + // MDC allows focus and hover colors to take precedence over disabled color. We add the disabled + // color here with higher specificity so that the disabled color takes precedence. + // TODO(mmalerba): Dicuss with MDC whether to change this in their code. + .mdc-list-item.mdc-list-item--disabled .mdc-list-item__primary-text { + @include token-utils.create-token-slot(color, list-item-disabled-label-text-color); + } - // Apply the disabled opacity to the checkbox/radio indicators. - // TODO(mmalerba): We should probably stop doing this and allow the checkbox/radio to decide - // what their disabled state looks like. This is done for now to avoid screenshot diffs. - .mdc-list-item--disabled { - .mdc-radio, - .mdc-checkbox { - @include token-utils.create-token-slot(opacity, list-item-disabled-label-text-opacity); - } - } + // We don't use MDC's state layer since it's tied in with their ripple. Instead we emit slots + // for our own state layer. + // TODO(mmalerba): Consider using MDC's ripple & state layer. + .mdc-list-item:hover::before { + @include token-utils.create-token-slot(background-color, list-item-hover-state-layer-color); + @include token-utils.create-token-slot(opacity, list-item-hover-state-layer-opacity); + } + .mdc-list-item.mdc-list-item--disabled::before { + @include token-utils.create-token-slot(background-color, list-item-disabled-state-layer-color); + @include token-utils.create-token-slot(opacity, list-item-disabled-state-layer-opacity); + } + .mdc-list-item:focus::before { + @include token-utils.create-token-slot(background-color, list-item-focus-state-layer-color); + @include token-utils.create-token-slot(opacity, list-item-focus-state-layer-opacity); + } - // In Angular Material we put the avatar class directly on the .mdc-list-item__start element, - // rather than nested inside it, so we need to emit avatar slots ourselves. - // TODO(mmalerba): We should try to change MDC's recommended DOM or change ours to match their - // recommendation. - .mdc-list-item--with-leading-avatar .mat-mdc-list-item-avatar { - @include token-utils.create-token-slot(border-radius, list-item-leading-avatar-shape); - @include token-utils.create-token-slot(background-color, list-item-leading-avatar-color); + // Apply the disabled opacity to the checkbox/radio indicators. + // TODO(mmalerba): We should probably stop doing this and allow the checkbox/radio to decide + // what their disabled state looks like. This is done for now to avoid screenshot diffs. + .mdc-list-item--disabled { + .mdc-radio, + .mdc-checkbox { + @include token-utils.create-token-slot(opacity, list-item-disabled-label-text-opacity); } + } - // Set font-size of leading icon to same value as its width and height. Ensure icon scales to - // "list-item-leading-icon-size" token. In Angular Material, the icon is on the same element as - // ".mdc-list-item__start", rather than a child of ".mdc-list-item__start". - .mat-mdc-list-item-icon { - @include token-utils.create-token-slot(font-size, list-item-leading-icon-size); - } + // In Angular Material we put the avatar class directly on the .mdc-list-item__start element, + // rather than nested inside it, so we need to emit avatar slots ourselves. + // TODO(mmalerba): We should try to change MDC's recommended DOM or change ours to match their + // recommendation. + .mdc-list-item--with-leading-avatar .mat-mdc-list-item-avatar { + @include token-utils.create-token-slot(border-radius, list-item-leading-avatar-shape); + @include token-utils.create-token-slot(background-color, list-item-leading-avatar-color); + } + + // Set font-size of leading icon to same value as its width and height. Ensure icon scales to + // "list-item-leading-icon-size" token. In Angular Material, the icon is on the same element as + // ".mdc-list-item__start", rather than a child of ".mdc-list-item__start". + .mat-mdc-list-item-icon { + @include token-utils.create-token-slot(font-size, list-item-leading-icon-size); } } @@ -200,7 +174,7 @@ mat-action-list button { } } -@include token-utils.use-tokens(m2-mat-list.$prefix, m2-mat-list.get-token-slots()) { +@include token-utils.use-tokens(tokens-mat-list.$prefix, tokens-mat-list.get-token-slots()) { .mdc-list-item--with-leading-icon .mdc-list-item__start { @include token-utils.create-token-slot(margin-inline-start, list-item-leading-icon-start-space); @include token-utils.create-token-slot(margin-inline-end, list-item-leading-icon-end-space); diff --git a/src/material/list/list.ts b/src/material/list/list.ts index fcfc79bbd97d..fd6662d4a468 100644 --- a/src/material/list/list.ts +++ b/src/material/list/list.ts @@ -59,6 +59,9 @@ export class MatList extends MatListBase {} '[class.mdc-list-item--with-leading-avatar]': '_avatars.length !== 0', '[class.mdc-list-item--with-leading-icon]': '_icons.length !== 0', '[class.mdc-list-item--with-trailing-meta]': '_meta.length !== 0', + // Utility class that makes it easier to target the case where there's both a leading + // and a trailing icon. Avoids having to write out all the combinations. + '[class.mat-mdc-list-item-both-leading-and-trailing]': '_hasBothLeadingAndTrailing()', '[class._mat-animation-noopable]': '_noopAnimations', '[attr.aria-current]': '_getAriaCurrent()', }, @@ -103,4 +106,8 @@ export class MatListItem extends MatListItemBase { _getAriaCurrent(): string | null { return this._hostElement.nodeName === 'A' && this._activated ? 'page' : null; } + + protected _hasBothLeadingAndTrailing(): boolean { + return this._meta.length !== 0 && (this._avatars.length !== 0 || this._icons.length !== 0); + } } diff --git a/src/material/tabs/tab-group.spec.ts b/src/material/tabs/tab-group.spec.ts index c93b7ca3cd89..bcd7ce602283 100644 --- a/src/material/tabs/tab-group.spec.ts +++ b/src/material/tabs/tab-group.spec.ts @@ -792,7 +792,7 @@ describe('MDC-based MatTabGroup', () => { ); expect(contentElements.map(element => element.style.visibility)).toEqual([ - '', + 'visible', 'hidden', 'hidden', 'hidden', @@ -805,7 +805,7 @@ describe('MDC-based MatTabGroup', () => { expect(contentElements.map(element => element.style.visibility)).toEqual([ 'hidden', 'hidden', - '', + 'visible', 'hidden', ]); @@ -815,7 +815,7 @@ describe('MDC-based MatTabGroup', () => { expect(contentElements.map(element => element.style.visibility)).toEqual([ 'hidden', - '', + 'visible', 'hidden', 'hidden', ]); diff --git a/src/material/tabs/tabs-animations.ts b/src/material/tabs/tabs-animations.ts index 5e7955d4a373..e0097556cba2 100644 --- a/src/material/tabs/tabs-animations.ts +++ b/src/material/tabs/tabs-animations.ts @@ -6,12 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ import { + AnimationTriggerMetadata, animate, state, style, transition, trigger, - AnimationTriggerMetadata, } from '@angular/animations'; /** @@ -24,7 +24,10 @@ export const matTabsAnimations: { /** Animation translates a tab along the X axis. */ translateTab: trigger('translateTab', [ // Transitions to `none` instead of 0, because some browsers might blur the content. - state('center, void, left-origin-center, right-origin-center', style({transform: 'none'})), + state( + 'center, void, left-origin-center, right-origin-center', + style({transform: 'none', visibility: 'visible'}), + ), // If the tab is either on the left or right, we additionally add a `min-height` of 1px // in order to ensure that the element has a height before its state changes. This is diff --git a/src/material/tooltip/tooltip.spec.ts b/src/material/tooltip/tooltip.spec.ts index b61eb9479982..5569df953caa 100644 --- a/src/material/tooltip/tooltip.spec.ts +++ b/src/material/tooltip/tooltip.spec.ts @@ -413,12 +413,12 @@ describe('MDC-based MatTooltip', () => { it('should not show tooltip if message is not present or empty', () => { assertTooltipInstance(tooltipDirective, false); - tooltipDirective.message = undefined!; + tooltipDirective.message = undefined; fixture.detectChanges(); tooltipDirective.show(); assertTooltipInstance(tooltipDirective, false); - tooltipDirective.message = null!; + tooltipDirective.message = null; fixture.detectChanges(); tooltipDirective.show(); assertTooltipInstance(tooltipDirective, false); diff --git a/src/material/tooltip/tooltip.ts b/src/material/tooltip/tooltip.ts index b5e7a73f30ac..be12011ed202 100644 --- a/src/material/tooltip/tooltip.ts +++ b/src/material/tooltip/tooltip.ts @@ -296,11 +296,11 @@ export class MatTooltip implements OnDestroy, AfterViewInit { /** The message to be displayed in the tooltip */ @Input('matTooltip') - get message() { + get message(): string { return this._message; } - set message(value: string) { + set message(value: string | null | undefined) { this._ariaDescriber.removeDescription(this._elementRef.nativeElement, this._message, 'tooltip'); // If the message is not a string (e.g. number), convert it to a string and trim it. diff --git a/src/youtube-player/README.md b/src/youtube-player/README.md index 48abed9e76ec..63eae67a87c9 100644 --- a/src/youtube-player/README.md +++ b/src/youtube-player/README.md @@ -8,8 +8,8 @@ File any bugs against the [angular/components repo](https://github.com/angular/c To install, run `ng add @angular/youtube-player`. ## Usage -Import the component either by adding the `YouTubePlayerModule` to your app or by importing -`YouTubePlayer` into a standalone component. Then add the `` will show a placeholder element instead of loading the API +By default, the `` will show a placeholder element instead of loading the API up-front until the user interacts with it. This speeds up the initial render of the page by not loading unnecessary JavaScript for a video that might not be played. Once the user clicks on the video, the API will be loaded and the placeholder will be swapped out with the actual video. diff --git a/tools/public_api_guard/cdk/drag-drop.md b/tools/public_api_guard/cdk/drag-drop.md index 82b57b513f0d..6611f665f4f7 100644 --- a/tools/public_api_guard/cdk/drag-drop.md +++ b/tools/public_api_guard/cdk/drag-drop.md @@ -76,6 +76,8 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { // (undocumented) static ngAcceptInputType_disabled: unknown; // (undocumented) + static ngAcceptInputType_scale: unknown; + // (undocumented) ngAfterViewInit(): void; // (undocumented) ngOnChanges(changes: SimpleChanges): void; @@ -92,6 +94,7 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { // (undocumented) _resetPreviewTemplate(preview: CdkDragPreview): void; rootElementSelector: string; + scale: number; setFreeDragPosition(value: Point): void; // (undocumented) _setPlaceholderTemplate(placeholder: CdkDragPlaceholder): void; @@ -99,7 +102,7 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { _setPreviewTemplate(preview: CdkDragPreview): void; readonly started: EventEmitter; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration, "[cdkDrag]", ["cdkDrag"], { "data": { "alias": "cdkDragData"; "required": false; }; "lockAxis": { "alias": "cdkDragLockAxis"; "required": false; }; "rootElementSelector": { "alias": "cdkDragRootElement"; "required": false; }; "boundaryElement": { "alias": "cdkDragBoundary"; "required": false; }; "dragStartDelay": { "alias": "cdkDragStartDelay"; "required": false; }; "freeDragPosition": { "alias": "cdkDragFreeDragPosition"; "required": false; }; "disabled": { "alias": "cdkDragDisabled"; "required": false; }; "constrainPosition": { "alias": "cdkDragConstrainPosition"; "required": false; }; "previewClass": { "alias": "cdkDragPreviewClass"; "required": false; }; "previewContainer": { "alias": "cdkDragPreviewContainer"; "required": false; }; }, { "started": "cdkDragStarted"; "released": "cdkDragReleased"; "ended": "cdkDragEnded"; "entered": "cdkDragEntered"; "exited": "cdkDragExited"; "dropped": "cdkDragDropped"; "moved": "cdkDragMoved"; }, never, never, true, never>; + static ɵdir: i0.ɵɵDirectiveDeclaration, "[cdkDrag]", ["cdkDrag"], { "data": { "alias": "cdkDragData"; "required": false; }; "lockAxis": { "alias": "cdkDragLockAxis"; "required": false; }; "rootElementSelector": { "alias": "cdkDragRootElement"; "required": false; }; "boundaryElement": { "alias": "cdkDragBoundary"; "required": false; }; "dragStartDelay": { "alias": "cdkDragStartDelay"; "required": false; }; "freeDragPosition": { "alias": "cdkDragFreeDragPosition"; "required": false; }; "disabled": { "alias": "cdkDragDisabled"; "required": false; }; "constrainPosition": { "alias": "cdkDragConstrainPosition"; "required": false; }; "previewClass": { "alias": "cdkDragPreviewClass"; "required": false; }; "previewContainer": { "alias": "cdkDragPreviewContainer"; "required": false; }; "scale": { "alias": "cdkDragScale"; "required": false; }; }, { "started": "cdkDragStarted"; "released": "cdkDragReleased"; "ended": "cdkDragEnded"; "entered": "cdkDragEntered"; "exited": "cdkDragExited"; "dropped": "cdkDragDropped"; "moved": "cdkDragMoved"; }, never, never, true, never>; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration, [null, { optional: true; skipSelf: true; }, null, null, null, { optional: true; }, { optional: true; }, null, null, { optional: true; self: true; }, { optional: true; skipSelf: true; }]>; } @@ -440,6 +443,7 @@ export class DragRef { event: MouseEvent | TouchEvent; }>; reset(): void; + scale: number; setFreeDragPosition(value: Point): this; _sortFromLastPointerPosition(): void; readonly started: Subject<{ diff --git a/tools/public_api_guard/material/list.md b/tools/public_api_guard/material/list.md index 3a35d95306cc..da4aad970fa1 100644 --- a/tools/public_api_guard/material/list.md +++ b/tools/public_api_guard/material/list.md @@ -73,6 +73,8 @@ export class MatListItem extends MatListItemBase { _activated: boolean; _getAriaCurrent(): string | null; // (undocumented) + protected _hasBothLeadingAndTrailing(): boolean; + // (undocumented) _itemText: ElementRef; // (undocumented) _lines: QueryList; @@ -170,6 +172,8 @@ export class MatListOption extends MatListItemBase implements ListOption, OnInit _getTogglePosition(): MatListOptionTogglePosition; // (undocumented) _handleBlur(): void; + // (undocumented) + protected _hasBothLeadingAndTrailing(): boolean; _hasCheckboxAt(position: MatListOptionTogglePosition): boolean; _hasIconsOrAvatarsAt(position: 'before' | 'after'): boolean; _hasProjected(type: 'icons' | 'avatars', position: 'before' | 'after'): boolean; diff --git a/tools/public_api_guard/material/tooltip.md b/tools/public_api_guard/material/tooltip.md index f41b4073a7d8..fd8ad3d28e28 100644 --- a/tools/public_api_guard/material/tooltip.md +++ b/tools/public_api_guard/material/tooltip.md @@ -76,7 +76,7 @@ export class MatTooltip implements OnDestroy, AfterViewInit { set hideDelay(value: NumberInput); _isTooltipVisible(): boolean; get message(): string; - set message(value: string); + set message(value: string | null | undefined); // (undocumented) ngAfterViewInit(): void; ngOnDestroy(): void;