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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ This repository contains the source code and documentation for Base UI: a headl

- Do not randomly cast (for example `as any`) if there are no type errors without doing so. Run `pnpm typescript` to verify types.
- Ensure your changes pass linting - run `pnpm eslint`.
- Ensure your styles pass stylelint - run `pnpm stylelint`.
- Ensure your markdown passes markdownlint - run `pnpm markdownlint`.
- Ensure your changes are formatted correctly - run `pnpm prettier`.
- When you change a public component API (props or JSDoc), run `pnpm docs:api`.

Expand Down
9 changes: 9 additions & 0 deletions docs/reference/generated/tooltip-trigger.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@
"description": "A payload to pass to the tooltip when it is opened.",
"detailedType": "Payload | undefined"
},
"disabled": {
"type": "boolean",
"default": "false",
"description": "Whether the trigger is disabled. If `true`, the tooltip will not open when interacting with this trigger.\nNote that this doesn't apply the `disabled` attribute to the trigger element.\nIf you want to disable the trigger element itself, you can pass the `disabled` prop to the trigger element via the `render` prop.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Whether the trigger is disabled" should probably be updated

"detailedType": "boolean | undefined"
},
"delay": {
"type": "number",
"default": "600",
Expand Down Expand Up @@ -42,6 +48,9 @@
"dataAttributes": {
"data-popup-open": {
"description": "Present when the corresponding tooltip is open."
},
"data-trigger-disabled": {
"description": "Present when the trigger is disabled, either by the `disabled` prop or by a parent `<Tooltip.Root>` component."
}
},
"cssVariables": {}
Expand Down
4 changes: 2 additions & 2 deletions docs/src/app/(docs)/react/components/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1376,8 +1376,8 @@ A popup that appears when an element is hovered or focused, showing a hint for s
- Tooltip - Root
- Props: actionsRef, children, defaultOpen, defaultTriggerId, disableHoverablePopup, disabled, handle, onOpenChange, onOpenChangeComplete, open, trackCursorAxis, triggerId
- Tooltip - Trigger
- Props: className, closeDelay, delay, handle, payload, render, style
- Data Attributes: data-popup-open
- Props: className, closeDelay, delay, disabled, handle, payload, render, style
- Data Attributes: data-popup-open, data-trigger-disabled
- Tooltip - Portal
- Props: className, container, keepMounted, render, style
- Tooltip - Positioner
Expand Down
186 changes: 186 additions & 0 deletions docs/src/app/(private)/experiments/tooltip/disabled.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
.Container {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
justify-content: center;
}

.ButtonGroup {
display: flex;
}

.IconButton {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
padding: 0;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
background-color: var(--color-gray-50);
color: var(--color-gray-900);
user-select: none;

&[data-popup-open] {
background-color: var(--color-gray-100);
}

&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}

@media (hover: hover) {
&:hover {
background-color: var(--color-gray-100);
}
}

&:active {
background-color: var(--color-gray-100);
}

&:only-child {
border-radius: 0.375rem;
}

&:first-child:not(:only-child) {
border-radius: 0.375rem 0 0 0.375rem;
}

&:last-child:not(:only-child) {
border-radius: 0 0.375rem 0.375rem 0;
}

&:not(:first-child) {
border-left: none;
}
}

.Icon {
width: 1.25rem;
height: 1.25rem;
}

.Positioner {
height: var(--positioner-height);
width: var(--positioner-width);
max-width: var(--available-width);
}

.Popup {
box-sizing: border-box;
font-size: 0.875rem;
line-height: 1.25rem;
padding: 0.25rem 0.5rem;
border-radius: 0.375rem;
background-color: canvas;
transform-origin: var(--transform-origin);
transition:
transform 150ms,
opacity 150ms;

&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}

&[data-instant] {
transition: none;
}

@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow:
0 10px 15px -3px var(--color-gray-200),
0 4px 6px -4px var(--color-gray-200);
}

@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
outline-offset: -1px;
}
}

.Arrow {
display: flex;

&[data-side='top'] {
bottom: -8px;
rotate: 180deg;
}

&[data-side='bottom'] {
top: -8px;
rotate: 0deg;
}

&[data-side='left'] {
right: -13px;
rotate: 90deg;
}

&[data-side='right'] {
left: -13px;
rotate: -90deg;
}
}

.ArrowFill {
fill: canvas;
}

.ArrowOuterStroke {
@media (prefers-color-scheme: light) {
fill: var(--color-gray-200);
}
}

.ArrowInnerStroke {
@media (prefers-color-scheme: dark) {
fill: var(--color-gray-300);
}
}

.Button {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
min-height: 2.5rem;
padding: 0 0.875rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 0.375rem;
background-color: var(--color-gray-50);
font-family: inherit;
font-size: 1rem;
font-weight: 500;
line-height: 1.5rem;
color: var(--color-gray-900);
user-select: none;

@media (hover: hover) {
&:hover:not([data-disabled]) {
background-color: var(--color-gray-100);
}
}

&:active:not([data-disabled]) {
background-color: var(--color-gray-100);
}

&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}

