Skip to content

fix(material/dialog): reserve room for touch targets in scrollable content - #33676

Open
ManicardiFrancesco wants to merge 1 commit into
angular:mainfrom
ManicardiFrancesco:fix-dialog-content-touch-target-overflow
Open

fix(material/dialog): reserve room for touch targets in scrollable content#33676
ManicardiFrancesco wants to merge 1 commit into
angular:mainfrom
ManicardiFrancesco:fix-dialog-content-touch-target-overflow

Conversation

@ManicardiFrancesco

Copy link
Copy Markdown
Contributor

What this fixes

A dialog whose content ends with a button, checkbox, radio or slide toggle paints a vertical scrollbar over content that is fully visible.

mat-dialog-content is a scroll container (overflow: auto) and its block-end padding is 0 when the dialog has an actions row (dialog-with-actions-content-padding: 20px 24px 0). Those controls center an absolutely positioned touch target on a smaller control — 48px on a 40px button/checkbox/radio, 48px on a 32px switch — so the target overhangs the control by 4–8px. An absolutely positioned box still counts towards its scroll container's block-end scrollable overflow, so the content scrolls by those invisible pixels.

This is why the same dialog is fine without an actions row: dialog-content-padding ends in 20px, which absorbs the overhang.

This change reserves the overhang in the block-end padding, so the touch targets keep their accessible size. It addresses the concern raised in #29164, that the control "is supposed to have a 40px height visually and layout-wise, but for accessibility reasons it needs to be a minimum of 48px" — the container gives the target room instead of the control giving up size.

Fixes #29164. Same root cause as #4764, #4748, #23565 and #26176.

Easy repro

Any dialog with an actions row and a trailing control:

<h2 mat-dialog-title>Hi</h2>
<mat-dialog-content>
  <p>What's your favorite animal?</p>
  <mat-checkbox>Remember my answer</mat-checkbox>
</mat-dialog-content>
<mat-dialog-actions>
  <button matButton mat-dialog-close>Ok</button>
</mat-dialog-actions>

The content fits, but the dialog scrolls by 4px. document.querySelector('mat-dialog-content') reports scrollHeight - clientHeight === 4, and the offending box is .mat-mdc-checkbox-touch-target. Removing the checkbox, or giving it margin-bottom: 4px, makes the scrollbar go away — which is why this tends to get misdiagnosed as an app-level styling problem.

The geometry alone reproduces without Angular, in any browser:

<div style="width: 300px">
  <!-- stands in for mat-dialog-content: scrolls, no block-end padding -->
  <div id="content" style="overflow: auto; padding: 20px 24px 0">
    <p style="margin: 0 0 16px">Content that fits.</p>
    <div style="display: flex; align-items: center">
      <!-- stands in for a 40px control with a 48px touch target -->
      <div style="position: relative; width: 40px; height: 40px">
        <div style="position: absolute; top: 50%; left: 50%; width: 48px; height: 48px;
                    transform: translate(-50%, -50%)"></div>
      </div>
      <label>Remember my answer</label>
    </div>
  </div>
  <div style="padding: 16px 24px; min-height: 52px">Ok</div>
</div>
<script>
  const c = document.getElementById('content');
  console.log(c.scrollHeight - c.clientHeight); // 4
</script>

Verification

Stock Angular Material app, no custom CSS, default density, measured as scrollHeight - clientHeight on mat-dialog-content before and after the change:

Dialog content ends with Chromium Firefox WebKit
checkbox (#4764) 4px → 0 4px → 0 4px → 0
radio button 4px → 0 4px → 0 4px → 0
slide toggle (#23565) 8px → 0 8px → 0 8px → 0
button (#29164) 4px → 0 4px → 0 4px → 0
form field (no touch target) 0 → 0 0 → 0 0 → 0
the dialog overview example, verbatim 0 → 0 0 → 0 0 → 0

Trade-off, and how to avoid it

Dialogs with an actions row get 8px taller, and the gap between the content and the actions goes from 16px to 24px. If you would rather keep the current spacing exactly, the reservation can be compensated on the actions row, which leaves both the dialog height and the visible gap unchanged:

.mat-mdc-dialog-actions {
  // The content reserves room for its controls' touch targets, so drop the same amount here.
  .mat-mdc-dialog-content + & {
    padding-block-start: 8px;
  }
}

I left that out of the change because it overrides the block-start half of a consumer's own dialog-actions-padding. Happy to add it, or to reserve only 4px (which covers buttons, checkboxes and radios but leaves 4px for slide toggles), if either is preferred.

Alternatives that do not work

  • touch-target-display: none on the controls inside dialog content — the workaround suggested in bug(material/button): Components with absolutely positioned touch targets (like mat-button) can cause scrollbar to appear. #26176. It fixes the scrollbar in every engine, but shrinks the tap area to the 40px control, which is the thing the touch target exists to prevent.
  • Anchoring the touch target to the block-end/inline-end edges so it only overhangs towards the block-start and inline-start, which are never scrollable. This looked ideal (no layout change, full 48px target still hit-testable at both extremes, verified with elementFromPoint) and fixed Chromium and Firefox, but a stock dialog still measured 4px of overflow in WebKit, so it is not a reliable fix.
  • overflow: clip + overflow-clip-margin on the control, to keep the target painted and hit-testable but out of the ancestor's overflow. Chromium and Firefox still reported the full 4px, and WebKit does not support overflow-clip-margin (CSS.supports is false), where it clipped the target out of hit-testing instead — elementFromPoint in the overhang returned the scroll container.
  • :has() on the content to only pay the padding when a control is present — ruled out by the existing note in dialog.scss, that the added specificity breaks internal clients.

Notes

  • CSS-only, no API or token-name changes; dialog-with-actions-content-padding remains overridable.
  • No unit test: dialog.spec.ts has no layout/geometry assertions to extend, and this is not observable without real component CSS. Verified manually in the three engines as above.
  • The M2 token map is unaffected — dialog-with-actions-content-padding there is 20px 24px, which already absorbs the overhang.

…ntent

`mat-dialog-content` scrolls, and its block-end padding is zero when the dialog has an
actions row. Buttons, checkboxes and radios center a 48px touch target on a 40px control,
and slide toggles center one on a 32px switch, so the target overhangs the control by 4-8px.
An overhanging target at the end of the content is scrollable overflow, so a dialog whose
content ends in one of those controls paints a scrollbar over content that fits.

Reserve the overhang in the block-end padding instead. This keeps the touch targets at their
accessible size, which the alternatives do not: disabling them
(`touch-target-display: none`) shrinks the tap area, and anchoring them so they only overhang
the block-start edge still left 4px of overflow in WebKit in local testing.

Verified with a stock Material app in Chromium, Firefox and WebKit. A dialog with a trailing
checkbox, radio, slide toggle or button goes from 4px, 4px, 8px and 4px of scrollable
overflow respectively to 0 in all three engines. Dialogs with actions grow by 8px, which is
the trade-off for not touching the tap areas.

Fixes angular#29164.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(Material 3 > Button): When button is last child element in mat-dialog-content it causes a vertical scrollbar to appear

1 participant