Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions apps/studio/patches/@wordpress+ui+0.14.0.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
diff --git a/node_modules/@wordpress/ui/build-module/icon-button/icon-button.mjs b/node_modules/@wordpress/ui/build-module/icon-button/icon-button.mjs
index 1623748..f2e8f9a 100644
--- a/node_modules/@wordpress/ui/build-module/icon-button/icon-button.mjs
+++ b/node_modules/@wordpress/ui/build-module/icon-button/icon-button.mjs
@@ -110,7 +110,7 @@ var IconButton = forwardRef(
...restProps
}, ref) {
const classes = clsx(style_default["icon-button"], className);
- return /* @__PURE__ */ jsx(Tooltip.Provider, { delay: 0, children: /* @__PURE__ */ jsxs(Tooltip.Root, { children: [
+ return /* @__PURE__ */ jsxs(Tooltip.Root, { children: [
/* @__PURE__ */ jsx(
Tooltip.Trigger,
{
@@ -143,7 +143,7 @@ var IconButton = forwardRef(
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: shortcut.displayShortcut })
] })
] })
- ] }) });
+ ] });
}
);
export {
diff --git a/node_modules/@wordpress/ui/build/icon-button/icon-button.cjs b/node_modules/@wordpress/ui/build/icon-button/icon-button.cjs
index 3f2a8a4..ab4df38 100644
--- a/node_modules/@wordpress/ui/build/icon-button/icon-button.cjs
+++ b/node_modules/@wordpress/ui/build/icon-button/icon-button.cjs
@@ -144,7 +144,7 @@ var IconButton = (0, import_element.forwardRef)(
...restProps
}, ref) {
const classes = (0, import_clsx.default)(style_default["icon-button"], className);
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Tooltip.Provider, { delay: 0, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Tooltip.Root, { children: [
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Tooltip.Root, { children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
Tooltip.Trigger,
{
@@ -177,7 +177,7 @@ var IconButton = (0, import_element.forwardRef)(
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", children: shortcut.displayShortcut })
] })
] })
- ] }) });
+ ] });
}
);
// Annotate the CommonJS export names for ESM import in node:
3 changes: 2 additions & 1 deletion apps/ui/src/app/app-providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { QueryClientProvider } from '@tanstack/react-query';
import { defaultI18n } from '@wordpress/i18n';
import { I18nProvider } from '@wordpress/react-i18n';
import { privateApis } from '@wordpress/theme';
import { Tooltip } from '@wordpress/ui';
import { ConnectorProvider, queryClient } from '@/data/core';
import { AgentRunProvider } from '@/data/queries/use-agent-run';
import { useSyncSessionsWithEvents } from '@/data/queries/use-sessions';
Expand Down Expand Up @@ -36,7 +37,7 @@ export function AppProviders( { children, connector }: AppProvidersProps ) {
<SiteEventsBridge />
<I18nProvider i18n={ defaultI18n }>
<ThemeProvider isRoot color={ themeColor } density="compact">
{ children }
<Tooltip.Provider>{ children }</Tooltip.Provider>
</ThemeProvider>
</I18nProvider>
</AgentRunProvider>
Expand Down
64 changes: 31 additions & 33 deletions apps/ui/src/components/site-dropdown/dropdown-trigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,40 +43,38 @@ export const DropdownTrigger = forwardRef< ElementRef< typeof Button >, Props >(
const dotClass = environment === 'live' ? styles.dot_live : styles[ `dot_${ status }` ];
const dotLabel = isLive ? __( 'Live site' ) : statusLabel;
return (
<Tooltip.Provider delay={ 0 }>
<Tooltip.Root>
<Tooltip.Trigger
ref={ ref }
render={ <Button variant="minimal" tone="neutral" size="small" { ...props } /> }
className={ clsx( styles.trigger, className ) }
<Tooltip.Root>
<Tooltip.Trigger
ref={ ref }
render={ <Button variant="minimal" tone="neutral" size="small" { ...props } /> }
className={ clsx( styles.trigger, className ) }
>
{ showSiteIcon ? (
<SiteIcon
className={ styles.siteIcon }
seed={ siteIconSeed ?? `${ siteName }:${ siteUrl }` }
imageSrc={ siteIconImage }
/>
) : null }
<span
className={ clsx( styles.statusBadge, isLive && styles.statusBadge_live ) }
role="img"
aria-label={ dotLabel }
title={ dotLabel }
>
{ showSiteIcon ? (
<SiteIcon
className={ styles.siteIcon }
seed={ siteIconSeed ?? `${ siteName }:${ siteUrl }` }
imageSrc={ siteIconImage }
/>
) : null }
<span
className={ clsx( styles.statusBadge, isLive && styles.statusBadge_live ) }
role="img"
aria-label={ dotLabel }
title={ dotLabel }
>
<span className={ clsx( styles.dot, dotClass ) } aria-hidden="true" />
{ isLive ? <span className={ styles.statusLabel }>{ __( 'Live' ) }</span> : null }
</span>
<span className={ styles.identity }>
<span className={ styles.site }>{ siteName }</span>
<span className={ styles.url }>{ siteUrl }</span>
</span>
<Icon className={ styles.chevron } icon={ chevronDownSmall } />
</Tooltip.Trigger>
<Tooltip.Popup positioner={ <Tooltip.Positioner side="bottom" /> }>
{ __( 'Open site menu' ) }
</Tooltip.Popup>
</Tooltip.Root>
</Tooltip.Provider>
<span className={ clsx( styles.dot, dotClass ) } aria-hidden="true" />
{ isLive ? <span className={ styles.statusLabel }>{ __( 'Live' ) }</span> : null }
</span>
<span className={ styles.identity }>
<span className={ styles.site }>{ siteName }</span>
<span className={ styles.url }>{ siteUrl }</span>
</span>
<Icon className={ styles.chevron } icon={ chevronDownSmall } />
</Tooltip.Trigger>
<Tooltip.Popup positioner={ <Tooltip.Positioner side="bottom" /> }>
{ __( 'Open site menu' ) }
</Tooltip.Popup>
</Tooltip.Root>
);
}
);
44 changes: 20 additions & 24 deletions apps/ui/src/components/site-dropdown/main-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,33 +136,29 @@ export function MainView( { site, onSetupClick, onDisconnectClick }: Props ) {
children,
...props
}: ButtonProps & { tooltip: string } ) => (
<Tooltip.Provider delay={ 0 }>
<Tooltip.Root>
<Tooltip.Trigger render={ <Button { ...props }>{ children }</Button> } />
<Tooltip.Popup positioner={ <Tooltip.Positioner side="top" /> }>{ tooltip }</Tooltip.Popup>
</Tooltip.Root>
</Tooltip.Provider>
<Tooltip.Root>
<Tooltip.Trigger render={ <Button { ...props }>{ children }</Button> } />
<Tooltip.Popup positioner={ <Tooltip.Positioner side="top" /> }>{ tooltip }</Tooltip.Popup>
</Tooltip.Root>
);

