From 2d9c5b867b52c03fcf82c414ab4aff4e3f8ea25f Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Fri, 9 Oct 2026 15:23:57 -0400 Subject: [PATCH 1/5] refactor(mosaic): size the fullscreen profile from the profile, not the dialog popup --- .../src/components/dialog/__tests__/dialog.test.tsx | 8 ++------ .../mosaic/src/components/dialog/dialog.styles.ts | 6 ------ packages/mosaic/src/components/dialog/dialog.tsx | 2 -- .../mosaic/src/components/profile/profile.styles.ts | 2 +- .../mosaic/src/components/profile/profile.test.tsx | 12 ++++++++++++ 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/mosaic/src/components/dialog/__tests__/dialog.test.tsx b/packages/mosaic/src/components/dialog/__tests__/dialog.test.tsx index ff7309a4c9a..eb2eb20cb6a 100644 --- a/packages/mosaic/src/components/dialog/__tests__/dialog.test.tsx +++ b/packages/mosaic/src/components/dialog/__tests__/dialog.test.tsx @@ -639,12 +639,8 @@ describe('compactPlacement', () => { describe('fullscreen track', () => { const probe = stylex.create({ noInset: { paddingInline: 0 }, fill: { width: '100%' } }); - it('fills the track, where other variants fit their content', () => { - const fullscreen = renderVariant('fullscreen'); - expect(classesOf('.cl-dialog-popup')).toEqual(expect.arrayContaining(atomFor(probe.fill))); - fullscreen.unmount(); - - renderVariant('profile'); + it('fits its surface like every other variant, leaving the width to the profile', () => { + renderVariant('fullscreen'); expect(classesOf('.cl-dialog-popup')).not.toEqual(expect.arrayContaining(atomFor(probe.fill))); }); diff --git a/packages/mosaic/src/components/dialog/dialog.styles.ts b/packages/mosaic/src/components/dialog/dialog.styles.ts index 7ab4113b020..534ad6a3076 100644 --- a/packages/mosaic/src/components/dialog/dialog.styles.ts +++ b/packages/mosaic/src/components/dialog/dialog.styles.ts @@ -365,12 +365,6 @@ export const trackVariants = stylex.create({ }, }); -export const fullscreenPopup = stylex.create({ - fill: { - width: '100%', - }, -}); - export const variants = stylex.create({ // The surface is a `Card` — so it comes from `Card`'s own `elevations.overlay`, and the popup // contributes geometry and motion. Compose it by rendering the card INSIDE the popup: diff --git a/packages/mosaic/src/components/dialog/dialog.tsx b/packages/mosaic/src/components/dialog/dialog.tsx index 65a490dd90e..32cb40b5261 100644 --- a/packages/mosaic/src/components/dialog/dialog.tsx +++ b/packages/mosaic/src/components/dialog/dialog.tsx @@ -20,7 +20,6 @@ import { backdropMotion, closeInsets, compactPlacements, - fullscreenPopup, popupMotion, styles, trackCompactPlacements, @@ -421,7 +420,6 @@ const Popup = React.forwardRef(function Dialog reset.base, styles.popup, variants[surfaceOf(variant)], - variant === 'fullscreen' && fullscreenPopup.fill, compactPlacements[compactPlacement], // One cell per (variant, placement) that exists, selected rather than layered: StyleX // dedupes by PROPERTY across a `stylex.props` call, so a thin "sheet only" atom would diff --git a/packages/mosaic/src/components/profile/profile.styles.ts b/packages/mosaic/src/components/profile/profile.styles.ts index 471f2d31911..f4e60f603c7 100644 --- a/packages/mosaic/src/components/profile/profile.styles.ts +++ b/packages/mosaic/src/components/profile/profile.styles.ts @@ -115,7 +115,7 @@ export const styles = stylex.create({ }, rootFullscreen: { - width: '100%', + inlineSize: '100cqi', }, layoutFullscreen: { diff --git a/packages/mosaic/src/components/profile/profile.test.tsx b/packages/mosaic/src/components/profile/profile.test.tsx index 2c616952ddc..d5df6fefdbd 100644 --- a/packages/mosaic/src/components/profile/profile.test.tsx +++ b/packages/mosaic/src/components/profile/profile.test.tsx @@ -655,6 +655,18 @@ describe('Profile', () => { expect(onOpenChange).toHaveBeenCalledWith(false, expect.anything()); }); + it('spans the dialog by its own width, where a profile dialog fits its content', () => { + const spans = atomsOf(stylex.create({ spans: { inlineSize: '100cqi' } }).spans); + const root = () => Array.from(document.querySelector('.cl-profile')!.classList); + + const fullscreen = renderFullscreen(); + expect(root()).toEqual(expect.arrayContaining(spans)); + fullscreen.unmount(); + + renderInDialog(); + expect(root()).not.toEqual(expect.arrayContaining(spans)); + }); + it('reads the back button label from the catalog', () => { renderFullscreen(vi.fn(), { overrides: { 'profile.backToApp': 'Retour à l’application' } }); From 95130b88b894df94725fd35fa5ec7282544e4748 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Fri, 9 Oct 2026 15:26:23 -0400 Subject: [PATCH 2/5] test(mosaic): drop the class-probe tests for the fullscreen profile --- .../dialog/__tests__/dialog.test.tsx | 21 +------------------ .../src/components/profile/profile.test.tsx | 12 ----------- 2 files changed, 1 insertion(+), 32 deletions(-) diff --git a/packages/mosaic/src/components/dialog/__tests__/dialog.test.tsx b/packages/mosaic/src/components/dialog/__tests__/dialog.test.tsx index eb2eb20cb6a..7a13072242e 100644 --- a/packages/mosaic/src/components/dialog/__tests__/dialog.test.tsx +++ b/packages/mosaic/src/components/dialog/__tests__/dialog.test.tsx @@ -617,11 +617,10 @@ describe('compactPlacement', () => { warn.mockRestore(); }); - it('ignores a placement on a fullscreen profile, and warns', () => { + it('warns when a fullscreen profile is given a placement', () => { const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); renderPlacement('sheet', 'fullscreen'); - expect(classesOf('.cl-dialog-popup')).not.toEqual(expect.arrayContaining(atomFor(probe.anchored))); expect(warn).toHaveBeenCalledWith(expect.stringContaining('variant="fullscreen"')); warn.mockRestore(); }); @@ -636,24 +635,6 @@ describe('compactPlacement', () => { }); }); -describe('fullscreen track', () => { - const probe = stylex.create({ noInset: { paddingInline: 0 }, fill: { width: '100%' } }); - - it('fits its surface like every other variant, leaving the width to the profile', () => { - renderVariant('fullscreen'); - expect(classesOf('.cl-dialog-popup')).not.toEqual(expect.arrayContaining(atomFor(probe.fill))); - }); - - it('has no inset around the popup, where a profile keeps one', () => { - const fullscreen = renderVariant('fullscreen'); - expect(classesOf('.cl-dialog-track')).toEqual(expect.arrayContaining(atomFor(probe.noInset))); - fullscreen.unmount(); - - renderVariant('profile'); - expect(classesOf('.cl-dialog-track')).not.toEqual(expect.arrayContaining(atomFor(probe.noInset))); - }); -}); - describe('viewport scroll behaviour', () => { // The inside/outside scroll split. A pinned `height: 100%` cannot grow, so an over-tall popup // spills past the viewport's padding box and loses the bottom inset; `min-height: 100%` lets the diff --git a/packages/mosaic/src/components/profile/profile.test.tsx b/packages/mosaic/src/components/profile/profile.test.tsx index d5df6fefdbd..2c616952ddc 100644 --- a/packages/mosaic/src/components/profile/profile.test.tsx +++ b/packages/mosaic/src/components/profile/profile.test.tsx @@ -655,18 +655,6 @@ describe('Profile', () => { expect(onOpenChange).toHaveBeenCalledWith(false, expect.anything()); }); - it('spans the dialog by its own width, where a profile dialog fits its content', () => { - const spans = atomsOf(stylex.create({ spans: { inlineSize: '100cqi' } }).spans); - const root = () => Array.from(document.querySelector('.cl-profile')!.classList); - - const fullscreen = renderFullscreen(); - expect(root()).toEqual(expect.arrayContaining(spans)); - fullscreen.unmount(); - - renderInDialog(); - expect(root()).not.toEqual(expect.arrayContaining(spans)); - }); - it('reads the back button label from the catalog', () => { renderFullscreen(vi.fn(), { overrides: { 'profile.backToApp': 'Retour à l’application' } }); From c2ca5ce5738974c0b2593d9da92f41bd36f2705a Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Fri, 9 Oct 2026 15:38:32 -0400 Subject: [PATCH 3/5] refactor(mosaic): make fullscreen a real dialog variant and resolve the profile dismiss once --- .../mosaic-fullscreen-profile-refactor.md | 2 + .../src/components/dialog/dialog.styles.ts | 150 ++++++++++-------- .../mosaic/src/components/dialog/dialog.tsx | 42 ++--- .../src/components/profile/profile.context.ts | 4 +- .../src/components/profile/profile.styles.ts | 4 - .../mosaic/src/components/profile/profile.tsx | 25 +-- 6 files changed, 119 insertions(+), 108 deletions(-) create mode 100644 .changeset/mosaic-fullscreen-profile-refactor.md diff --git a/.changeset/mosaic-fullscreen-profile-refactor.md b/.changeset/mosaic-fullscreen-profile-refactor.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/mosaic-fullscreen-profile-refactor.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/mosaic/src/components/dialog/dialog.styles.ts b/packages/mosaic/src/components/dialog/dialog.styles.ts index 534ad6a3076..4dca4aa3bac 100644 --- a/packages/mosaic/src/components/dialog/dialog.styles.ts +++ b/packages/mosaic/src/components/dialog/dialog.styles.ts @@ -269,6 +269,8 @@ export const styles = stylex.create({ }, }); +const profileCloseInset = { insetBlockStart: space['3'], insetInlineEnd: space['3'] }; + /** * Positions the ICON the surface's inset from the corner, not the button box: the `sm` circle * carries `(space[7] - space[4]) / 2` = `space[1.5]` of its own padding around the glyph, so each @@ -277,9 +279,27 @@ export const styles = stylex.create({ */ export const closeInsets = stylex.create({ card: { insetBlockStart: space['2.5'], insetInlineEnd: space['2.5'] }, - profile: { insetBlockStart: space['3'], insetInlineEnd: space['3'] }, + profile: profileCloseInset, + fullscreen: profileCloseInset, }); +const profileViewport = { + // A definite container height is NOT enough on its own: an `auto` grid row still sizes to its + // content and happily exceeds the container, which is how a profile of rows measured 2208px + // inside a 1251px overlay. `minmax(0, 1fr)` pins the single row to the content box, so the row + // is what an item stretches to and what its overflow is measured against. + // + // Deliberately NOT applied to the scrolling variants: it would clamp the row there too, which is + // exactly what has to stop happening for the popup to grow past the fold. + gridTemplateRows: 'minmax(0, 1fr)', + // A DEFINITE height, taken from the overlay (`position: fixed; inset: 0`), which makes the + // single grid row definite too. That is what lets `variants.profile` fill the content box with + // `align-self: stretch` alone — no `dvh` arithmetic, so nothing can disagree with the box a + // bottom-anchored sheet aligns to. They genuinely do diverge: on an emulated iPhone the + // overlay measures 1251px while `100dvh` reports 844. + height: '100%', +}; + /** * Named for what the surface IS rather than for a t-shirt step, because these are different * surfaces rather than one surface at two scales — the names stay honest if they later diverge @@ -294,6 +314,8 @@ export const closeInsets = stylex.create({ * `profile` fixes the height. Its content NAVIGATES — a settings surface switches sections in * place — and a content-driven height would resize the window on every section change, in both * directions at once since the viewport centers it. Its width is the surface's own. + * + * `fullscreen` is a `profile` with no frame of overlay around it, at every width. */ /** * How the viewport behaves when the popup is taller than the screen — the "inside scroll" vs @@ -314,22 +336,8 @@ export const closeInsets = stylex.create({ */ export const viewportVariants = stylex.create({ card: { minHeight: '100%' }, - profile: { - // A definite container height is NOT enough on its own: an `auto` grid row still sizes to its - // content and happily exceeds the container, which is how a profile of rows measured 2208px - // inside a 1251px overlay. `minmax(0, 1fr)` pins the single row to the content box, so the row - // is what an item stretches to and what its overflow is measured against. - // - // Deliberately NOT applied to the scrolling variants: it would clamp the row there too, which is - // exactly what has to stop happening for the popup to grow past the fold. - gridTemplateRows: 'minmax(0, 1fr)', - // A DEFINITE height, taken from the overlay (`position: fixed; inset: 0`), which makes the - // single grid row definite too. That is what lets `variants.profile` fill the content box with - // `align-self: stretch` alone — no `dvh` arithmetic, so nothing can disagree with the box a - // bottom-anchored sheet aligns to. They genuinely do diverge: on an emulated iPhone the - // overlay measures 1251px while `100dvh` reports 844. - height: '100%', - }, + profile: profileViewport, + fullscreen: profileViewport, }); /** Pins the box for a sheet, so `compactPlacements.sheet` can cap the popup at the overlay's own height. */ @@ -362,9 +370,32 @@ export const trackVariants = stylex.create({ fullscreen: { '--_cl-dialog-inset': '0px', paddingInline: 0, + gridTemplateRows: 'minmax(0, 1fr)', }, }); +const profileSurface = { + borderColor: null, + borderRadius: null, + borderStyle: null, + borderWidth: null, + // The profile does NOT scroll itself, and that is the whole design. A fixed-height surface + // needs somewhere for overflow to go, but putting the scroll on the POPUP takes everything + // anchored to it along for the ride — the close button most obviously. So the popup clips, + // and the scroll region is the surface's own: `Profile` scrolls its content column. + // Deliberately a flex column with no `align-items` override, so the surface inside grows to + // the popup's height. + // + // `clip` rather than `hidden` for the same reason as the viewport: `hidden` would make the + // profile a scroll container, and focusing anything inside it that sits outside its box would + // scroll the profile itself. + overflow: 'clip', + // Fills the viewport's content box rather than computing a height from `dvh`. The grid row + // is definite (see `styles.viewport`), so `stretch` lands the profile's edges on exactly the + // lines a bottom-anchored sheet reaches with `align-self: end`, and clamps to them. + alignSelf: 'stretch', +}; + export const variants = stylex.create({ // The surface is a `Card` — so it comes from `Card`'s own `elevations.overlay`, and the popup // contributes geometry and motion. Compose it by rendering the card INSIDE the popup: @@ -401,27 +432,8 @@ export const variants = stylex.create({ * * Consequence worth knowing: `variant="profile"` with no surface inside renders an unpainted box. */ - profile: { - borderColor: null, - borderRadius: null, - borderStyle: null, - borderWidth: null, - // The profile does NOT scroll itself, and that is the whole design. A fixed-height surface - // needs somewhere for overflow to go, but putting the scroll on the POPUP takes everything - // anchored to it along for the ride — the close button most obviously. So the popup clips, - // and the scroll region is the surface's own: `Profile` scrolls its content column. - // Deliberately a flex column with no `align-items` override, so the surface inside grows to - // the popup's height. - // - // `clip` rather than `hidden` for the same reason as the viewport: `hidden` would make the - // profile a scroll container, and focusing anything inside it that sits outside its box would - // scroll the profile itself. - overflow: 'clip', - // Fills the viewport's content box rather than computing a height from `dvh`. The grid row - // is definite (see `styles.viewport`), so `stretch` lands the profile's edges on exactly the - // lines a bottom-anchored sheet reaches with `align-self: end`, and clamps to them. - alignSelf: 'stretch', - }, + profile: profileSurface, + fullscreen: profileSurface, }); /** @@ -504,6 +516,20 @@ export const trackCompactPlacements = stylex.create({ }, }); +const profileBackdropMotion = { + opacity: { + default: 1, + ':where([data-ending-style])': 0, + ':where([data-starting-style])': SCRIM_ENTER_OPACITY, + }, + transitionDuration: { + default: durationVars['--cl-duration-fast'], + ':where([data-ending-style])': durationVars['--cl-duration-fast'], + }, + transitionProperty: 'opacity', + transitionTimingFunction: 'linear', +}; + /** * Enter/exit motion, keyed by size, because the two surfaces want opposite things. * @@ -548,19 +574,8 @@ export const backdropMotion = stylex.create({ }, /** Identical to `card` — the popup it accompanies fades on the same clock, it just does not scale. */ - profile: { - opacity: { - default: 1, - ':where([data-ending-style])': 0, - ':where([data-starting-style])': SCRIM_ENTER_OPACITY, - }, - transitionDuration: { - default: durationVars['--cl-duration-fast'], - ':where([data-ending-style])': durationVars['--cl-duration-fast'], - }, - transitionProperty: 'opacity', - transitionTimingFunction: 'linear', - }, + profile: profileBackdropMotion, + fullscreen: profileBackdropMotion, }); // The plain CSS `ease-out` — `cubic-bezier(0, 0, 0.58, 1)` — used ONLY for the sheet's slide out. @@ -601,6 +616,22 @@ const ENTER_SCALE = 0.94; const STACK_SCALE = 0.96; const STACK_LIFT = '-0.5rem'; +const profilePopupMotion = { + opacity: { + default: 1, + ':where([data-starting-style], [data-ending-style])': 0, + }, + transitionDuration: { + default: durationVars['--cl-duration-fast'], + ':where([data-ending-style])': durationVars['--cl-duration-fast'], + }, + transitionProperty: 'opacity', + transitionTimingFunction: { + default: easingVars['--cl-ease-enter'], + ':where([data-ending-style])': easingVars['--cl-ease-exit'], + }, +}; + export const popupMotion = stylex.create({ /** * `compactPlacement='sheet'`: a card that scales from its center above the compact band, and slides up @@ -774,19 +805,6 @@ export const popupMotion = stylex.create({ * No reduced-motion branch, matching `card` — under `reduce` the two shed their transform and * are left with exactly this, so there is nothing here to drop. */ - profile: { - opacity: { - default: 1, - ':where([data-starting-style], [data-ending-style])': 0, - }, - transitionDuration: { - default: durationVars['--cl-duration-fast'], - ':where([data-ending-style])': durationVars['--cl-duration-fast'], - }, - transitionProperty: 'opacity', - transitionTimingFunction: { - default: easingVars['--cl-ease-enter'], - ':where([data-ending-style])': easingVars['--cl-ease-exit'], - }, - }, + profile: profilePopupMotion, + fullscreen: profilePopupMotion, }); diff --git a/packages/mosaic/src/components/dialog/dialog.tsx b/packages/mosaic/src/components/dialog/dialog.tsx index 32cb40b5261..bb76c64723a 100644 --- a/packages/mosaic/src/components/dialog/dialog.tsx +++ b/packages/mosaic/src/components/dialog/dialog.tsx @@ -37,10 +37,10 @@ import { guardKeyboardTouch } from './keyboard-touch'; * viewport. Not a `size`, because these are different surfaces rather than one surface at two * widths — a second card width would be a size of the `card` variant. */ -export type DialogVariant = keyof typeof variants | 'fullscreen'; +export type DialogVariant = keyof typeof variants; -function surfaceOf(variant: DialogVariant): keyof typeof variants { - return variant === 'fullscreen' ? 'profile' : variant; +function holdsProfile(variant: DialogVariant): boolean { + return variant !== 'card'; } /** @@ -227,7 +227,7 @@ const CloseButton = React.forwardRef( const variant = surface?.variant ?? 'card'; useCloseButtonWarning(role === 'alertdialog'); return ( - + ); @@ -298,7 +293,7 @@ function Viewport({ stylex.props( reset.base, styles.viewport, - viewportVariants[surfaceOf(variant)], + viewportVariants[variant], viewportCompactPlacements[compactPlacement], ), )} @@ -306,13 +301,7 @@ function Viewport({
@@ -332,7 +321,7 @@ function Viewport({ */ function useNestedVariantWarning(isNestedInDialog: boolean, variant: DialogVariant) { React.useEffect(() => { - if (process.env.NODE_ENV === 'production' || !isNestedInDialog || surfaceOf(variant) !== 'profile') { + if (process.env.NODE_ENV === 'production' || !isNestedInDialog || !holdsProfile(variant)) { return; } console.warn( @@ -351,7 +340,7 @@ function useCompactPlacementWarning(variant: DialogVariant, placement: DialogCom React.useEffect(() => { if ( process.env.NODE_ENV === 'production' || - surfaceOf(variant) !== 'profile' || + !holdsProfile(variant) || placement === undefined || placement === 'center' ) { @@ -379,8 +368,7 @@ const Popup = React.forwardRef(function Dialog const { role, isStacked: isNestedInDialog, labelId, descriptionId } = useHeadlessDialogContext(); const isAlert = role === 'alertdialog'; // A profile has its own compact-band treatment and takes no placement; the warning says so. - const compactPlacement: DialogCompactPlacement = - surfaceOf(variant) === 'profile' ? 'center' : (compactPlacementProp ?? 'sheet'); + const compactPlacement: DialogCompactPlacement = holdsProfile(variant) ? 'center' : (compactPlacementProp ?? 'sheet'); useCompactPlacementWarning(variant, compactPlacementProp); useNestedVariantWarning(isNestedInDialog, variant); @@ -391,7 +379,7 @@ const Popup = React.forwardRef(function Dialog // Observed through state rather than a plain ref, because the warnings have to re-run when the // node arrives and a ref mutation does not re-render. const [node, setNode] = React.useState(null); - useAccessibleNameWarning(node, 'Dialog', surfaceOf(variant) === 'profile' ? 'Profile.Title' : 'Card.Title'); + useAccessibleNameWarning(node, 'Dialog', holdsProfile(variant) ? 'Profile.Title' : 'Card.Title'); // A name alone is enough for an ordinary dialog; an alert is announced as an interruption and // its description is what says which decision is being asked for. useAccessibleDescriptionWarning(isAlert ? node : null, 'Dialog', 'Card.Description'); @@ -419,12 +407,12 @@ const Popup = React.forwardRef(function Dialog stylex.props( reset.base, styles.popup, - variants[surfaceOf(variant)], + variants[variant], compactPlacements[compactPlacement], // One cell per (variant, placement) that exists, selected rather than layered: StyleX // dedupes by PROPERTY across a `stylex.props` call, so a thin "sheet only" atom would // replace the centered cell's `transform` wholesale and take the desktop scale with it. - compactPlacement === 'sheet' ? popupMotion.cardSheet : popupMotion[surfaceOf(variant)], + compactPlacement === 'sheet' ? popupMotion.cardSheet : popupMotion[variant], xstyle, ), rest, @@ -453,9 +441,7 @@ const Popup = React.forwardRef(function Dialog ); return ( - - {surfaceOf(variant) === 'profile' ? {viewport} : viewport} - + {holdsProfile(variant) ? {viewport} : viewport} ); }); diff --git a/packages/mosaic/src/components/profile/profile.context.ts b/packages/mosaic/src/components/profile/profile.context.ts index 55566b0c624..0301a4f57fa 100644 --- a/packages/mosaic/src/components/profile/profile.context.ts +++ b/packages/mosaic/src/components/profile/profile.context.ts @@ -2,6 +2,8 @@ import React from 'react'; export type ProfileNavLayout = 'column' | 'select' | 'sheet'; +export type ProfileDismiss = 'back' | 'corner' | 'none'; + export interface ProfileContextValue { titleId: string; renderBranding: boolean; @@ -18,7 +20,7 @@ export interface ProfileContextValue { pageTitleRef: React.MutableRefObject; navTriggerRef: React.MutableRefObject; inline: boolean; - fullscreen: boolean; + dismiss: ProfileDismiss; } export const ProfileContext = React.createContext(null); diff --git a/packages/mosaic/src/components/profile/profile.styles.ts b/packages/mosaic/src/components/profile/profile.styles.ts index f4e60f603c7..a7c1219e9cc 100644 --- a/packages/mosaic/src/components/profile/profile.styles.ts +++ b/packages/mosaic/src/components/profile/profile.styles.ts @@ -120,11 +120,7 @@ export const styles = stylex.create({ layoutFullscreen: { borderRadius: 0, - borderWidth: '0px', - blockSize: 'auto', boxShadow: 'none', - flexGrow: 1, - minHeight: 0, }, backItem: { diff --git a/packages/mosaic/src/components/profile/profile.tsx b/packages/mosaic/src/components/profile/profile.tsx index be808a1bb5c..4066ff86912 100644 --- a/packages/mosaic/src/components/profile/profile.tsx +++ b/packages/mosaic/src/components/profile/profile.tsx @@ -16,13 +16,14 @@ import { rtl } from '../../styles/rtl.styles'; import { truncationStyles } from '../../styles/typography.styles'; import { BadgeContext } from '../badge/badge.context'; import { Branding } from '../branding'; +import type { DialogContextValue } from '../dialog'; import { Dialog, DialogContext, isInDialog } from '../dialog'; import { Drawer } from '../drawer'; import { Heading, HeadingLevelProvider, useHeadingLevel } from '../heading'; import { Icon } from '../icon'; import { SelectPopup } from '../select'; import { VisuallyHidden } from '../visually-hidden'; -import type { ProfileContextValue, ProfileNavLayout } from './profile.context'; +import type { ProfileContextValue, ProfileDismiss, ProfileNavLayout } from './profile.context'; import { ContentPanelContext, ProfileContext } from './profile.context'; import { contentScroll, contentViewportScroll, styles } from './profile.styles'; @@ -34,6 +35,13 @@ function navLayoutFor(sentinelWidth: number): ProfileNavLayout { return sentinelWidth >= 2 ? 'select' : 'column'; } +function dismissOf(dialog: DialogContextValue | null, compact: boolean): ProfileDismiss { + if (!isInDialog(dialog) || dialog.role === 'alertdialog') { + return 'none'; + } + return dialog.variant === 'fullscreen' && !compact ? 'back' : 'corner'; +} + function useProfileContext(part: string): ProfileContextValue { const context = React.useContext(ProfileContext); if (!context) { @@ -83,6 +91,7 @@ const Root = React.forwardRef(function Profile return autoUpdate(sentinel, () => setNavLayout(navLayoutFor(getDimensions(sentinel).width))); }, [sentinel]); const compact = navLayout !== 'column'; + const dismiss = dismissOf(dialog, compact); const pageTitleId = React.useId(); const pageTitleRef = React.useRef(null); const navTriggerRef = React.useRef(null); @@ -119,7 +128,7 @@ const Root = React.forwardRef(function Profile pageTitleRef, navTriggerRef, inline, - fullscreen, + dismiss, }), [ titleId, @@ -134,7 +143,7 @@ const Root = React.forwardRef(function Profile navItems, pageTitleId, inline, - fullscreen, + dismiss, ], ); const element = useRender({ @@ -161,9 +170,7 @@ const Root = React.forwardRef(function Profile {...mergeStyleProps(themeProps('profile-sentinel'), stylex.props(reset.base, styles.sentinel))} /> {/* First in the DOM so it takes the dialog's opening focus, as in `Card.Header`. */} - {isInDialog(dialog) && dialog.role !== 'alertdialog' && !(fullscreen && !compact) ? ( - - ) : null} + {dismiss === 'corner' ? : null}
(function Profile reset.base, styles.layout, inline && styles.layoutInline, - dialog !== null && !inline && !fullscreen && styles.layoutInDialog, + dialog !== null && !inline && styles.layoutInDialog, fullscreen && styles.layoutFullscreen, ), )} @@ -275,7 +282,7 @@ const Nav = React.forwardRef(function ProfileNav( setNavItems, navTriggerRef, inline, - fullscreen, + dismiss, } = useProfileContext('Profile.Nav'); useSafeLayoutEffect(() => { setNavItems(children); @@ -294,7 +301,7 @@ const Nav = React.forwardRef(function ProfileNav( ), children: ( <> - {fullscreen && navLayout === 'column' ? : null} + {dismiss === 'back' ? : null} {children} From 436c7cfe281f5242752c474f43ac630b6ea6555b Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Fri, 9 Oct 2026 15:41:49 -0400 Subject: [PATCH 4/5] refactor(mosaic): read the fullscreen variant once in Profile --- packages/mosaic/src/components/profile/profile.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/mosaic/src/components/profile/profile.tsx b/packages/mosaic/src/components/profile/profile.tsx index 4066ff86912..2a0e0efad94 100644 --- a/packages/mosaic/src/components/profile/profile.tsx +++ b/packages/mosaic/src/components/profile/profile.tsx @@ -35,11 +35,11 @@ function navLayoutFor(sentinelWidth: number): ProfileNavLayout { return sentinelWidth >= 2 ? 'select' : 'column'; } -function dismissOf(dialog: DialogContextValue | null, compact: boolean): ProfileDismiss { +function dismissOf(dialog: DialogContextValue | null, backInNav: boolean): ProfileDismiss { if (!isInDialog(dialog) || dialog.role === 'alertdialog') { return 'none'; } - return dialog.variant === 'fullscreen' && !compact ? 'back' : 'corner'; + return backInNav ? 'back' : 'corner'; } function useProfileContext(part: string): ProfileContextValue { @@ -91,7 +91,7 @@ const Root = React.forwardRef(function Profile return autoUpdate(sentinel, () => setNavLayout(navLayoutFor(getDimensions(sentinel).width))); }, [sentinel]); const compact = navLayout !== 'column'; - const dismiss = dismissOf(dialog, compact); + const dismiss = dismissOf(dialog, fullscreen && !compact); const pageTitleId = React.useId(); const pageTitleRef = React.useRef(null); const navTriggerRef = React.useRef(null); From c47b18d0aff7df55e340fdd94f73165dc1fd74ed Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Fri, 9 Oct 2026 15:48:56 -0400 Subject: [PATCH 5/5] refactor(mosaic): drop a redundant variant comment --- packages/mosaic/src/components/dialog/dialog.styles.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/mosaic/src/components/dialog/dialog.styles.ts b/packages/mosaic/src/components/dialog/dialog.styles.ts index 4dca4aa3bac..40a3d88c315 100644 --- a/packages/mosaic/src/components/dialog/dialog.styles.ts +++ b/packages/mosaic/src/components/dialog/dialog.styles.ts @@ -314,8 +314,6 @@ const profileViewport = { * `profile` fixes the height. Its content NAVIGATES — a settings surface switches sections in * place — and a content-driven height would resize the window on every section change, in both * directions at once since the viewport centers it. Its width is the surface's own. - * - * `fullscreen` is a `profile` with no frame of overlay around it, at every width. */ /** * How the viewport behaves when the popup is taller than the screen — the "inside scroll" vs