diff --git a/scripts/check-mdc-tests-config.ts b/scripts/check-mdc-tests-config.ts index fefb4efe8b83..fb702020cea8 100644 --- a/scripts/check-mdc-tests-config.ts +++ b/scripts/check-mdc-tests-config.ts @@ -175,6 +175,9 @@ export const config = { 'should re-add margin if label is added asynchronously', 'should properly update margin if label content is projected', + // The MDC slide toggle uses a `button` which isn't able to block form submission. + 'should prevent the form from submit when being required', + // TODO: the focus origin functionality has to be implemeted for the MDC slide toggle. 'should not change focus origin if origin not specified' ], diff --git a/src/e2e-app/protractor.conf.js b/src/e2e-app/protractor.conf.js index 17317b8708c3..7b4e269f53d5 100644 --- a/src/e2e-app/protractor.conf.js +++ b/src/e2e-app/protractor.conf.js @@ -24,7 +24,10 @@ exports.config = { {id: 'region', enabled: false}, // Don't require at least one `

` since we don't have any content. - {id: 'page-has-heading-one', enabled: false} + {id: 'page-has-heading-one', enabled: false}, + + // Axe incorrectly picks up that `aria-required` is not allowed on the MDC slide toggle. + {id: 'aria-allowed-attr', selector: '*:not(.mdc-switch)'} ] } ], diff --git a/src/material-experimental/mdc-helpers/_focus-indicators.scss b/src/material-experimental/mdc-helpers/_focus-indicators.scss index 20c39f28d3c4..4189d3486265 100644 --- a/src/material-experimental/mdc-helpers/_focus-indicators.scss +++ b/src/material-experimental/mdc-helpers/_focus-indicators.scss @@ -60,7 +60,8 @@ // which will clip a square focus indicator so we have to turn it into a circle. .mat-mdc-checkbox-ripple.mat-mdc-focus-indicator::before, .mat-radio-ripple.mat-mdc-focus-indicator::before, - .mat-mdc-slider .mat-mdc-focus-indicator::before { + .mat-mdc-slider .mat-mdc-focus-indicator::before, + .mat-mdc-slide-toggle .mat-mdc-focus-indicator::before { border-radius: 50%; } diff --git a/src/material-experimental/mdc-slide-toggle/_slide-toggle-theme.scss b/src/material-experimental/mdc-slide-toggle/_slide-toggle-theme.scss index fbb9e67bfe59..7b4a9eb3daff 100644 --- a/src/material-experimental/mdc-slide-toggle/_slide-toggle-theme.scss +++ b/src/material-experimental/mdc-slide-toggle/_slide-toggle-theme.scss @@ -1,103 +1,121 @@ -@use '@material/theme/theme-color' as mdc-theme-color; -@use '@material/switch/deprecated' as mdc-switch with ($deprecated-suffix: ''); -@use '@material/form-field' as mdc-form-field; @use 'sass:map'; +@use 'sass:color'; +@use '@material/switch/switch-theme' as mdc-switch-theme; +@use '@material/theme/color-palette' as mdc-color-palette; +@use '@material/form-field' as mdc-form-field; @use '../mdc-helpers/mdc-helpers'; @use '../../material/core/typography/typography'; @use '../../material/core/theming/theming'; +@use '../../material/core/theming/palette'; + +// Generates all color mapping for the properties that only change based on the theme. +@function _get-theme-base-map($is-dark) { + $on-surface: if($is-dark, mdc-color-palette.$grey-100, mdc-color-palette.$grey-800); + $hairline: if($is-dark, mdc-color-palette.$grey-500, mdc-color-palette.$grey-300); + $on-surface-variant: if($is-dark, mdc-color-palette.$grey-200, mdc-color-palette.$grey-700); + $on-surface-state-content: if($is-dark, mdc-color-palette.$grey-50, mdc-color-palette.$grey-900); + $disabled-handle-color: mdc-color-palette.$grey-800; + $selected-icon-color: mdc-color-palette.$grey-100; + $icon-color: if($is-dark, mdc-color-palette.$grey-800, mdc-color-palette.$grey-100); + + @return ( + disabled-selected-handle-color: $disabled-handle-color, + disabled-unselected-handle-color: $disabled-handle-color, + + disabled-selected-track-color: $on-surface, + disabled-unselected-track-color: $on-surface, + unselected-focus-state-layer-color: $on-surface, + unselected-pressed-state-layer-color: $on-surface, + unselected-hover-state-layer-color: $on-surface, + + unselected-focus-track-color: $hairline, + unselected-hover-track-color: $hairline, + unselected-pressed-track-color: $hairline, + unselected-track-color: $hairline, + + unselected-focus-handle-color: $on-surface-state-content, + unselected-hover-handle-color: $on-surface-state-content, + unselected-pressed-handle-color: $on-surface-state-content, + + handle-surface-color: surface, + unselected-handle-color: $on-surface-variant, + + selected-icon-color: $selected-icon-color, + disabled-selected-icon-color: $icon-color, + disabled-unselected-icon-color: $icon-color, + unselected-icon-color: $icon-color, + ); +} +// Generates the mapping for the properties that change based on the slide toggle color. +@function _get-theme-color-map($color-palette) { + $state-content: color.scale($color-palette, $blackness: 50%); + $inverse: color.scale($color-palette, $lightness: 75%); + + @return ( + selected-focus-state-layer-color: $color-palette, + selected-handle-color: $color-palette, + selected-hover-state-layer-color: $color-palette, + selected-pressed-state-layer-color: $color-palette, + + selected-focus-handle-color: $state-content, + selected-hover-handle-color: $state-content, + selected-pressed-handle-color: $state-content, + + selected-focus-track-color: $inverse, + selected-hover-track-color: $inverse, + selected-pressed-track-color: $inverse, + selected-track-color: $inverse, + ); +} @mixin color($config-or-theme) { $config: theming.get-color-config($config-or-theme); $primary: theming.get-color-from-palette(map.get($config, primary)); $accent: theming.get-color-from-palette(map.get($config, accent)); $warn: theming.get-color-from-palette(map.get($config, warn)); - - // Save original values of MDC global variables. We need to save these so we can restore the - // variables to their original values and prevent unintended side effects from using this mixin. - $orig-baseline-theme-color: mdc-switch.$baseline-theme-color; - $orig-toggled-off-thumb-color: mdc-switch.$toggled-off-thumb-color; - $orig-toggled-off-track-color: mdc-switch.$toggled-off-track-color; - $orig-disabled-thumb-color: mdc-switch.$disabled-thumb-color; - $orig-disabled-track-color: mdc-switch.$disabled-track-color; + $is-dark: map.get($config, is-dark); @include mdc-helpers.mat-using-mdc-theme($config) { - mdc-switch.$baseline-theme-color: primary; - mdc-switch.$toggled-off-thumb-color: mdc-theme-color.prop-value(surface); - mdc-switch.$toggled-off-track-color: mdc-theme-color.prop-value(on-surface); - mdc-switch.$disabled-thumb-color: mdc-theme-color.prop-value(surface); - mdc-switch.$disabled-track-color: mdc-theme-color.prop-value(on-surface); - // MDC's switch doesn't support a `color` property. We add support // for it by adding a CSS class for accent and warn style. .mat-mdc-slide-toggle { @include mdc-form-field.core-styles($query: mdc-helpers.$mat-theme-styles-query); - - .mdc-switch__thumb-underlay::after, .mat-ripple-element { - background: mdc-switch.$toggled-off-ripple-color; - } + @include mdc-switch-theme.theme(_get-theme-base-map($is-dark)); &.mat-primary { - @include mdc-switch.without-ripple($query: mdc-helpers.$mat-theme-styles-query); + @include mdc-switch-theme.theme(_get-theme-color-map($primary)); } &.mat-accent { - mdc-switch.$baseline-theme-color: secondary; - @include mdc-switch.without-ripple($query: mdc-helpers.$mat-theme-styles-query); + @include mdc-switch-theme.theme(_get-theme-color-map($accent)); } &.mat-warn { - mdc-switch.$baseline-theme-color: error; - @include mdc-switch.without-ripple($query: mdc-helpers.$mat-theme-styles-query); - } - } - - // The ripple color matches the palette only when it's checked. - .mat-mdc-slide-toggle-checked { - .mdc-switch__thumb-underlay::after, .mat-ripple-element { - background: $primary; - } - - &.mat-accent { - .mdc-switch__thumb-underlay::after, .mat-ripple-element { - background: $accent; - } - } - - &.mat-warn { - .mdc-switch__thumb-underlay::after, .mat-ripple-element { - background: $warn; - } + @include mdc-switch-theme.theme(_get-theme-color-map($warn)); } } } - - // Restore original values of MDC global variables. - mdc-switch.$baseline-theme-color: $orig-baseline-theme-color; - mdc-switch.$toggled-off-thumb-color: $orig-toggled-off-thumb-color; - mdc-switch.$toggled-off-track-color: $orig-toggled-off-track-color; - mdc-switch.$disabled-thumb-color: $orig-disabled-thumb-color; - mdc-switch.$disabled-track-color: $orig-disabled-track-color; } @mixin typography($config-or-theme) { $config: typography.private-typography-to-2018-config( theming.get-typography-config($config-or-theme)); @include mdc-helpers.mat-using-mdc-typography($config) { - @include mdc-switch.without-ripple($query: mdc-helpers.$mat-typography-styles-query); @include mdc-form-field.core-styles($query: mdc-helpers.$mat-typography-styles-query); } } @mixin density($config-or-theme) { $density-scale: theming.get-density-config($config-or-theme); - .mat-mdc-slide-toggle .mdc-switch { - @include mdc-switch.density($density-scale, $query: mdc-helpers.$mat-base-styles-query); + .mat-mdc-slide-toggle { + @include mdc-switch-theme.theme(mdc-switch-theme.density($density-scale)); } } @mixin theme($theme-or-color-config) { $theme: theming.private-legacy-get-theme($theme-or-color-config); + @include theming.private-check-duplicate-theme-styles($theme, 'mat-mdc-slide-toggle') { $color: theming.get-color-config($theme); $density: theming.get-density-config($theme); diff --git a/src/material-experimental/mdc-slide-toggle/slide-toggle.e2e.spec.ts b/src/material-experimental/mdc-slide-toggle/slide-toggle.e2e.spec.ts index bba931a5d706..0979f9429846 100644 --- a/src/material-experimental/mdc-slide-toggle/slide-toggle.e2e.spec.ts +++ b/src/material-experimental/mdc-slide-toggle/slide-toggle.e2e.spec.ts @@ -3,7 +3,7 @@ import {expectToExist} from '../../cdk/testing/private/e2e'; describe('MDC-based slide-toggle', () => { - const getInput = () => element(by.css('#normal-slide-toggle input')); + const getButton = () => element(by.css('#normal-slide-toggle button')); const getNormalToggle = () => element(by.css('#normal-slide-toggle')); beforeEach(async () => await browser.get('mdc-slide-toggle')); @@ -13,44 +13,44 @@ describe('MDC-based slide-toggle', () => { }); it('should change the checked state on click', async () => { - const inputEl = getInput(); + const buttonEl = getButton(); - expect(await inputEl.getAttribute('checked')) - .toBeFalsy('Expect slide-toggle to be unchecked'); + expect(await buttonEl.getAttribute('aria-checked')) + .toBe('false', 'Expect slide-toggle to be unchecked'); await getNormalToggle().click(); - expect(await inputEl.getAttribute('checked')) - .toBeTruthy('Expect slide-toggle to be checked'); + expect(await buttonEl.getAttribute('aria-checked')) + .toBe('true', 'Expect slide-toggle to be checked'); }); it('should change the checked state on click', async () => { - const inputEl = getInput(); + const buttonEl = getButton(); - expect(await inputEl.getAttribute('checked')) - .toBeFalsy('Expect slide-toggle to be unchecked'); + expect(await buttonEl.getAttribute('aria-checked')) + .toBe('false', 'Expect slide-toggle to be unchecked'); await getNormalToggle().click(); - expect(await inputEl.getAttribute('checked')) - .toBeTruthy('Expect slide-toggle to be checked'); + expect(await buttonEl.getAttribute('aria-checked')) + .toBe('true', 'Expect slide-toggle to be checked'); }); it('should not change the checked state on click when disabled', async () => { - const inputEl = getInput(); + const buttonEl = getButton(); - expect(await inputEl.getAttribute('checked')) - .toBeFalsy('Expect slide-toggle to be unchecked'); + expect(await buttonEl.getAttribute('aria-checked')) + .toBe('false', 'Expect slide-toggle to be unchecked'); await element(by.css('#disabled-slide-toggle')).click(); - expect(await inputEl.getAttribute('checked')) - .toBeFalsy('Expect slide-toggle to be unchecked'); + expect(await buttonEl.getAttribute('aria-checked')) + .toBe('false', 'Expect slide-toggle to be unchecked'); }); it('should move the thumb on state change', async () => { const slideToggleEl = getNormalToggle(); - const thumbEl = element(by.css('#normal-slide-toggle .mdc-switch__thumb-underlay')); + const thumbEl = element(by.css('#normal-slide-toggle .mdc-switch__handle')); const previousPosition = await thumbEl.getLocation(); await slideToggleEl.click(); @@ -61,15 +61,15 @@ describe('MDC-based slide-toggle', () => { }); it('should toggle the slide-toggle on space key', async () => { - const inputEl = getInput(); + const buttonEl = getButton(); - expect(await inputEl.getAttribute('checked')) - .toBeFalsy('Expect slide-toggle to be unchecked'); + expect(await buttonEl.getAttribute('aria-checked')) + .toBe('false', 'Expect slide-toggle to be unchecked'); - await inputEl.sendKeys(Key.SPACE); + await buttonEl.sendKeys(Key.SPACE); - expect(await inputEl.getAttribute('checked')) - .toBeTruthy('Expect slide-toggle to be checked'); + expect(await buttonEl.getAttribute('aria-checked')) + .toBe('true', 'Expect slide-toggle to be checked'); }); }); diff --git a/src/material-experimental/mdc-slide-toggle/slide-toggle.html b/src/material-experimental/mdc-slide-toggle/slide-toggle.html index 2e3a95febad8..2c92c287e977 100644 --- a/src/material-experimental/mdc-slide-toggle/slide-toggle.html +++ b/src/material-experimental/mdc-slide-toggle/slide-toggle.html @@ -1,33 +1,51 @@
-
+ -
diff --git a/src/material-experimental/mdc-slide-toggle/slide-toggle.scss b/src/material-experimental/mdc-slide-toggle/slide-toggle.scss index 8cedd7347bcf..0ae0c919ad3a 100644 --- a/src/material-experimental/mdc-slide-toggle/slide-toggle.scss +++ b/src/material-experimental/mdc-slide-toggle/slide-toggle.scss @@ -1,5 +1,6 @@ @use 'sass:map'; -@use '@material/switch/deprecated' as mdc-switch with ($deprecated-suffix: ''); +@use '@material/switch/switch' as mdc-switch; +@use '@material/switch/switch-theme' as mdc-switch-theme; @use '@material/form-field' as mdc-form-field; @use '@material/ripple' as mdc-ripple; @use '../mdc-helpers/mdc-helpers'; @@ -7,8 +8,8 @@ @use '../../material/core/style/private'; @use '../../cdk/a11y'; -@include mdc-switch.without-ripple($query: mdc-helpers.$mat-base-styles-query); @include mdc-form-field.core-styles($query: mdc-helpers.$mat-base-styles-query); +@include mdc-switch.static-styles-without-ripple; .mat-mdc-slide-toggle { display: inline-block; @@ -16,8 +17,14 @@ // Remove the native outline since we use the ripple for focus indication. outline: 0; + .mdc-switch { + // MDC theme styles also include structural styles so we have to include the theme at least + // once here. The values will be overwritten by our own theme file afterwards. + @include mdc-switch-theme.theme-styles(mdc-switch-theme.$light-theme); + } + // The ripple needs extra specificity so the base ripple styling doesn't override its `position`. - .mat-mdc-slide-toggle-ripple, .mdc-switch__thumb-underlay::after { + .mat-mdc-slide-toggle-ripple, #{mdc-switch.$ripple-target}::after { @include layout-common.fill; border-radius: 50%; // Disable pointer events for the ripple container so that it doesn't eat the mouse events meant @@ -31,32 +38,18 @@ } } - // The thumb-underlay element has `mat-mdc-focus-indicator` which sets - // a relative position. This element must have absolute positioning. This - // has increased specificity than the style set in MDC to guarantee that - // it will be absolutely positioned. - .mdc-switch__thumb-underlay { - position: absolute; - } - - // The MDC switch styles related to the hover state are intertwined with the MDC ripple styles. - // We currently don't use the MDC ripple due to size concerns, therefore we need to add some - // additional styles to restore the hover state. - // We use `::after`, rather than `::before`, because `::before` is used by the - // generic strong focus indication styles. - .mdc-switch__thumb-underlay::after { - border-radius: 50%; + #{mdc-switch.$ripple-target}::after { content: ''; opacity: 0; } - .mdc-switch:hover .mdc-switch__thumb-underlay::after { + .mdc-switch:hover #{mdc-switch.$ripple-target}::after { opacity: map.get(mdc-ripple.$dark-ink-opacities, hover); transition: mdc-switch-transition-enter(opacity, 0, 75ms); } // Needs a little more specificity so the :hover styles don't override it. - &.mat-mdc-slide-toggle-focused .mdc-switch .mdc-switch__thumb-underlay::after { + &.mat-mdc-slide-toggle-focused .mdc-switch #{mdc-switch.$ripple-target}::after { opacity: map.get(mdc-ripple.$dark-ink-opacities, focus); } @@ -65,33 +58,14 @@ .mat-ripple-element { opacity: map.get(mdc-ripple.$dark-ink-opacities, press); } - - .mat-ripple { - border-radius: 50%; - } - - // Angular Material supports disabling all animations when NoopAnimationsModule is imported. - // TODO(mmalerba): Look into using MDC's Sass queries to separate the animation styles and - // conditionally add them. Consider the size cost when deciding whether to switch. - &._mat-animation-noopable { - .mdc-switch__thumb-underlay, - .mdc-switch__thumb-underlay::after { - transition: none; - } - } } @include a11y.high-contrast(active, off) { - // Bump the opacity on the disable slide toggle to make it easier to see. - .mat-mdc-slide-toggle .mdc-switch--disabled { - opacity: 0.75; - } - .mat-mdc-slide-toggle-focused .mdc-switch__track { // Usually 1px would be enough, but MDC reduces the opacity on the // element so we need to make this a bit more prominent. outline: solid 2px; - outline-offset: private.private-div(mdc-switch.$track-height, 2); + outline-offset: private.private-div(map.get(mdc-switch-theme.$light-theme, track-height), 2); } } diff --git a/src/material-experimental/mdc-slide-toggle/slide-toggle.spec.ts b/src/material-experimental/mdc-slide-toggle/slide-toggle.spec.ts index bec7139ec940..e25d736236a9 100644 --- a/src/material-experimental/mdc-slide-toggle/slide-toggle.spec.ts +++ b/src/material-experimental/mdc-slide-toggle/slide-toggle.spec.ts @@ -41,7 +41,7 @@ describe('MDC-based MatSlideToggle without forms', () => { let slideToggle: MatSlideToggle; let slideToggleElement: HTMLElement; let labelElement: HTMLLabelElement; - let inputElement: HTMLInputElement; + let buttonElement: HTMLButtonElement; beforeEach(fakeAsync(() => { fixture = TestBed.createComponent(SlideToggleBasic); @@ -59,7 +59,7 @@ describe('MDC-based MatSlideToggle without forms', () => { testComponent = fixture.debugElement.componentInstance; slideToggle = slideToggleDebug.componentInstance; slideToggleElement = slideToggleDebug.nativeElement; - inputElement = fixture.debugElement.query(By.css('input'))!.nativeElement; + buttonElement = fixture.debugElement.query(By.css('button'))!.nativeElement; labelElement = fixture.debugElement.query(By.css('label'))!.nativeElement; })); @@ -76,28 +76,27 @@ describe('MDC-based MatSlideToggle without forms', () => { }); it('should correctly update the disabled property', () => { - expect(inputElement.disabled).toBeFalsy(); + expect(buttonElement.disabled).toBeFalsy(); testComponent.isDisabled = true; fixture.detectChanges(); - expect(inputElement.disabled).toBeTruthy(); + expect(buttonElement.disabled).toBeTruthy(); }); it('should correctly update the checked property', () => { expect(slideToggle.checked).toBeFalsy(); - expect(inputElement.getAttribute('aria-checked')).toBe('false'); + expect(buttonElement.getAttribute('aria-checked')).toBe('false'); testComponent.slideChecked = true; fixture.detectChanges(); - expect(inputElement.checked).toBeTruthy(); - expect(inputElement.getAttribute('aria-checked')).toBe('true'); + expect(buttonElement.getAttribute('aria-checked')).toBe('true'); }); it('should set the toggle to checked on click', () => { expect(slideToggle.checked).toBe(false); - expect(inputElement.getAttribute('aria-checked')).toBe('false'); + expect(buttonElement.getAttribute('aria-checked')).toBe('false'); expect(slideToggleElement.classList).not.toContain('mat-mdc-slide-toggle-checked'); labelElement.click(); @@ -105,13 +104,13 @@ describe('MDC-based MatSlideToggle without forms', () => { expect(slideToggleElement.classList).toContain('mat-mdc-slide-toggle-checked'); expect(slideToggle.checked).toBe(true); - expect(inputElement.getAttribute('aria-checked')).toBe('true'); + expect(buttonElement.getAttribute('aria-checked')).toBe('true'); }); it('should not trigger the click event multiple times', fakeAsync(() => { // By default, when clicking on a label element, a generated click will be dispatched - // on the associated input element. - // Since we're using a label element and a visual hidden input, this behavior can led + // on the associated button element. + // Since we're using a label element and a visual hidden button, this behavior can led // to an issue, where the click events on the slide-toggle are getting executed twice. expect(slideToggle.checked).toBe(false); @@ -127,133 +126,117 @@ describe('MDC-based MatSlideToggle without forms', () => { })); it('should trigger the change event properly', () => { - expect(inputElement.checked).toBe(false); expect(slideToggleElement.classList).not.toContain('mat-mdc-slide-toggle-checked'); labelElement.click(); fixture.detectChanges(); - expect(inputElement.checked).toBe(true); expect(slideToggleElement.classList).toContain('mat-mdc-slide-toggle-checked'); expect(testComponent.onSlideChange).toHaveBeenCalledTimes(1); }); it('should not trigger the change event by changing the native value', fakeAsync(() => { - expect(inputElement.checked).toBe(false); expect(slideToggleElement.classList).not.toContain('mat-mdc-slide-toggle-checked'); testComponent.slideChecked = true; fixture.detectChanges(); - expect(inputElement.checked).toBe(true); expect(slideToggleElement.classList).toContain('mat-mdc-slide-toggle-checked'); tick(); expect(testComponent.onSlideChange).not.toHaveBeenCalled(); })); - it('should not trigger the change event on initialization', fakeAsync(() => { - expect(inputElement.checked).toBe(false); - expect(slideToggleElement.classList).not.toContain('mat-mdc-slide-toggle-checked'); - - testComponent.slideChecked = true; - fixture.detectChanges(); - - expect(inputElement.checked).toBe(true); - expect(slideToggleElement.classList).toContain('mat-mdc-slide-toggle-checked'); - tick(); - - expect(testComponent.onSlideChange).not.toHaveBeenCalled(); - })); - - it('should add a suffix to the inputs id', () => { + it('should add a suffix to the element id', () => { testComponent.slideId = 'myId'; fixture.detectChanges(); expect(slideToggleElement.id).toBe('myId'); - expect(inputElement.id).toBe(`${slideToggleElement.id}-input`); + expect(buttonElement.id).toBe(`${slideToggleElement.id}-button`); testComponent.slideId = 'nextId'; fixture.detectChanges(); expect(slideToggleElement.id).toBe('nextId'); - expect(inputElement.id).toBe(`${slideToggleElement.id}-input`); + expect(buttonElement.id).toBe(`${slideToggleElement.id}-button`); testComponent.slideId = null; fixture.detectChanges(); // Once the id binding is set to null, the id property should auto-generate a unique id. - expect(inputElement.id).toMatch(/mat-mdc-slide-toggle-\d+-input/); + expect(buttonElement.id).toMatch(/mat-mdc-slide-toggle-\d+-button/); }); - it('should forward the tabIndex to the underlying input', () => { + it('should forward the tabIndex to the underlying element', () => { fixture.detectChanges(); - expect(inputElement.tabIndex).toBe(0); + expect(buttonElement.tabIndex).toBe(0); testComponent.slideTabindex = 4; fixture.detectChanges(); - expect(inputElement.tabIndex).toBe(4); + expect(buttonElement.tabIndex).toBe(4); }); - it('should forward the specified name to the input', () => { + it('should forward the specified name to the element', () => { testComponent.slideName = 'myName'; fixture.detectChanges(); - expect(inputElement.name).toBe('myName'); + expect(buttonElement.name).toBe('myName'); testComponent.slideName = 'nextName'; fixture.detectChanges(); - expect(inputElement.name).toBe('nextName'); + expect(buttonElement.name).toBe('nextName'); testComponent.slideName = null; fixture.detectChanges(); - expect(inputElement.name).toBe(''); + expect(buttonElement.name).toBe(''); }); - it('should forward the aria-label attribute to the input', () => { + it('should forward the aria-label attribute to the element', () => { testComponent.slideLabel = 'ariaLabel'; fixture.detectChanges(); - expect(inputElement.getAttribute('aria-label')).toBe('ariaLabel'); + expect(buttonElement.getAttribute('aria-label')).toBe('ariaLabel'); testComponent.slideLabel = null; fixture.detectChanges(); - expect(inputElement.hasAttribute('aria-label')).toBeFalsy(); + expect(buttonElement.hasAttribute('aria-label')).toBeFalsy(); }); - it('should forward the aria-labelledby attribute to the input', () => { + it('should forward the aria-labelledby attribute to the element', () => { testComponent.slideLabelledBy = 'ariaLabelledBy'; fixture.detectChanges(); - expect(inputElement.getAttribute('aria-labelledby')).toBe('ariaLabelledBy'); + expect(buttonElement.getAttribute('aria-labelledby')).toBe('ariaLabelledBy'); testComponent.slideLabelledBy = null; fixture.detectChanges(); - expect(inputElement.hasAttribute('aria-labelledby')).toBeFalsy(); + // We fall back to pointing to the label if a value isn't provided. + expect(buttonElement.getAttribute('aria-labelledby')) + .toMatch(/mat-mdc-slide-toggle-label-\d+/); }); - it('should forward the aria-describedby attribute to the input', () => { + it('should forward the aria-describedby attribute to the element', () => { testComponent.slideAriaDescribedBy = 'some-element'; fixture.detectChanges(); - expect(inputElement.getAttribute('aria-describedby')).toBe('some-element'); + expect(buttonElement.getAttribute('aria-describedby')).toBe('some-element'); testComponent.slideAriaDescribedBy = null; fixture.detectChanges(); - expect(inputElement.hasAttribute('aria-describedby')).toBe(false); + expect(buttonElement.hasAttribute('aria-describedby')).toBe(false); }); - it('should set the `for` attribute to the id of the input element', () => { + it('should set the `for` attribute to the id of the element', () => { expect(labelElement.getAttribute('for')).toBeTruthy(); - expect(inputElement.getAttribute('id')).toBeTruthy(); - expect(labelElement.getAttribute('for')).toBe(inputElement.getAttribute('id')); + expect(buttonElement.getAttribute('id')).toBeTruthy(); + expect(labelElement.getAttribute('for')).toBe(buttonElement.getAttribute('id')); }); it('should emit the new values properly', fakeAsync(() => { @@ -283,46 +266,46 @@ describe('MDC-based MatSlideToggle without forms', () => { testComponent.isRequired = true; fixture.detectChanges(); - expect(inputElement.required).toBe(true); + expect(buttonElement.getAttribute('aria-required')).toBe('true'); testComponent.isRequired = false; fixture.detectChanges(); - expect(inputElement.required).toBe(false); + expect(buttonElement.getAttribute('aria-required')).toBe('false'); }); - it('should focus on underlying input element when focus() is called', () => { - expect(document.activeElement).not.toBe(inputElement); + it('should focus on underlying element when focus() is called', () => { + expect(document.activeElement).not.toBe(buttonElement); slideToggle.focus(); fixture.detectChanges(); - expect(document.activeElement).toBe(inputElement); + expect(document.activeElement).toBe(buttonElement); }); - it('should focus on underlying input element when the host is focused', fakeAsync(() => { - expect(document.activeElement).not.toBe(inputElement); + it('should focus on underlying element when the host is focused', fakeAsync(() => { + expect(document.activeElement).not.toBe(buttonElement); slideToggleElement.focus(); fixture.detectChanges(); tick(); - expect(document.activeElement).toBe(inputElement); + expect(document.activeElement).toBe(buttonElement); })); - it('should not manually move focus to underlying input when focus comes from mouse or touch', + it('should not manually move focus to underlying when focus comes from mouse or touch', fakeAsync(inject([FocusMonitor], (focusMonitor: FocusMonitor) => { - expect(document.activeElement).not.toBe(inputElement); + expect(document.activeElement).not.toBe(buttonElement); focusMonitor.focusVia(slideToggleElement, 'mouse'); fixture.detectChanges(); flush(); - expect(document.activeElement).not.toBe(inputElement); + expect(document.activeElement).not.toBe(buttonElement); focusMonitor.focusVia(slideToggleElement, 'touch'); fixture.detectChanges(); flush(); - expect(document.activeElement).not.toBe(inputElement); + expect(document.activeElement).not.toBe(buttonElement); }))); it('should set a element class if labelPosition is set to before', () => { @@ -363,8 +346,8 @@ describe('MDC-based MatSlideToggle without forms', () => { }); it('should have a focus indicator', () => { - const underlayElement = slideToggleElement.querySelector('.mdc-switch__thumb-underlay')!; - expect(underlayElement.classList.contains('mat-mdc-focus-indicator')).toBe(true); + const rippleElement = slideToggleElement.querySelector('.mat-mdc-slide-toggle-ripple')!; + expect(rippleElement.classList).toContain('mat-mdc-focus-indicator'); }); }); @@ -444,7 +427,7 @@ describe('MDC-based MatSlideToggle without forms', () => { const slideToggleDebug = fixture.debugElement.query(By.css('mat-slide-toggle'))!; const slideToggle = slideToggleDebug.componentInstance; - const inputElement = fixture.debugElement.query(By.css('input'))!.nativeElement; + const buttonElement = fixture.debugElement.query(By.css('button'))!.nativeElement; const labelElement = fixture.debugElement.query(By.css('label'))!.nativeElement; expect(testComponent.toggleTriggered).toBe(0); @@ -459,7 +442,7 @@ describe('MDC-based MatSlideToggle without forms', () => { expect(testComponent.toggleTriggered).toBe(1, 'Expect toggle once'); expect(testComponent.dragTriggered).toBe(0); - inputElement.click(); + buttonElement.click(); fixture.detectChanges(); tick(); @@ -517,7 +500,7 @@ describe('MDC-based MatSlideToggle with forms', () => { let slideToggle: MatSlideToggle; let slideToggleElement: HTMLElement; let slideToggleModel: NgModel; - let inputElement: HTMLInputElement; + let buttonElement: HTMLButtonElement; let labelElement: HTMLLabelElement; // This initialization is async() because it needs to wait for ngModel to set the initial value. @@ -531,7 +514,7 @@ describe('MDC-based MatSlideToggle with forms', () => { slideToggle = slideToggleDebug.componentInstance; slideToggleElement = slideToggleDebug.nativeElement; slideToggleModel = slideToggleDebug.injector.get(NgModel); - inputElement = fixture.debugElement.query(By.css('input'))!.nativeElement; + buttonElement = fixture.debugElement.query(By.css('button'))!.nativeElement; labelElement = fixture.debugElement.query(By.css('label'))!.nativeElement; })); @@ -563,17 +546,16 @@ describe('MDC-based MatSlideToggle with forms', () => { // become dirty (not pristine), but remain untouched if focus is still there. slideToggle.checked = true; - // Dispatch a change event on the input element to fake a user interaction that triggered - // the state change. - dispatchFakeEvent(inputElement, 'change'); + dispatchFakeEvent(buttonElement, 'focus'); + buttonElement.click(); expect(slideToggleModel.valid).toBe(true); expect(slideToggleModel.pristine).toBe(false); expect(slideToggleModel.touched).toBe(false); - // Once the input element loses focus, the control should remain dirty but should + // Once the button element loses focus, the control should remain dirty but should // also turn touched. - dispatchFakeEvent(inputElement, 'blur'); + dispatchFakeEvent(buttonElement, 'blur'); fixture.detectChanges(); flushMicrotasks(); @@ -584,9 +566,9 @@ describe('MDC-based MatSlideToggle with forms', () => { it('should not throw an error when disabling while focused', fakeAsync(() => { expect(() => { - // Focus the input element because after disabling, the `blur` event should automatically + // Focus the button element because after disabling, the `blur` event should automatically // fire and not result in a changed after checked exception. Related: #12323 - inputElement.focus(); + buttonElement.focus(); tick(); fixture.componentInstance.isDisabled = true; @@ -608,9 +590,9 @@ describe('MDC-based MatSlideToggle with forms', () => { expect(slideToggleModel.touched).toBe(false); expect(slideToggleElement.classList).toContain('mat-mdc-slide-toggle-checked'); - // Once the input element loses focus, the control should remain dirty but should + // Once the button element loses focus, the control should remain dirty but should // also turn touched. - dispatchFakeEvent(inputElement, 'blur'); + dispatchFakeEvent(buttonElement, 'blur'); fixture.detectChanges(); flushMicrotasks(); @@ -649,7 +631,7 @@ describe('MDC-based MatSlideToggle with forms', () => { flushMicrotasks(); // Now the new checked variable has been updated in the slide-toggle and the slide-toggle - // is marked for check because it still needs to update the underlying input. + // is marked for check because it still needs to update the underlying button. fixture.detectChanges(); expect(slideToggle.checked) @@ -696,7 +678,7 @@ describe('MDC-based MatSlideToggle with forms', () => { let testComponent: SlideToggleWithFormControl; let slideToggle: MatSlideToggle; - let inputElement: HTMLInputElement; + let buttonElement: HTMLButtonElement; beforeEach(() => { fixture = TestBed.createComponent(SlideToggleWithFormControl); @@ -704,24 +686,24 @@ describe('MDC-based MatSlideToggle with forms', () => { testComponent = fixture.debugElement.componentInstance; slideToggle = fixture.debugElement.query(By.directive(MatSlideToggle))!.componentInstance; - inputElement = fixture.debugElement.query(By.css('input'))!.nativeElement; + buttonElement = fixture.debugElement.query(By.css('button'))!.nativeElement; }); it('should toggle the disabled state', () => { expect(slideToggle.disabled).toBe(false); - expect(inputElement.disabled).toBe(false); + expect(buttonElement.disabled).toBe(false); testComponent.formControl.disable(); fixture.detectChanges(); expect(slideToggle.disabled).toBe(true); - expect(inputElement.disabled).toBe(true); + expect(buttonElement.disabled).toBe(true); testComponent.formControl.enable(); fixture.detectChanges(); expect(slideToggle.disabled).toBe(false); - expect(inputElement.disabled).toBe(false); + expect(buttonElement.disabled).toBe(false); }); }); @@ -729,7 +711,6 @@ describe('MDC-based MatSlideToggle with forms', () => { let fixture: ComponentFixture; let testComponent: SlideToggleWithForm; let buttonElement: HTMLButtonElement; - let inputElement: HTMLInputElement; // This initialization is async() because it needs to wait for ngModel to set the initial value. beforeEach(fakeAsync(() => { @@ -738,32 +719,15 @@ describe('MDC-based MatSlideToggle with forms', () => { testComponent = fixture.debugElement.componentInstance; buttonElement = fixture.debugElement.query(By.css('button'))!.nativeElement; - inputElement = fixture.debugElement.query(By.css('input'))!.nativeElement; })); - it('should prevent the form from submit when being required', () => { - if (typeof (inputElement as any).reportValidity === 'undefined') { - // If the browser does not report the validity then the tests will break. - // e.g Safari 8 on Mobile. - return; - } - - testComponent.isRequired = true; - - fixture.detectChanges(); - - buttonElement.click(); - fixture.detectChanges(); - + it('should not submit the form when clicked', () => { expect(testComponent.isSubmitted).toBe(false); - testComponent.isRequired = false; - fixture.detectChanges(); - buttonElement.click(); fixture.detectChanges(); - expect(testComponent.isSubmitted).toBe(true); + expect(testComponent.isSubmitted).toBe(false); }); it('should have proper invalid state if unchecked', () => { @@ -777,7 +741,7 @@ describe('MDC-based MatSlideToggle with forms', () => { // The required slide-toggle will be checked and the form control // should become valid. - inputElement.click(); + buttonElement.click(); fixture.detectChanges(); expect(slideToggleEl.classList).not.toContain('ng-invalid'); @@ -785,7 +749,7 @@ describe('MDC-based MatSlideToggle with forms', () => { // The required slide-toggle will be unchecked and the form control // should become invalid. - inputElement.click(); + buttonElement.click(); fixture.detectChanges(); expect(slideToggleEl.classList).toContain('ng-invalid'); diff --git a/src/material-experimental/mdc-slide-toggle/slide-toggle.ts b/src/material-experimental/mdc-slide-toggle/slide-toggle.ts index 932b42a00922..56abba54c291 100644 --- a/src/material-experimental/mdc-slide-toggle/slide-toggle.ts +++ b/src/material-experimental/mdc-slide-toggle/slide-toggle.ts @@ -109,7 +109,7 @@ export class MatSlideToggle implements ControlValueAccessor, AfterViewInit, OnDe setNativeControlChecked: checked => this._checked = checked, setNativeControlDisabled: disabled => this._disabled = disabled, setNativeControlAttr: (name, value) => { - this._inputElement.nativeElement.setAttribute(name, value); + this._switchElement.nativeElement.setAttribute(name, value); } }; @@ -122,13 +122,16 @@ export class MatSlideToggle implements ControlValueAccessor, AfterViewInit, OnDe /** Whether noop animations are enabled. */ _noopAnimations: boolean; + /** Unique ID for the label element. */ + _labelId = `mat-mdc-slide-toggle-label-${++nextUniqueId}`; + /** The color palette for this slide toggle. */ @Input() color: ThemePalette; - /** Name value will be applied to the input element if present. */ + /** Name value will be applied to the button element if present. */ @Input() name: string | null = null; - /** A unique id for the slide-toggle input. If none is supplied, it will be auto-generated. */ + /** A unique id for the slide-toggle button. If none is supplied, it will be auto-generated. */ @Input() id: string = this._uniqueId; /** Tabindex for the input element. */ @@ -142,13 +145,13 @@ export class MatSlideToggle implements ControlValueAccessor, AfterViewInit, OnDe /** Whether the label should appear after or before the slide-toggle. Defaults to 'after'. */ @Input() labelPosition: 'before' | 'after' = 'after'; - /** Used to set the aria-label attribute on the underlying input element. */ + /** Used to set the aria-label attribute on the underlying button element. */ @Input('aria-label') ariaLabel: string | null = null; - /** Used to set the aria-labelledby attribute on the underlying input element. */ + /** Used to set the aria-labelledby attribute on the underlying button element. */ @Input('aria-labelledby') ariaLabelledby: string | null = null; - /** Used to set the aria-describedby attribute on the underlying input element. */ + /** Used to set the aria-describedby attribute on the underlying button element. */ @Input('aria-describedby') ariaDescribedby: string; /** Whether the slide-toggle is required. */ @@ -198,11 +201,8 @@ export class MatSlideToggle implements ControlValueAccessor, AfterViewInit, OnDe /** Event will be dispatched each time the slide-toggle input is toggled. */ @Output() readonly toggleChange: EventEmitter = new EventEmitter(); - /** Returns the unique id for the visual hidden input. */ - get inputId(): string { return `${this.id || this._uniqueId}-input`; } - - /** Reference to the underlying input element. */ - @ViewChild('input') _inputElement: ElementRef; + /** Returns the unique id for the visual hidden button. */ + get buttonId(): string { return `${this.id || this._uniqueId}-button`; } /** Reference to the MDC switch element. */ @ViewChild('switch') _switchElement: ElementRef; @@ -234,7 +234,7 @@ export class MatSlideToggle implements ControlValueAccessor, AfterViewInit, OnDe // 1. It can prevent clicks from landing in Chrome (see #18269). // 2. They're already handled by the wrapping `label` element. if (focusOrigin === 'keyboard' || focusOrigin === 'program') { - this._inputElement.nativeElement.focus(); + this._switchElement.nativeElement.focus(); this._focused = true; } else if (!focusOrigin) { // When a focused element becomes disabled, the browser *immediately* fires a blur event. @@ -253,48 +253,19 @@ export class MatSlideToggle implements ControlValueAccessor, AfterViewInit, OnDe ngOnDestroy() { this._focusMonitor.stopMonitoring(this._elementRef); - - if (this._foundation) { - this._foundation.destroy(); - } + this._foundation?.destroy(); } - /** Method being called whenever the underlying input emits a change event. */ - _onChangeEvent(event: Event) { - // We always have to stop propagation on the change event. - // Otherwise the change event, from the input element, will bubble up and - // emit its event object to the component's `change` output. - event.stopPropagation(); + /** Method being called whenever the underlying button is clicked. */ + _handleClick(event: Event) { this.toggleChange.emit(); this._foundation.handleChange(event); - // When the slide toggle's config disabled toggle change event by setting - // `disableToggleValue: true`, the slide toggle's value does not change, - // and the checked state of the underlying input needs to be changed back. - if (this.defaults.disableToggleValue) { - this._inputElement.nativeElement.checked = this.checked; - return; + if (!this.defaults.disableToggleValue) { + this.checked = !this.checked; + this._onChange(this.checked); + this.change.emit(new MatSlideToggleChange(this, this.checked)); } - - // Sync the value from the underlying input element with the component instance. - this.checked = this._inputElement.nativeElement.checked; - - // Emit our custom change event only if the underlying input emitted one. This ensures that - // there is no change event, when the checked state changes programmatically. - this._onChange(this.checked); - this.change.emit(new MatSlideToggleChange(this, this.checked)); - } - - /** Method being called whenever the slide-toggle has been clicked. */ - _onInputClick(event: Event) { - // We have to stop propagation for click events on the visual hidden input element. - // By default, when a user clicks on a label element, a generated click event will be - // dispatched on the associated input element. Since we are using a label element as our - // root container, the click event on the `slide-toggle` will be executed twice. - // The real click event will bubble up, and the generated click event also tries to bubble up. - // This will lead to multiple click events. - // Preventing bubbling for the second event will solve that issue. - event.stopPropagation(); } /** Implemented as part of ControlValueAccessor. */ @@ -321,7 +292,7 @@ export class MatSlideToggle implements ControlValueAccessor, AfterViewInit, OnDe /** Focuses the slide-toggle. */ focus(): void { - this._inputElement.nativeElement.focus(); + this._switchElement.nativeElement.focus(); } /** Toggles the checked state of the slide-toggle. */ @@ -330,6 +301,16 @@ export class MatSlideToggle implements ControlValueAccessor, AfterViewInit, OnDe this._onChange(this.checked); } + _getAriaLabelledBy() { + if (this.ariaLabelledby) { + return this.ariaLabelledby; + } + + // Even though we have a `label` element with a `for` pointing to the button, we need the + // `aria-labelledby`, because the button gets flagged as not having a label by tools like axe. + return this.ariaLabel ? null : this._labelId; + } + static ngAcceptInputType_tabIndex: NumberInput; static ngAcceptInputType_required: BooleanInput; static ngAcceptInputType_checked: BooleanInput; diff --git a/src/material-experimental/mdc-slide-toggle/testing/BUILD.bazel b/src/material-experimental/mdc-slide-toggle/testing/BUILD.bazel index 98ab73b1d6ee..0641d6595c59 100644 --- a/src/material-experimental/mdc-slide-toggle/testing/BUILD.bazel +++ b/src/material-experimental/mdc-slide-toggle/testing/BUILD.bazel @@ -9,6 +9,7 @@ ts_library( exclude = ["**/*.spec.ts"], ), deps = [ + "//src/cdk/coercion", "//src/cdk/testing", "//src/material/slide-toggle/testing", ], diff --git a/src/material-experimental/mdc-slide-toggle/testing/slide-toggle-harness.ts b/src/material-experimental/mdc-slide-toggle/testing/slide-toggle-harness.ts index 9ab7084934f3..984dfe96839f 100644 --- a/src/material-experimental/mdc-slide-toggle/testing/slide-toggle-harness.ts +++ b/src/material-experimental/mdc-slide-toggle/testing/slide-toggle-harness.ts @@ -7,6 +7,7 @@ */ import {HarnessPredicate} from '@angular/cdk/testing'; +import {coerceBooleanProperty} from '@angular/cdk/coercion'; import { _MatSlideToggleHarnessBase, SlideToggleHarnessFilters @@ -15,6 +16,7 @@ import { /** Harness for interacting with a MDC-based mat-slide-toggle in tests. */ export class MatSlideToggleHarness extends _MatSlideToggleHarnessBase { + protected _nativeElement = this.locatorFor('button'); static hostSelector = '.mat-mdc-slide-toggle'; /** @@ -34,10 +36,17 @@ export class MatSlideToggleHarness extends _MatSlideToggleHarnessBase { .addOption('name', options.name, async (harness, name) => await harness.getName() === name); } - private _inputContainer = this.locatorFor('.mdc-switch'); - async toggle(): Promise { - const elToClick = await this.isDisabled() ? this._inputContainer() : this._input(); - return (await elToClick).click(); + return (await this._nativeElement()).click(); + } + + override async isRequired(): Promise { + const ariaRequired = await (await this._nativeElement()).getAttribute('aria-required'); + return ariaRequired === 'true'; + } + + async isChecked(): Promise { + const checked = (await this._nativeElement()).getAttribute('aria-checked'); + return coerceBooleanProperty(await checked); } } diff --git a/src/material/slide-toggle/slide-toggle.spec.ts b/src/material/slide-toggle/slide-toggle.spec.ts index c4c9326c4409..792a3f09e338 100644 --- a/src/material/slide-toggle/slide-toggle.spec.ts +++ b/src/material/slide-toggle/slide-toggle.spec.ts @@ -180,7 +180,7 @@ describe('MatSlideToggle without forms', () => { expect(testComponent.onSlideChange).not.toHaveBeenCalled(); })); - it('should add a suffix to the inputs id', () => { + it('should add a suffix to the element id', () => { testComponent.slideId = 'myId'; fixture.detectChanges(); @@ -200,7 +200,7 @@ describe('MatSlideToggle without forms', () => { expect(inputElement.id).toMatch(/mat-slide-toggle-\d+-input/); }); - it('should forward the tabIndex to the underlying input', () => { + it('should forward the tabIndex to the underlying element', () => { fixture.detectChanges(); expect(inputElement.tabIndex).toBe(0); @@ -211,7 +211,7 @@ describe('MatSlideToggle without forms', () => { expect(inputElement.tabIndex).toBe(4); }); - it('should forward the specified name to the input', () => { + it('should forward the specified name to the element', () => { testComponent.slideName = 'myName'; fixture.detectChanges(); @@ -228,7 +228,7 @@ describe('MatSlideToggle without forms', () => { expect(inputElement.name).toBe(''); }); - it('should forward the aria-label attribute to the input', () => { + it('should forward the aria-label attribute to the element', () => { testComponent.slideLabel = 'ariaLabel'; fixture.detectChanges(); @@ -240,7 +240,7 @@ describe('MatSlideToggle without forms', () => { expect(inputElement.hasAttribute('aria-label')).toBeFalsy(); }); - it('should forward the aria-labelledby attribute to the input', () => { + it('should forward the aria-labelledby attribute to the element', () => { testComponent.slideLabelledBy = 'ariaLabelledBy'; fixture.detectChanges(); @@ -252,7 +252,7 @@ describe('MatSlideToggle without forms', () => { expect(inputElement.hasAttribute('aria-labelledby')).toBeFalsy(); }); - it('should forward the aria-describedby attribute to the input', () => { + it('should forward the aria-describedby attribute to the element', () => { testComponent.slideAriaDescribedBy = 'some-element'; fixture.detectChanges(); @@ -264,7 +264,7 @@ describe('MatSlideToggle without forms', () => { expect(inputElement.hasAttribute('aria-describedby')).toBe(false); }); - it('should set the `for` attribute to the id of the input element', () => { + it('should set the `for` attribute to the id of the element', () => { expect(labelElement.getAttribute('for')).toBeTruthy(); expect(inputElement.getAttribute('id')).toBeTruthy(); expect(labelElement.getAttribute('for')).toBe(inputElement.getAttribute('id')); @@ -305,7 +305,7 @@ describe('MatSlideToggle without forms', () => { expect(inputElement.required).toBe(false); }); - it('should focus on underlying input element when focus() is called', () => { + it('should focus on underlying element when focus() is called', () => { expect(document.activeElement).not.toBe(inputElement); slideToggle.focus(); @@ -314,7 +314,7 @@ describe('MatSlideToggle without forms', () => { expect(document.activeElement).toBe(inputElement); }); - it('should focus on underlying input element when the host is focused', () => { + it('should focus on underlying element when the host is focused', () => { expect(document.activeElement).not.toBe(inputElement); slideToggleElement.focus(); @@ -323,7 +323,7 @@ describe('MatSlideToggle without forms', () => { expect(document.activeElement).toBe(inputElement); }); - it('should not manually move focus to underlying input when focus comes from mouse or touch', + it('should not manually move focus to underlying when focus comes from mouse or touch', inject([FocusMonitor], (focusMonitor: FocusMonitor) => { expect(document.activeElement).not.toBe(inputElement); diff --git a/src/material/slide-toggle/testing/shared.spec.ts b/src/material/slide-toggle/testing/shared.spec.ts index b8f7b3dc7da3..fe91a36bd978 100644 --- a/src/material/slide-toggle/testing/shared.spec.ts +++ b/src/material/slide-toggle/testing/shared.spec.ts @@ -144,7 +144,7 @@ export function runHarnessTests( it('should not toggle disabled slide-toggle', async () => { if (platform.FIREFOX) { - // do run this test on firefox as click events on the label of the underlying + // do not run this test on firefox as click events on the label of the underlying // input checkbox cause the value to be changed. Read more in the bug report: // https://bugzilla.mozilla.org/show_bug.cgi?id=1540995 return; diff --git a/src/material/slide-toggle/testing/slide-toggle-harness.ts b/src/material/slide-toggle/testing/slide-toggle-harness.ts index c7168ad6237f..782a2bd158cc 100644 --- a/src/material/slide-toggle/testing/slide-toggle-harness.ts +++ b/src/material/slide-toggle/testing/slide-toggle-harness.ts @@ -6,32 +6,34 @@ * found in the LICENSE file at https://angular.io/license */ -import {ComponentHarness, HarnessPredicate} from '@angular/cdk/testing'; +import { + AsyncFactoryFn, + ComponentHarness, + HarnessPredicate, + TestElement, +} from '@angular/cdk/testing'; import {coerceBooleanProperty} from '@angular/cdk/coercion'; import {SlideToggleHarnessFilters} from './slide-toggle-harness-filters'; export abstract class _MatSlideToggleHarnessBase extends ComponentHarness { private _label = this.locatorFor('label'); - protected _input = this.locatorFor('input'); + protected abstract _nativeElement: AsyncFactoryFn; /** Toggle the checked state of the slide-toggle. */ abstract toggle(): Promise; /** Whether the slide-toggle is checked. */ - async isChecked(): Promise { - const checked = (await this._input()).getProperty('checked'); - return coerceBooleanProperty(await checked); - } + abstract isChecked(): Promise; /** Whether the slide-toggle is disabled. */ async isDisabled(): Promise { - const disabled = (await this._input()).getAttribute('disabled'); + const disabled = (await this._nativeElement()).getAttribute('disabled'); return coerceBooleanProperty(await disabled); } /** Whether the slide-toggle is required. */ async isRequired(): Promise { - const required = (await this._input()).getAttribute('required'); + const required = (await this._nativeElement()).getAttribute('required'); return coerceBooleanProperty(await required); } @@ -43,17 +45,17 @@ export abstract class _MatSlideToggleHarnessBase extends ComponentHarness { /** Gets the slide-toggle's name. */ async getName(): Promise { - return (await this._input()).getAttribute('name'); + return (await this._nativeElement()).getAttribute('name'); } /** Gets the slide-toggle's aria-label. */ async getAriaLabel(): Promise { - return (await this._input()).getAttribute('aria-label'); + return (await this._nativeElement()).getAttribute('aria-label'); } /** Gets the slide-toggle's aria-labelledby. */ async getAriaLabelledby(): Promise { - return (await this._input()).getAttribute('aria-labelledby'); + return (await this._nativeElement()).getAttribute('aria-labelledby'); } /** Gets the slide-toggle's label text. */ @@ -63,17 +65,17 @@ export abstract class _MatSlideToggleHarnessBase extends ComponentHarness { /** Focuses the slide-toggle. */ async focus(): Promise { - return (await this._input()).focus(); + return (await this._nativeElement()).focus(); } /** Blurs the slide-toggle. */ async blur(): Promise { - return (await this._input()).blur(); + return (await this._nativeElement()).blur(); } /** Whether the slide-toggle is focused. */ async isFocused(): Promise { - return (await this._input()).isFocused(); + return (await this._nativeElement()).isFocused(); } /** @@ -101,6 +103,9 @@ export abstract class _MatSlideToggleHarnessBase extends ComponentHarness { /** Harness for interacting with a standard mat-slide-toggle in tests. */ export class MatSlideToggleHarness extends _MatSlideToggleHarnessBase { + private _inputContainer = this.locatorFor('.mat-slide-toggle-bar'); + protected _nativeElement = this.locatorFor('input'); + /** The selector for the host element of a `MatSlideToggle` instance. */ static hostSelector = '.mat-slide-toggle'; @@ -120,10 +125,14 @@ export class MatSlideToggleHarness extends _MatSlideToggleHarnessBase { .addOption('name', options.name, async (harness, name) => await harness.getName() === name); } - private _inputContainer = this.locatorFor('.mat-slide-toggle-bar'); - /** Toggle the checked state of the slide-toggle. */ async toggle(): Promise { return (await this._inputContainer()).click(); } + + /** Whether the slide-toggle is checked. */ + async isChecked(): Promise { + const checked = (await this._nativeElement()).getProperty('checked'); + return coerceBooleanProperty(await checked); + } } diff --git a/tools/public_api_guard/material/slide-toggle-testing.md b/tools/public_api_guard/material/slide-toggle-testing.md index 5e10132b0f4d..b710f424546c 100644 --- a/tools/public_api_guard/material/slide-toggle-testing.md +++ b/tools/public_api_guard/material/slide-toggle-testing.md @@ -13,6 +13,9 @@ import { TestElement } from '@angular/cdk/testing'; // @public export class MatSlideToggleHarness extends _MatSlideToggleHarnessBase { static hostSelector: string; + isChecked(): Promise; + // (undocumented) + protected _nativeElement: AsyncFactoryFn; toggle(): Promise; static with(options?: SlideToggleHarnessFilters): HarnessPredicate; } @@ -26,13 +29,13 @@ export abstract class _MatSlideToggleHarnessBase extends ComponentHarness { getAriaLabelledby(): Promise; getLabelText(): Promise; getName(): Promise; - // (undocumented) - protected _input: AsyncFactoryFn; - isChecked(): Promise; + abstract isChecked(): Promise; isDisabled(): Promise; isFocused(): Promise; isRequired(): Promise; isValid(): Promise; + // (undocumented) + protected abstract _nativeElement: AsyncFactoryFn; abstract toggle(): Promise; uncheck(): Promise; }