&[data-disabled] {
opacity: 0.5;
}
}
72 changes: 72 additions & 0 deletions docs/src/app/(private)/experiments/tooltip/disabled.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use client';
import * as React from 'react';
import { Button } from '@base-ui/react/button';
import { Tooltip } from '@base-ui/react/tooltip';
import styles from './disabled.module.css';

const demoTooltip = Tooltip.createHandle();

export default function TooltipDetachedTriggersSimpleDemo() {
return (
<React.Fragment>
<div className={styles.ButtonGroup}>
<Tooltip.Trigger className={styles.Button} handle={demoTooltip}>
Enabled trigger
</Tooltip.Trigger>

<Tooltip.Trigger className={styles.Button} handle={demoTooltip} disabled>
Disabled trigger
</Tooltip.Trigger>

<Tooltip.Trigger
className={styles.Button}
handle={demoTooltip}
render={<Button disabled focusableWhenDisabled />}
>
Enabled trigger rendered as disabled button
</Tooltip.Trigger>

<Tooltip.Trigger
className={styles.Button}
handle={demoTooltip}
disabled
render={<Button disabled focusableWhenDisabled />}
>
Disabled trigger rendered as disabled button
</Tooltip.Trigger>
</div>

<Tooltip.Root handle={demoTooltip}>
<Tooltip.Portal>
<Tooltip.Positioner sideOffset={10}>
<Tooltip.Popup className={styles.Popup}>
<Tooltip.Arrow className={styles.Arrow}>
<ArrowSvg />
</Tooltip.Arrow>
Tooltip
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
</Tooltip.Root>
</React.Fragment>
);
}

function ArrowSvg(props: React.ComponentProps<'svg'>) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path
d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
className={styles.ArrowFill}
/>
<path
d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z"
className={styles.ArrowOuterStroke}
/>
<path
d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
className={styles.ArrowInnerStroke}
/>
</svg>
);
}
20 changes: 8 additions & 12 deletions packages/react/src/floating-ui-react/hooks/useFocus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
contains,
getDocument,
getTarget,
isTargetInsideEnabledTrigger,
isTypeableElement,
matchesFocusVisible,
} from '../utils';
Expand Down Expand Up @@ -153,15 +154,16 @@ export function useFocus(
}
}

const movedFromOtherTrigger =
event.relatedTarget &&
store.context.triggerElements.hasElement(event.relatedTarget as Element);
const movedFromOtherEnabledTrigger = isTargetInsideEnabledTrigger(
event.relatedTarget,
store.context.triggerElements,
);

const { nativeEvent, currentTarget } = event;
const delayValue = typeof delay === 'function' ? delay() : delay;

if (
(store.select('open') && movedFromOtherTrigger) ||
(store.select('open') && movedFromOtherEnabledTrigger) ||
delayValue === 0 ||
delayValue === undefined
) {
Expand Down Expand Up @@ -233,14 +235,8 @@ export function useFocus(
// the floating element. The focus handler of that trigger will
// handle the open state.
const nextFocusedElement = relatedTarget ?? activeEl;
if (isElement(nextFocusedElement)) {
const triggerElements = store.context.triggerElements;
if (
triggerElements.hasElement(nextFocusedElement) ||
triggerElements.hasMatchingElement((trigger) => contains(trigger, nextFocusedElement))
) {
return;
}
if (isTargetInsideEnabledTrigger(nextFocusedElement, store.context.triggerElements)) {
return;
}

store.setOpen(false, createChangeEventDetails(REASONS.triggerFocus, nativeEvent));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import { useStableCallback } from '@base-ui/utils/useStableCallback';
import { useIsoLayoutEffect } from '@base-ui/utils/useIsoLayoutEffect';

import type { FloatingContext, FloatingRootContext } from '../types';
import { getDocument, getTarget, isMouseLikePointerType } from '../utils';
import {
getDocument,
getTarget,
isMouseLikePointerType,
isTargetInsideEnabledTrigger,
} from '../utils';

import { createChangeEventDetails } from '../../utils/createBaseUIEventDetails';
import { REASONS } from '../../utils/reasons';
Expand Down Expand Up @@ -66,6 +71,10 @@ export function useHoverFloatingInteraction(
return type?.includes('mouse') && type !== 'mousedown';
});

const isRelatedTargetInsideEnabledTrigger = useStableCallback((target: EventTarget | null) => {
return isTargetInsideEnabledTrigger(target, store.context.triggerElements);
});

const closeWithDelay = React.useCallback(
(event: MouseEvent, runElseBranch = true) => {
const closeDelay = getDelay(closeDelayProp, instance.pointerType);
Expand Down Expand Up @@ -178,8 +187,7 @@ export function useHoverFloatingInteraction(
return;
}

const triggerElements = store.context.triggerElements;
if (event.relatedTarget && triggerElements.hasElement(event.relatedTarget as Element)) {
if (isRelatedTargetInsideEnabledTrigger(event.relatedTarget)) {
// If the mouse is leaving the reference element to another trigger, don't explicitly close the popup
// as it will be moved.
return;
Expand Down Expand Up @@ -227,6 +235,7 @@ export function useHoverFloatingInteraction(
store,
dataRef,
isClickLikeOpenEvent,
isRelatedTargetInsideEnabledTrigger,
closeWithDelay,
clearPointerEvents,
cleanupMouseMoveHandler,
Expand Down
Loading
Loading