const renderUrlLink = ( { text, url, label }: { text: string; url: string; label: string } ) => (
<Tooltip.Provider delay={ 0 }>
<Tooltip.Root>
<Tooltip.Trigger
render={
<button
type="button"
className={ styles.urlLink }
aria-label={ label }
onClick={ () => openExternal( url ) }
>
<span>{ text }</span>
<Icon icon={ external } size={ 12 } aria-hidden="true" />
</button>
}
/>
<Tooltip.Popup positioner={ <Tooltip.Positioner side="top" /> }>{ label }</Tooltip.Popup>
</Tooltip.Root>
</Tooltip.Provider>
<Tooltip.Root>
<Tooltip.Trigger
render={
<button
type="button"
className={ styles.urlLink }
aria-label={ label }
onClick={ () => openExternal( url ) }
>
<span>{ text }</span>
<Icon icon={ external } size={ 12 } aria-hidden="true" />
</button>
}
/>
<Tooltip.Popup positioner={ <Tooltip.Positioner side="top" /> }>{ label }</Tooltip.Popup>
</Tooltip.Root>
);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,8 @@ function getErrorLabel( direction: SyncDirection ): string {
}

// All three states render an IconButton so we reuse its built-in tooltip
// wiring (@wordpress/ui wraps each IconButton in its own Tooltip.Provider
// with delay=0). `focusableWhenDisabled` keeps the tooltip trigger active
// even when the button itself is visually disabled for pending/success.
// wiring. `focusableWhenDisabled` keeps the tooltip trigger active even
// when the button itself is visually disabled for pending/success.
export function SyncActivityIndicator( { siteId }: { siteId: string } ) {
const activity = useSiteSyncActivity( siteId );
if ( ! activity ) {
Expand Down
106 changes: 51 additions & 55 deletions apps/ui/src/components/site-list/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -180,22 +180,20 @@ function SessionItem( { session, isVisible }: { session: AiSessionSummary; isVis
}
>
{ hasPendingQuestion ? (
<Tooltip.Provider delay={ 0 }>
<Tooltip.Root>
<Tooltip.Trigger
render={
<span
className={ styles.sessionQuestionIndicator }
role="status"
aria-label={ __( 'Studio needs an answer.' ) }
/>
}
/>
<Tooltip.Popup positioner={ <Tooltip.Positioner side="top" /> }>
{ __( 'Studio needs an answer.' ) }
</Tooltip.Popup>
</Tooltip.Root>
</Tooltip.Provider>
<Tooltip.Root>
<Tooltip.Trigger
render={
<span
className={ styles.sessionQuestionIndicator }
role="status"
aria-label={ __( 'Studio needs an answer.' ) }
/>
}
/>
<Tooltip.Popup positioner={ <Tooltip.Positioner side="top" /> }>
{ __( 'Studio needs an answer.' ) }
</Tooltip.Popup>
</Tooltip.Root>
) : isRunning ? (
<Spinner className={ styles.sessionInlineSpinner } label={ __( 'Working…' ) } />
) : null }
Expand Down Expand Up @@ -511,53 +509,51 @@ function SiteStatusButton( {
};

return (
<Tooltip.Provider delay={ 0 }>
<Tooltip.Root>
<Tooltip.Trigger
render={
<button
type="button"
className={ styles.siteStatus }
aria-label={ label }
aria-busy={ busy || undefined }
aria-disabled={ busy || undefined }
data-state={ status }
onClick={ busy ? undefined : handleClick }
<Tooltip.Root>
<Tooltip.Trigger
render={
<button
type="button"
className={ styles.siteStatus }
aria-label={ label }
aria-busy={ busy || undefined }
aria-disabled={ busy || undefined }
data-state={ status }
onClick={ busy ? undefined : handleClick }
>
<svg
className={ styles.siteStatusGlyph }
viewBox={ status === 'stopped' ? '0 0 10 10' : '0 0 8 8' }
aria-hidden="true"
focusable="false"
>
{ status === 'stopped' ? (
<path className={ styles.siteStatusPlayShape } d="M2.5 1 L9 5 L2.5 9 Z" />
) : (
<rect className={ styles.siteStatusShape } x="0" y="0" width="8" height="8" />
) }
</svg>
{ ! busy ? (
<svg
className={ styles.siteStatusGlyph }
viewBox={ status === 'stopped' ? '0 0 10 10' : '0 0 8 8' }
className={ styles.siteStatusActionGlyph }
viewBox="0 0 10 10"
aria-hidden="true"
focusable="false"
>
{ status === 'stopped' ? (
<path className={ styles.siteStatusPlayShape } d="M2.5 1 L9 5 L2.5 9 Z" />
{ site.running ? (
<rect x="1" y="1" width="8" height="8" rx="1" fill="currentColor" />
) : (
<rect className={ styles.siteStatusShape } x="0" y="0" width="8" height="8" />
<path d="M2.5 1 L9 5 L2.5 9 Z" fill="currentColor" />
) }
</svg>
{ ! busy ? (
<svg
className={ styles.siteStatusActionGlyph }
viewBox="0 0 10 10"
aria-hidden="true"
focusable="false"
>
{ site.running ? (
<rect x="1" y="1" width="8" height="8" rx="1" fill="currentColor" />
) : (
<path d="M2.5 1 L9 5 L2.5 9 Z" fill="currentColor" />
) }
</svg>
) : null }
</button>
}
/>
<Tooltip.Popup positioner={ <Tooltip.Positioner side="top" /> }>
{ tooltipLabel }
</Tooltip.Popup>
</Tooltip.Root>
</Tooltip.Provider>
) : null }
</button>
}
/>
<Tooltip.Popup positioner={ <Tooltip.Positioner side="top" /> }>
{ tooltipLabel }
</Tooltip.Popup>
</Tooltip.Root>
);
}

Expand Down
41 changes: 39 additions & 2 deletions apps/ui/src/components/site-preview/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { fireEvent, render, screen } from '@testing-library/react';
import { Tooltip } from '@wordpress/ui';
import { describe, expect, it, vi } from 'vitest';
import { useConnector } from '@/data/core';
import { getPathFromPreviewUrl, getToolbarPageTitle, SitePreview } from './index';
Expand All @@ -19,7 +20,11 @@ function renderPreview( children: ReactNode ) {
mutations: { retry: false },
},
} );
return render( <QueryClientProvider client={ queryClient }>{ children }</QueryClientProvider> );
return render(
<QueryClientProvider client={ queryClient }>
<Tooltip.Provider>{ children }</Tooltip.Provider>
</QueryClientProvider>
);
}

function createSite( overrides: Partial< SiteDetails > = {} ): SiteDetails {
Expand Down Expand Up @@ -48,8 +53,40 @@ describe( 'SitePreview', () => {
expect( pageTitle ).toBeVisible();

fireEvent.mouseEnter( pageTitle );
fireEvent.mouseMove( pageTitle, { movementX: 1, movementY: 1 } );

expect( await screen.findByText( 'http://localhost:8881/wp-admin/' ) ).toBeVisible();
expect( screen.queryByText( 'http://localhost:8881/wp-admin/' ) ).not.toBeInTheDocument();
// Tooltips use Base UI's default open delay, so wait long enough for the popup to appear.
expect(
await screen.findByText( 'http://localhost:8881/wp-admin/', {}, { timeout: 2000 } )
Comment thread
shaunandrews marked this conversation as resolved.
).toBeVisible();
} );

it( 'shows adjacent toolbar tooltips immediately while the delay group is active', async () => {
useConnectorMock.mockReturnValue( {
startSite: vi.fn().mockResolvedValue( undefined ),
} as never );

renderPreview(
<SitePreview site={ createSite( { running: true } ) } path="/wp-admin/" reloadNonce={ 0 } />
);

const pageTitle = screen.getByText( 'Example Site' );
fireEvent.mouseEnter( pageTitle );
fireEvent.mouseMove( pageTitle, { movementX: 1, movementY: 1 } );

await screen.findByText( 'http://localhost:8881/wp-admin/', {}, { timeout: 2000 } );

const refreshButton = screen.getByRole( 'button', { name: 'Refresh' } );
expect( screen.queryByText( /^Refresh/ ) ).not.toBeInTheDocument();

fireEvent.mouseLeave( pageTitle, { relatedTarget: refreshButton } );
fireEvent.mouseEnter( refreshButton, { relatedTarget: pageTitle } );
fireEvent.mouseMove( refreshButton, { movementX: 1, movementY: 1 } );

const refreshTooltip = screen.getByText( /^Refresh/ );
expect( refreshTooltip ).toBeInTheDocument();
expect( refreshTooltip ).toHaveAttribute( 'data-instant', 'delay' );
} );

it( 'hides the browser controls when the site is not running', () => {
Expand Down
Loading