diff --git a/.changeset/mosaic-card-footer-button-width.md b/.changeset/mosaic-card-footer-button-width.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/mosaic-card-footer-button-width.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/mosaic/src/blocks/confirmation/confirmation.tsx b/packages/mosaic/src/blocks/confirmation/confirmation.tsx index b7e91ce72de..c41fc8008bc 100644 --- a/packages/mosaic/src/blocks/confirmation/confirmation.tsx +++ b/packages/mosaic/src/blocks/confirmation/confirmation.tsx @@ -57,7 +57,6 @@ export function Confirmation({ color = 'negative', ...props }: C @@ -65,7 +64,6 @@ export function Confirmation({ color = 'negative', ...props }: C /> ({ @@ -96,7 +95,6 @@ function DestructiveStep({ /> & { - styles?: StyleXStyles; - sizeStyles?: StyleXStyles; - iconStyles?: StyleXStyles; - } ->({}); +type ButtonDefaults = Pick & { + styles?: StyleXStyles; + sizeStyles?: StyleXStyles; + iconStyles?: StyleXStyles; +}; + +export const NO_BUTTON_DEFAULTS: ButtonDefaults = {}; + +export const ButtonContext = React.createContext(NO_BUTTON_DEFAULTS); diff --git a/packages/mosaic/src/components/button/button.tsx b/packages/mosaic/src/components/button/button.tsx index 9d107a20986..92dbd1b0d5a 100644 --- a/packages/mosaic/src/components/button/button.tsx +++ b/packages/mosaic/src/components/button/button.tsx @@ -61,7 +61,7 @@ export const Button = React.forwardRef(function variant: variantProp, size: sizeProp, shape: shapeProp, - fullWidth = false, + fullWidth: fullWidthProp, touchTarget = true, disabled: disabledProp, focusableWhenDisabled = false, @@ -76,6 +76,7 @@ export const Button = React.forwardRef(function const variant = variantProp ?? defaults.variant ?? 'filled'; const size = sizeProp ?? defaults.size ?? 'md'; const shape = shapeProp ?? defaults.shape ?? 'default'; + const fullWidth = fullWidthProp ?? defaults.fullWidth ?? false; const disabled = defaults.disabled || disabledProp || false; const isIconShape = shape === 'square' || shape === 'circle'; const hasTouchTarget = touchTarget && variant !== 'link'; diff --git a/packages/mosaic/src/components/card/card.test.tsx b/packages/mosaic/src/components/card/card.test.tsx index 89739b7814d..f2a3277c0e5 100644 --- a/packages/mosaic/src/components/card/card.test.tsx +++ b/packages/mosaic/src/components/card/card.test.tsx @@ -4,6 +4,8 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; import { afterEach, describe, expect, it, vi } from 'vitest'; +import { Button, SubmitButton } from '../button'; +import { ButtonContext } from '../button/button.context'; import { Dialog } from '../dialog'; import { Flow } from '../flow'; import { Card } from './card'; @@ -753,6 +755,55 @@ describe('Mosaic Card', () => { }); }); +describe('Card.Footer buttons', () => { + it('makes buttons full width by default', () => { + render( + + + Save + , + ); + + expect(screen.getByRole('button', { name: 'Cancel' })).toHaveAttribute('data-full-width', ''); + expect(screen.getByRole('button', { name: 'Save' })).toHaveAttribute('data-full-width', ''); + }); + + it('lets a button opt out', () => { + render( + + + , + ); + + expect(screen.getByRole('button', { name: 'Sign in' })).not.toHaveAttribute('data-full-width'); + }); + + it('keeps button defaults from an enclosing provider', () => { + render( + + + + + , + ); + + expect(screen.getByRole('button', { name: 'Save' })).toBeDisabled(); + }); + + it('leaves buttons outside the footer alone', () => { + render( + + + + + + , + ); + + expect(screen.getByRole('button', { name: 'Inside content' })).not.toHaveAttribute('data-full-width'); + }); +}); + describe('Card surface', () => { // Written out rather than imported, so a drift from `card.styles.ts` fails here instead of passing silently. const probe = stylex.create({ diff --git a/packages/mosaic/src/components/card/card.tsx b/packages/mosaic/src/components/card/card.tsx index 631b12ab3c6..4fc2766ac6e 100644 --- a/packages/mosaic/src/components/card/card.tsx +++ b/packages/mosaic/src/components/card/card.tsx @@ -13,6 +13,7 @@ import type { BannerRootProps } from '../banner'; import { Banner } from '../banner'; import { Branding } from '../branding'; import { Button } from '../button'; +import { ButtonContext } from '../button/button.context'; import type { DialogContextValue } from '../dialog'; import { Dialog, DialogContext, isInDialog } from '../dialog'; import { Icon } from '../icon'; @@ -251,7 +252,9 @@ const Footer = React.forwardRef>(fun ref, ) { const elevation = React.useContext(CardContext)?.elevation ?? DEFAULT_ELEVATION; - return useRender({ + const parentButtonDefaults = React.useContext(ButtonContext); + const buttonDefaults = React.useMemo(() => ({ ...parentButtonDefaults, fullWidth: true }), [parentButtonDefaults]); + const element = useRender({ defaultTagName: 'div', render, ref, @@ -263,6 +266,8 @@ const Footer = React.forwardRef>(fun ), }, }); + + return {element}; }); /** diff --git a/packages/mosaic/src/components/dialog/__tests__/dialog.test.tsx b/packages/mosaic/src/components/dialog/__tests__/dialog.test.tsx index 7a2587396d1..af6605da7f1 100644 --- a/packages/mosaic/src/components/dialog/__tests__/dialog.test.tsx +++ b/packages/mosaic/src/components/dialog/__tests__/dialog.test.tsx @@ -6,6 +6,8 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import type { MosaicComponentProps } from '../../../props'; import { colorVars, radiusVars, space } from '../../../tokens.stylex'; +import { Button } from '../../button'; +import { ButtonContext } from '../../button/button.context'; import { Card } from '../../card'; import type { DialogVariant } from '../dialog'; import { Dialog } from '../dialog'; @@ -395,6 +397,38 @@ describe('stacked backdrops', () => { }); }); +describe('button defaults', () => { + it('does not inherit full width from a card footer it is opened from', () => { + render( + + + + + + + + , + ); + + expect(screen.getByRole('button', { name: 'Inside popup' })).not.toHaveAttribute('data-full-width'); + }); + + it('does not inherit disabled from an enclosing provider', () => { + render( + + + + + + + + , + ); + + expect(screen.getByRole('button', { name: 'Inside popup' })).toBeEnabled(); + }); +}); + describe('Dialog.CloseButton', () => { it('closes the dialog and carries a default accessible name', async () => { const user = userEvent.setup(); diff --git a/packages/mosaic/src/components/dialog/dialog.tsx b/packages/mosaic/src/components/dialog/dialog.tsx index 9ef61a4566a..e142b13cc20 100644 --- a/packages/mosaic/src/components/dialog/dialog.tsx +++ b/packages/mosaic/src/components/dialog/dialog.tsx @@ -16,6 +16,7 @@ import type { MosaicComponentProps } from '../../props'; import { mergeStyleProps, themeProps } from '../../props'; import { reset } from '../../styles/reset.styles'; import { Button } from '../button'; +import { ButtonContext, NO_BUTTON_DEFAULTS } from '../button/button.context'; import { Icon } from '../icon'; import { ToastProvider } from '../toast/toast'; import { @@ -475,7 +476,11 @@ const Popup = React.forwardRef(function Dialog ); return ( - {holdsProfile(variant) ? {viewport} : viewport} + + + {holdsProfile(variant) ? {viewport} : viewport} + + ); }); diff --git a/packages/mosaic/src/components/popover/popover.test.tsx b/packages/mosaic/src/components/popover/popover.test.tsx index 5271670f381..7a42e87f753 100644 --- a/packages/mosaic/src/components/popover/popover.test.tsx +++ b/packages/mosaic/src/components/popover/popover.test.tsx @@ -4,6 +4,9 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; import { afterEach, describe, expect, it, vi } from 'vitest'; +import { Button } from '../button'; +import { ButtonContext } from '../button/button.context'; +import { Card } from '../card'; import { Popover } from './popover'; afterEach(() => cleanup()); @@ -14,6 +17,36 @@ const settle = () => await new Promise(resolve => setTimeout(resolve, 0)); }); +describe('button defaults', () => { + it('does not inherit full width from a card footer it is opened from', () => { + render( + + + + + + + , + ); + + expect(screen.getByRole('button', { name: 'Inside popup' })).not.toHaveAttribute('data-full-width'); + }); + + it('does not inherit disabled from an enclosing provider', () => { + render( + + + + + + + , + ); + + expect(screen.getByRole('button', { name: 'Inside popup' })).toBeEnabled(); + }); +}); + describe('Mosaic Popover', () => { it('uses a custom anchor and restores the trigger when it is removed', async () => { const anchor = document.createElement('div'); diff --git a/packages/mosaic/src/components/popover/popover.tsx b/packages/mosaic/src/components/popover/popover.tsx index fb728d5da00..d79f6e65a0e 100644 --- a/packages/mosaic/src/components/popover/popover.tsx +++ b/packages/mosaic/src/components/popover/popover.tsx @@ -7,6 +7,7 @@ import { Popover as Primitive } from '../../primitives/popover'; import type { MosaicComponentProps } from '../../props'; import { mergeStyleProps, themeProps } from '../../props'; import { reset } from '../../styles/reset.styles'; +import { ButtonContext, NO_BUTTON_DEFAULTS } from '../button/button.context'; import { sizes, styles } from './popover.styles'; export type PopoverSize = 'sm' | 'md' | 'lg' | 'anchor'; @@ -98,14 +99,16 @@ const Popup = React.forwardRef(function Popov {...(ariaLabel == null ? {} : { 'aria-label': ariaLabel })} {...(ariaLabelledby == null ? {} : { 'aria-labelledby': ariaLabelledby })} > - + + + ); diff --git a/packages/mosaic/src/features/api-keys/create-api-key.dialog.tsx b/packages/mosaic/src/features/api-keys/create-api-key.dialog.tsx index 95fee435a9b..3a49e5d7506 100644 --- a/packages/mosaic/src/features/api-keys/create-api-key.dialog.tsx +++ b/packages/mosaic/src/features/api-keys/create-api-key.dialog.tsx @@ -163,7 +163,6 @@ function CreateKeyStep(props: CreateAPIKeyDialogProps & { inputRef: Ref - - {m.copyAndClose} - + {m.copyAndClose} ); diff --git a/packages/mosaic/src/features/create-organization/create-organization.view.tsx b/packages/mosaic/src/features/create-organization/create-organization.view.tsx index 3497d22088f..717c8f285eb 100644 --- a/packages/mosaic/src/features/create-organization/create-organization.view.tsx +++ b/packages/mosaic/src/features/create-organization/create-organization.view.tsx @@ -147,7 +147,6 @@ function CreateStep({ form, slugEnabled, onNameChange, onCancel }: CreateOrganiz @@ -112,7 +111,6 @@ export function OrganizationProfileInviteMembersDialog({ /> } > @@ -91,7 +90,6 @@ export function OrganizationProfileEditNameDialog({ } > @@ -92,7 +91,6 @@ export function OrganizationProfileEditSlugDialog({ {m.error.close} @@ -103,6 +102,7 @@ export function ReverificationPending() { @@ -129,7 +128,6 @@ export function UserProfileEditPasswordDialog({ /> } > @@ -197,7 +195,6 @@ export function VerifyPhoneStep(props: VerifyPhoneStepProps) { )} {m.name.cancel} @@ -117,7 +116,6 @@ export function UserProfileEditNameDialog({ /> {m.cancel} @@ -82,7 +81,6 @@ export function UserProfileEditUsernameDialog({ /> {m.solanaDialog.cancel} diff --git a/packages/mosaic/src/features/waitlist/waitlist.view.tsx b/packages/mosaic/src/features/waitlist/waitlist.view.tsx index b62a2c9fe1f..3ab11d17c8a 100644 --- a/packages/mosaic/src/features/waitlist/waitlist.view.tsx +++ b/packages/mosaic/src/features/waitlist/waitlist.view.tsx @@ -51,6 +51,7 @@ export function WaitlistView({ step, form, renderBranding, isRedirecting, onSign type='button' variant='link' size='sm' + fullWidth={false} onClick={onSignIn} > {m.signIn.action} diff --git a/packages/swingset/src/app/(clerk)/live/misc/reverification/page.tsx b/packages/swingset/src/app/(clerk)/live/misc/reverification/page.tsx index 82e3ba48662..dd2beab0f78 100644 --- a/packages/swingset/src/app/(clerk)/live/misc/reverification/page.tsx +++ b/packages/swingset/src/app/(clerk)/live/misc/reverification/page.tsx @@ -243,7 +243,6 @@ function DialogHarness() { + ); diff --git a/packages/swingset/src/stories/dialog.component.mdx b/packages/swingset/src/stories/dialog.component.mdx index 649bdd2f7b4..bf3662db30e 100644 --- a/packages/swingset/src/stories/dialog.component.mdx +++ b/packages/swingset/src/stories/dialog.component.mdx @@ -40,8 +40,8 @@ import { Dialog } from '@clerk/mosaic/components/dialog'; Are you sure you want to proceed? - }>Cancel - + }>Cancel + @@ -207,8 +207,8 @@ action as its submit. - }>Cancel - + }>Cancel + ``` diff --git a/packages/swingset/src/stories/dialog.component.stories.tsx b/packages/swingset/src/stories/dialog.component.stories.tsx index 8d455a6fa66..c4f67d6fcc8 100644 --- a/packages/swingset/src/stories/dialog.component.stories.tsx +++ b/packages/swingset/src/stories/dialog.component.stories.tsx @@ -122,21 +122,11 @@ export function Alert() { - - } - > - Cancel - + }>Cancel {/* Not a `Dialog.Close`: the action is where the work happens, so the caller closes once it resolves rather than the button closing on press. */} @@ -291,16 +271,7 @@ export function Stacked() { tree — which is what escape ordering, the stacking styles and the refcounted scroll lock all read. */} - - } - > - Cancel - + }>Cancel Your edits will be lost. - - } - > - Keep editing - + }>Keep editing )} /> - + diff --git a/packages/swingset/src/stories/flow.component.stories.tsx b/packages/swingset/src/stories/flow.component.stories.tsx index 82e2ddd514a..3c7348e1a42 100644 --- a/packages/swingset/src/stories/flow.component.stories.tsx +++ b/packages/swingset/src/stories/flow.component.stories.tsx @@ -38,12 +38,7 @@ function FlowDemo(): JSX.Element { Current controller state: {state.step} - + @@ -56,14 +51,13 @@ function FlowDemo(): JSX.Element { - + diff --git a/packages/swingset/src/stories/popover.component.mdx b/packages/swingset/src/stories/popover.component.mdx index 53ea83deadd..6745e1545c0 100644 --- a/packages/swingset/src/stories/popover.component.mdx +++ b/packages/swingset/src/stories/popover.component.mdx @@ -28,7 +28,6 @@ import { Popover } from '@clerk/mosaic/components/popover'; diff --git a/packages/swingset/src/stories/popover.component.stories.tsx b/packages/swingset/src/stories/popover.component.stories.tsx index 8b905825a19..ca59356498a 100644 --- a/packages/swingset/src/stories/popover.component.stories.tsx +++ b/packages/swingset/src/stories/popover.component.stories.tsx @@ -35,12 +35,7 @@ export function Default() { ada@example.com - + diff --git a/packages/swingset/src/stories/table.stories.tsx b/packages/swingset/src/stories/table.stories.tsx index 2d863bab27e..946d3d51537 100644 --- a/packages/swingset/src/stories/table.stories.tsx +++ b/packages/swingset/src/stories/table.stories.tsx @@ -562,7 +562,6 @@ export function ClickableRows() { render={