From 02190d46a5c03ff79e4e9283193e50f1f8ec8fbc Mon Sep 17 00:00:00 2001 From: tina_c_lin Date: Mon, 13 Nov 2023 12:50:26 +0800 Subject: [PATCH 1/6] fix: fix the OverflowTooltip issue in a specific environment --- packages/react/src/tooltip/OverflowTooltip.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/react/src/tooltip/OverflowTooltip.js b/packages/react/src/tooltip/OverflowTooltip.js index 37152c781b..3b62f82f3d 100644 --- a/packages/react/src/tooltip/OverflowTooltip.js +++ b/packages/react/src/tooltip/OverflowTooltip.js @@ -4,6 +4,23 @@ import { Truncate } from '../truncate'; import { useTruncateStyle } from '../truncate/styles'; import Tooltip from './Tooltip'; +const hasEllipsis = (el) => { + const isNoWrap = el.style.whiteSpace === 'nowrap' || window.getComputedStyle(el).whiteSpace === 'nowrap'; + if (isNoWrap) { + let scrollWidth = el.scrollWidth; + const oldWidth = el.style.width; + el.style.width = "max-content"; // set width to max-content to get the actual width of the element + const [clientRect] = el.getClientRects(); + if (clientRect?.width > scrollWidth) { + scrollWidth = clientRect?.width; + } + el.style.width = oldWidth; + return scrollWidth > el.clientWidth; + } + + return el.scrollHeight > el.clientHeight; +}; + const OverflowTooltip = forwardRef(( { children, @@ -16,7 +33,7 @@ const OverflowTooltip = forwardRef(( const truncateStyle = useTruncateStyle(); const onMouseEnter = useCallback((event) => { const el = event.currentTarget; - setIsOverflown((el.scrollHeight > el.clientHeight) || (el.scrollWidth > el.clientWidth)); + setIsOverflown(hasEllipsis(el)); }, []); const onMouseLeave = useCallback((event) => { setIsOverflown(false); From 55842490c4dfbecab998399f1084ef460576c710 Mon Sep 17 00:00:00 2001 From: cheton Date: Wed, 15 Nov 2023 14:35:33 +0800 Subject: [PATCH 2/6] feat: improve overflow detection using `getClientRects() --- packages/react/src/tooltip/OverflowTooltip.js | 63 +++++++++------ .../tooltip/__tests__/OverflowTooltip.test.js | 76 +++++++++++++++++++ .../src/tooltip/__tests__/Tooltip.test.js | 10 +-- 3 files changed, 120 insertions(+), 29 deletions(-) create mode 100644 packages/react/src/tooltip/__tests__/OverflowTooltip.test.js diff --git a/packages/react/src/tooltip/OverflowTooltip.js b/packages/react/src/tooltip/OverflowTooltip.js index 3b62f82f3d..137496d6a2 100644 --- a/packages/react/src/tooltip/OverflowTooltip.js +++ b/packages/react/src/tooltip/OverflowTooltip.js @@ -4,23 +4,6 @@ import { Truncate } from '../truncate'; import { useTruncateStyle } from '../truncate/styles'; import Tooltip from './Tooltip'; -const hasEllipsis = (el) => { - const isNoWrap = el.style.whiteSpace === 'nowrap' || window.getComputedStyle(el).whiteSpace === 'nowrap'; - if (isNoWrap) { - let scrollWidth = el.scrollWidth; - const oldWidth = el.style.width; - el.style.width = "max-content"; // set width to max-content to get the actual width of the element - const [clientRect] = el.getClientRects(); - if (clientRect?.width > scrollWidth) { - scrollWidth = clientRect?.width; - } - el.style.width = oldWidth; - return scrollWidth > el.clientWidth; - } - - return el.scrollHeight > el.clientHeight; -}; - const OverflowTooltip = forwardRef(( { children, @@ -29,23 +12,55 @@ const OverflowTooltip = forwardRef(( ref, ) => { const contentRef = useRef(); - const [isOverflown, setIsOverflown] = useState(); + const [isOverflow, setIsOverflow] = useState(); const truncateStyle = useTruncateStyle(); + + const detectOverflow = useCallback((el, sizeProperty) => { + if (sizeProperty !== 'width' && sizeProperty !== 'height') { + console.error(`Invalid size property: ${sizeProperty}. Use 'width' or 'height'.`); + return false; + } + + const originalSize = el.style[sizeProperty]; + const s1 = el.getClientRects()?.[0]?.[sizeProperty]; + el.style[sizeProperty] = 'max-content'; + const s2 = el.getClientRects()?.[0]?.[sizeProperty]; + el.style[sizeProperty] = originalSize; // Revert to original size + + if (sizeProperty === 'width') { + return s1 < s2 || el.scrollWidth > el.clientWidth; + } + + if (sizeProperty === 'height') { + return s1 < s2 || el.scrollHeight > el.clientHeight; + } + + return false; + }, []); + + const eventTargetFn = useCallback(() => { + return contentRef.current; + }, []); + const onMouseEnter = useCallback((event) => { const el = event.currentTarget; - setIsOverflown(hasEllipsis(el)); - }, []); + const isWidthOverflow = detectOverflow(el, 'width'); + const isHeightOverflow = detectOverflow(el, 'height'); + const isOverflowDetected = isWidthOverflow || isHeightOverflow; + setIsOverflow(isOverflowDetected); + }, [detectOverflow]); + const onMouseLeave = useCallback((event) => { - setIsOverflown(false); + setIsOverflow(false); }, []); - useEventListener(() => contentRef.current, 'mouseenter', onMouseEnter); - useEventListener(() => contentRef.current, 'mouseleave', onMouseLeave); + useEventListener(eventTargetFn, 'mouseenter', onMouseEnter); + useEventListener(eventTargetFn, 'mouseleave', onMouseLeave); return ( {(typeof children === 'function') ? ( diff --git a/packages/react/src/tooltip/__tests__/OverflowTooltip.test.js b/packages/react/src/tooltip/__tests__/OverflowTooltip.test.js new file mode 100644 index 0000000000..76dcbc1b74 --- /dev/null +++ b/packages/react/src/tooltip/__tests__/OverflowTooltip.test.js @@ -0,0 +1,76 @@ +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { render } from '@tonic-ui/react/test-utils/render'; +import { Box, OverflowTooltip } from '@tonic-ui/react/src'; +import React from 'react'; + +describe('OverflowTooltip', () => { + it('should display an overflow tooltip tooltip if the clientWidth is less than the scrollWidth', async () => { + const user = userEvent.setup(); + const textContent = 'This text string will be truncated when exceeding its container width. To see this in action, try resizing your browser viewport. If the text overflows, a tooltip will appear, displaying the full content.'; + const tooltipLabel = 'tooltip label'; + + // Define the scrollWidth, offsetWidth, and clientWidth of the textContent + const scrollWidth = 1193; + const offsetWidth = 900; + const clientWidth = 900; + const maxWidth = scrollWidth; + + render( + + + {textContent} + + + ); + + const text = screen.getByText(textContent); + + // Mock scrollWidth, offsetWidth, and clientWidth to simulate overflow + Object.defineProperty(text, 'scrollWidth', { configurable: true, value: scrollWidth }); + Object.defineProperty(text, 'offsetWidth', { configurable: true, value: offsetWidth }); + Object.defineProperty(text, 'clientWidth', { configurable: true, value: clientWidth }); + + await user.hover(text); + + await waitFor(() => { + expect(screen.queryByText(tooltipLabel)).toBeInTheDocument(); + }); + }); + + it('should display an overflow tooltip when the `clientRect` varies while setting the width to `max-content`', async () => { + const user = userEvent.setup(); + const textContent = 'This is a string to test overflow tooltip'; + const tooltipLabel = 'tooltip label'; + + // Define the clientRects of the textContent + const contentWidth = 120; + const contentHeight = 20; + const maxContentWidth = 120.84; + const maxContentHeight = 20; + + render( + + + {textContent} + + + ); + + const text = screen.getByText(textContent); + + // Mock the getClientRects() function to simulate overflow + text.getClientRects = jest.fn(); + text.getClientRects + .mockReturnValueOnce([{ x: 0, y: 0, top: 0, bottom: contentHeight, left: 0, right: contentWidth, width: contentWidth, height: contentHeight }]) + .mockReturnValueOnce([{ x: 0, y: 0, top: 0, bottom: contentHeight, left: 0, right: maxContentWidth, width: maxContentWidth, height: contentHeight }]) + .mockReturnValueOnce([{ x: 0, y: 0, top: 0, bottom: contentHeight, left: 0, right: contentWidth, width: contentWidth, height: contentHeight }]) + .mockReturnValueOnce([{ x: 0, y: 0, top: 0, bottom: maxContentHeight, left: 0, right: contentWidth, width: contentWidth, height: maxContentHeight }]); + + await user.hover(text); + + await waitFor(() => { + expect(screen.queryByText(tooltipLabel)).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/react/src/tooltip/__tests__/Tooltip.test.js b/packages/react/src/tooltip/__tests__/Tooltip.test.js index 75d754189b..dc18f571d7 100644 --- a/packages/react/src/tooltip/__tests__/Tooltip.test.js +++ b/packages/react/src/tooltip/__tests__/Tooltip.test.js @@ -35,7 +35,7 @@ describe('Tooltip', () => { await testA11y(container); }); - it('should not show on mouseover if `disabled` is set to `false`', async () => { + it('should display a tooltip if `disabled` is set to `false`', async () => { const user = userEvent.setup(); render(); @@ -45,7 +45,7 @@ describe('Tooltip', () => { expect(await screen.findByRole('tooltip')).toBeInTheDocument(); }); - it('should not show on mouseover if `disabled` is set to `true`', async () => { + it('should not display a tooltip if `disabled` is set to `true`', async () => { const user = userEvent.setup(); render(); @@ -57,7 +57,7 @@ describe('Tooltip', () => { }); }); - it('should display on mouseover and close when clicked if `closeOnClick` is set to `true`', async () => { + it('should display a tooltip and close when clicked if `closeOnClick` is set to `true`', async () => { const user = userEvent.setup(); render(); @@ -74,7 +74,7 @@ describe('Tooltip', () => { }); }); - it('should display on mouseover and close when `Escape` key is pressed if `closeOnEsc` is set to `true`', async () => { + it('should display a tooltip and close when `Escape` key is pressed if `closeOnEsc` is set to `true`', async () => { const user = userEvent.setup(); render(); @@ -91,7 +91,7 @@ describe('Tooltip', () => { }); }); - it('should display on mouseover and close when pointer down if `closeOnPointerDown` is set to `true`', async () => { + it('should display a tooltip and close when pointer down if `closeOnPointerDown` is set to `true`', async () => { const user = userEvent.setup(); render(); From e5373b50a2317a6e2dd20d2bf7ea81a0382818a1 Mon Sep 17 00:00:00 2001 From: cheton Date: Wed, 15 Nov 2023 14:35:56 +0800 Subject: [PATCH 3/6] feat: import utility functions from `@tonic-ui/utils` --- packages/react-hooks/src/useEventListener.js | 12 +----------- packages/react-hooks/src/useMediaQuery.js | 4 +--- packages/react-hooks/src/useOutsideClick.js | 9 ++------- 3 files changed, 4 insertions(+), 21 deletions(-) diff --git a/packages/react-hooks/src/useEventListener.js b/packages/react-hooks/src/useEventListener.js index 48ad7d04a8..86ea80c149 100644 --- a/packages/react-hooks/src/useEventListener.js +++ b/packages/react-hooks/src/useEventListener.js @@ -1,17 +1,7 @@ +import { noop, runIfFn } from '@tonic-ui/utils'; import { useEffect } from 'react'; import useEventCallback from './useEventCallback'; -// TODO: move to '@tonic-ui/utils' -const noop = () => {}; - -// TODO: move to '@tonic-ui/utils' -const runIfFn = (valueOrFn, ...args) => { - if (typeof valueOrFn === 'function') { - return valueOrFn(...args); - } - return valueOrFn; -}; - /** * A custom Hook to manage browser event listeners. * diff --git a/packages/react-hooks/src/useMediaQuery.js b/packages/react-hooks/src/useMediaQuery.js index 0f14302118..826acd9324 100644 --- a/packages/react-hooks/src/useMediaQuery.js +++ b/packages/react-hooks/src/useMediaQuery.js @@ -1,8 +1,6 @@ +import { noop } from '@tonic-ui/utils'; import { useEffect, useState } from 'react'; -// TODO: move to '@tonic-ui/utils' -const noop = () => {}; - const getInitialState = (query, defaultValue) => { if (defaultValue !== undefined) { return defaultValue; diff --git a/packages/react-hooks/src/useOutsideClick.js b/packages/react-hooks/src/useOutsideClick.js index 034f7917fb..73d67fb1ab 100644 --- a/packages/react-hooks/src/useOutsideClick.js +++ b/packages/react-hooks/src/useOutsideClick.js @@ -1,12 +1,7 @@ +import { getOwnerDocument, noop } from '@tonic-ui/utils'; import { useEffect } from 'react'; import useEventCallback from './useEventCallback'; -// TODO: move to '@tonic-ui/utils' -const noop = () => {}; - -// TODO: move to '@tonic-ui/utils' -const ownerDocument = (node) => (node?.ownerDocument || document); - const defaultEvents = ['mousedown', 'touchstart']; /** @@ -33,7 +28,7 @@ const useOutsideClick = ( const filteredEvents = (Array.isArray(events) ? events : []) .filter(x => (typeof x === 'string')); - const doc = ownerDocument(ref?.current); + const doc = getOwnerDocument(ref?.current); for (const eventName of filteredEvents) { doc?.addEventListener?.(eventName, savedHandler, true); From 3c9ad7e7ebc231db7f11f18dfc73d89346e45709 Mon Sep 17 00:00:00 2001 From: cheton Date: Wed, 15 Nov 2023 15:13:21 +0800 Subject: [PATCH 4/6] docs: update overtool tooltip examples --- .../components/overflow-tooltip/basic.js | 30 +++++++++++ .../pages/components/overflow-tooltip/facc.js | 21 ++++++++ .../index.page.mdx} | 32 ++---------- .../components/overflow-tooltip/multi-line.js | 50 +++++++++++++++++++ .../components/overflow-tooltip/use-portal.js | 15 ++++++ 5 files changed, 119 insertions(+), 29 deletions(-) create mode 100644 packages/react-docs/pages/components/overflow-tooltip/basic.js create mode 100644 packages/react-docs/pages/components/overflow-tooltip/facc.js rename packages/react-docs/pages/components/{overflow-tooltip.page.mdx => overflow-tooltip/index.page.mdx} (77%) create mode 100644 packages/react-docs/pages/components/overflow-tooltip/multi-line.js create mode 100644 packages/react-docs/pages/components/overflow-tooltip/use-portal.js diff --git a/packages/react-docs/pages/components/overflow-tooltip/basic.js b/packages/react-docs/pages/components/overflow-tooltip/basic.js new file mode 100644 index 0000000000..58c50026e9 --- /dev/null +++ b/packages/react-docs/pages/components/overflow-tooltip/basic.js @@ -0,0 +1,30 @@ +import { Box, Divider, OverflowTooltip } from '@tonic-ui/react'; +import React from 'react'; + +const App = () => { + return ( + <> + + This text string will be truncated when exceeding its container width. To see this in action, try resizing your browser viewport. If the text overflows, a tooltip will appear, displaying the full content. + + + + + This text string is truncted + + + + + This text string is not truncated + + + ); +}; + +export default App; diff --git a/packages/react-docs/pages/components/overflow-tooltip/facc.js b/packages/react-docs/pages/components/overflow-tooltip/facc.js new file mode 100644 index 0000000000..f6376fb208 --- /dev/null +++ b/packages/react-docs/pages/components/overflow-tooltip/facc.js @@ -0,0 +1,21 @@ +import { Icon, Flex, OverflowTooltip, Text } from '@tonic-ui/react'; +import React from 'react'; + +const App = () => { + return ( + + {({ ref, style }) => ( + + + + This text string will be truncated when exceeding its container width. To see this in action, try resizing your browser viewport. If the text overflows, a tooltip will appear, displaying the full content. + + + )} + + ); +}; + +export default App; diff --git a/packages/react-docs/pages/components/overflow-tooltip.page.mdx b/packages/react-docs/pages/components/overflow-tooltip/index.page.mdx similarity index 77% rename from packages/react-docs/pages/components/overflow-tooltip.page.mdx rename to packages/react-docs/pages/components/overflow-tooltip/index.page.mdx index f4e20587fd..eea9eb9795 100644 --- a/packages/react-docs/pages/components/overflow-tooltip.page.mdx +++ b/packages/react-docs/pages/components/overflow-tooltip/index.page.mdx @@ -12,30 +12,11 @@ import { OverflowTooltip } from '@tonic-ui/react'; If the text overflows its container, it will be truncated, and an ellipsis will be added. When you hover or focus on the ellipsis text, a tooltip will appear. -```jsx - - This text string will be truncated when exceeding its container width. To see this in action, try resizing your browser viewport. If the text overflows, a tooltip will appear, displaying the full content. - -``` +{render('./basic')} In the second example, a function is passed as a child of the `OverflowTooltip` component. The function is called with an object containing a `ref` and a `style` property, which should be spread to the element that needs to be truncated. In this case, a `Text` component is used to display the text, and the `ref` and `style` props are spread to it. This allows the `OverflowTooltip` component to detect when the text overflows and display a tooltip. -```jsx - - {({ ref, style }) => ( - - - - This text string will be truncated when exceeding its container width. To see this in action, try resizing your browser viewport. If the text overflows, a tooltip will appear, displaying the full content. - - - )} - -``` +{render('./facc')} ## Commonly Asked Questions @@ -45,14 +26,7 @@ By default, the `OverflowTooltip` component positions the tooltip relative to it To mitigate this issue, you can pass `PopperProps={{ usePortal: true }}` to `OverflowTooltip` to make it positioned on the document root. -```jsx - - This text string will be truncated when exceeding its container width. To see this in action, try resizing your browser viewport. If the text overflows, a tooltip will appear, displaying the full content. - -``` +{render('./use-portal')} ## Props diff --git a/packages/react-docs/pages/components/overflow-tooltip/multi-line.js b/packages/react-docs/pages/components/overflow-tooltip/multi-line.js new file mode 100644 index 0000000000..2c10bfc936 --- /dev/null +++ b/packages/react-docs/pages/components/overflow-tooltip/multi-line.js @@ -0,0 +1,50 @@ +import { Box, Divider, OverflowTooltip, Text } from '@tonic-ui/react'; +import React from 'react'; + +const App = () => { + return ( + <> + + + {({ ref, style }) => ( + + This text string will be truncated when exceeding its container width. To see this in action, try resizing your browser viewport. If the text overflows, a tooltip will appear, displaying the full content. + + )} + + + + + + {({ ref, style }) => ( + + This text string will be truncated when exceeding its container width. To see this in action, try resizing your browser viewport. If the text overflows, a tooltip will appear, displaying the full content. + + )} + + + + ); +}; + +export default App; diff --git a/packages/react-docs/pages/components/overflow-tooltip/use-portal.js b/packages/react-docs/pages/components/overflow-tooltip/use-portal.js new file mode 100644 index 0000000000..f8429b26c1 --- /dev/null +++ b/packages/react-docs/pages/components/overflow-tooltip/use-portal.js @@ -0,0 +1,15 @@ +import { OverflowTooltip } from '@tonic-ui/react'; +import React from 'react'; + +const App = () => { + return ( + + This text string will be truncated when exceeding its container width. To see this in action, try resizing your browser viewport. If the text overflows, a tooltip will appear, displaying the full content. + + ); +}; + +export default App; From 24ec1300413145557c7999ecf448e53f57907526 Mon Sep 17 00:00:00 2001 From: cheton Date: Wed, 15 Nov 2023 16:21:01 +0800 Subject: [PATCH 5/6] docs: update tooltip and overflow-tooltip examples --- .../{use-portal.js => faq-use-portal.js} | 0 ...facc.js => function-as-child-component.js} | 0 .../overflow-tooltip/index.page.mdx | 4 +- .../pages/components/tooltip.page.mdx | 493 ------------------ .../pages/components/tooltip/controlled.js | 23 + .../components/tooltip/custom-appearance.js | 43 ++ .../components/tooltip/faq-flip-modifier.js | 79 +++ .../components/tooltip/faq-use-portal.js | 13 + .../components/tooltip/focusable-content.js | 10 + .../pages/components/tooltip/index.page.mdx | 187 +++++++ .../components/tooltip/menu-with-tooltip.js | 79 +++ .../pages/components/tooltip/placement.js | 30 ++ .../tooltip/positioning-follow-cursor.js | 10 + .../tooltip/positioning-next-cursor.js | 10 + .../components/tooltip/positioning-offset.js | 49 ++ .../tooltip/tooltip-around-disabled-button.js | 10 + .../tooltip-around-wrapped-disabled-button.js | 12 + .../tooltip/tooltip-arrow-disabled.js | 10 + .../components/tooltip/tooltip-disabled.js | 16 + .../pages/components/tooltip/uncontrolled.js | 13 + .../components/tooltip/unfocusable-content.js | 10 + 21 files changed, 606 insertions(+), 495 deletions(-) rename packages/react-docs/pages/components/overflow-tooltip/{use-portal.js => faq-use-portal.js} (100%) rename packages/react-docs/pages/components/overflow-tooltip/{facc.js => function-as-child-component.js} (100%) delete mode 100644 packages/react-docs/pages/components/tooltip.page.mdx create mode 100644 packages/react-docs/pages/components/tooltip/controlled.js create mode 100644 packages/react-docs/pages/components/tooltip/custom-appearance.js create mode 100644 packages/react-docs/pages/components/tooltip/faq-flip-modifier.js create mode 100644 packages/react-docs/pages/components/tooltip/faq-use-portal.js create mode 100644 packages/react-docs/pages/components/tooltip/focusable-content.js create mode 100644 packages/react-docs/pages/components/tooltip/index.page.mdx create mode 100644 packages/react-docs/pages/components/tooltip/menu-with-tooltip.js create mode 100644 packages/react-docs/pages/components/tooltip/placement.js create mode 100644 packages/react-docs/pages/components/tooltip/positioning-follow-cursor.js create mode 100644 packages/react-docs/pages/components/tooltip/positioning-next-cursor.js create mode 100644 packages/react-docs/pages/components/tooltip/positioning-offset.js create mode 100644 packages/react-docs/pages/components/tooltip/tooltip-around-disabled-button.js create mode 100644 packages/react-docs/pages/components/tooltip/tooltip-around-wrapped-disabled-button.js create mode 100644 packages/react-docs/pages/components/tooltip/tooltip-arrow-disabled.js create mode 100644 packages/react-docs/pages/components/tooltip/tooltip-disabled.js create mode 100644 packages/react-docs/pages/components/tooltip/uncontrolled.js create mode 100644 packages/react-docs/pages/components/tooltip/unfocusable-content.js diff --git a/packages/react-docs/pages/components/overflow-tooltip/use-portal.js b/packages/react-docs/pages/components/overflow-tooltip/faq-use-portal.js similarity index 100% rename from packages/react-docs/pages/components/overflow-tooltip/use-portal.js rename to packages/react-docs/pages/components/overflow-tooltip/faq-use-portal.js diff --git a/packages/react-docs/pages/components/overflow-tooltip/facc.js b/packages/react-docs/pages/components/overflow-tooltip/function-as-child-component.js similarity index 100% rename from packages/react-docs/pages/components/overflow-tooltip/facc.js rename to packages/react-docs/pages/components/overflow-tooltip/function-as-child-component.js diff --git a/packages/react-docs/pages/components/overflow-tooltip/index.page.mdx b/packages/react-docs/pages/components/overflow-tooltip/index.page.mdx index eea9eb9795..3c8e04ed6a 100644 --- a/packages/react-docs/pages/components/overflow-tooltip/index.page.mdx +++ b/packages/react-docs/pages/components/overflow-tooltip/index.page.mdx @@ -16,7 +16,7 @@ If the text overflows its container, it will be truncated, and an ellipsis will In the second example, a function is passed as a child of the `OverflowTooltip` component. The function is called with an object containing a `ref` and a `style` property, which should be spread to the element that needs to be truncated. In this case, a `Text` component is used to display the text, and the `ref` and `style` props are spread to it. This allows the `OverflowTooltip` component to detect when the text overflows and display a tooltip. -{render('./facc')} +{render('./function-as-child-component')} ## Commonly Asked Questions @@ -26,7 +26,7 @@ By default, the `OverflowTooltip` component positions the tooltip relative to it To mitigate this issue, you can pass `PopperProps={{ usePortal: true }}` to `OverflowTooltip` to make it positioned on the document root. -{render('./use-portal')} +{render('./faq-use-portal')} ## Props diff --git a/packages/react-docs/pages/components/tooltip.page.mdx b/packages/react-docs/pages/components/tooltip.page.mdx deleted file mode 100644 index b403505272..0000000000 --- a/packages/react-docs/pages/components/tooltip.page.mdx +++ /dev/null @@ -1,493 +0,0 @@ -# Tooltip - -A tooltip is a brief, informative message that appears when a user interacts with an element. Tooltips are usually initiated in one of two ways: through a mouse-hover or keyboard-hover action. - -The `Tooltip` component follows the [WAI-ARIA](https://www.w3.org/TR/wai-aria-practices/#tooltip) Tooltip pattern. - -## Import - -```js -import { Tooltip } from '@tonic-ui/react'; -``` - -## Usage - -By default, you have to pass a single React element child to the `Tooltip` component. - -```jsx disabled - - Text content - -``` - -If you need to pass more than one child element or non-element children, wrap them in an element or pass the `shouldWrapChildren` prop. - -```jsx disabled - - Text content - - - - Text content - - - - - Text content - -``` - -### Controlled and uncontrolled tooltip - -Pass `isOpen` to the `Tooltip` component to control the state of the tooltip. - -```jsx -function Example() { - const [on, toggle] = useToggle(false); - - return ( - <> - - - - - Text content - - - ); -} -``` - -Tooltip is uncontrolled by default. You can set `defaultIsOpen` to `true` to have the tooltip open for the first render. - -```jsx - - Text content - -``` - -### Hide the tooltip - -To hide a tooltip in your app, you can use the `label` and `disabled` props. To hide the tooltip, set the `label` prop to an empty value represented by `""`, or set the `disabled` prop to `true`. Here are examples: - -```jsx - - - - - - - - - -``` - -### Hide the arrow of the tooltip - -To remove the arrow from a tooltip, you can pass `arrow={false}` to the `Tooltip` component. - -```jsx - - - -``` - -### Tooltip around disabled button - -By default the `Tooltip` is not shown when it is around a disabled `Button`. - -```jsx - - - -``` - -To show the `Tooltip` on a disabled `Button`, pass the `shouldWrapChildren` prop. - -```jsx - - - - - -``` - -### Tooltip with focusable content - -If the children of the tooltip is a focusable element, the tooltip will show when you focus or hover on the element, and will hide when you blur or move cursor out of the element. - -```jsx - - - -``` - -### Tooltip with non-focusable content - -If the tooltip anchor is not a focusable content, just like the text string, you can wrap it with a `Text` component and set `tabIndex="0"` to make it tabbable. - -```jsx - - Text content - -``` - -### Adjust tooltip positioning - -#### Using the `offset` prop - -The `offset` prop allows you to adjust the positioning of the tooltip by accepting an array of two numbers in the format `[skidding, distance]`. - -**Skidding** - -The `skidding` number determines the displacement of the `TooltipContent` from the `TooltipTrigger` and is measured in pixels. - -**Distance** - -The `distance` number controls the distance between the `TooltipContent` and the `TooltipTrigger` and is measured in pixels. - -```jsx noInline -render(() => { - const [skidding, setSkidding] = React.useState(0); - const [distance, setDistance] = React.useState(8); - - return ( - <> - - - skidding - - - setSkidding(Number(e.target.value))} - /> - {skidding} - - - - - distance - - - setDistance(Number(e.target.value))} - /> - {distance} - - - - Text content - - - ); -}); -``` - -#### Using the `nextToCursor` prop - -The `nextToCursor` prop positions the tooltip next to the cursor. - -```jsx - - Hover Me - -``` - -#### Using the `followCursor` prop - -The `followCursor` prop makes the tooltip follow the cursor as it moves. - -```jsx - - Hover Me - -``` - -### Customizing tooltip appearance - -To customize the appearance of a tooltip, you can pass style props to the Tooltip component. Here's an example that shows how to use the `useColorMode` and `useColorStyle` Hooks to determine the current color mode and color style, and then set the background and text colors of the tooltip accordingly. This can help ensure the tooltip looks consistent with your app's color scheme. Additionally, you can use a component as the label of the tooltip to render more complex content. - -```jsx -function Example() { - const [colorMode] = useColorMode(); - const [colorStyle] = useColorStyle({ colorMode }); - const backgroundColor = { - dark: 'gray:80', - light: 'white', - }[colorMode]; - const color = { - dark: 'white:primary', - light: 'black:primary', - }[colorMode]; - - return ( - - - Hover Me - - - - Title - - Content - - )} - backgroundColor={backgroundColor} - color={color} - > - Hover Me - - - ); -} -``` - -### Placement - -Use the `placement` prop to control the placement of the tooltip. - -```jsx disabled - - Tooltip anchor - -``` - -```jsx - - - {['top-start', 'top', 'top-end', - 'right-start', 'right', 'right-end', - 'bottom-start', 'bottom', 'bottom-end', - 'left-start', 'left', 'left-end' - ].map(placement => ( - - - - ))} - - -``` - -### More examples - -#### `Menu` component with `Tooltip` - -This example demonstrates how to use the `Menu` component along with a tooltip for navigating through a set of options or actions. - -```jsx -function Example() { - const [colorMode] = useColorMode(); - const [colorStyle] = useColorStyle({ colorMode }); - const inputRef = React.useRef(); - const [menuItem, setMenuItem] = React.useState('hostname'); - const handleMenuClick = (event) => { - // [optional] persist `Synthetic Event` for React v16 and earlier versions - event.persist(); - - const { value } = event.target.attributes.value; - setMenuItem(value); - - setTimeout(() => { - if (inputRef.current) { - inputRef.current.focus(); - } - }, 0); - }; - const buttonText = { - 'hostname': 'Search by: Endpoint name', - 'filename': 'Search by: File name', - }[menuItem]; - - return ( - - - - - - {buttonText} - - - - Endpoint name - File name - - - - - - - - ); -} -``` - -## Commonly Asked Questions - -### Resolving tooltip content cut-off with `PopperProps` - -By default, the `Tooltip` component positions the tooltip relative to its parent container. In some cases, the tooltip content might be cut off when it extends outside the container that holds it. - -To address this issue, you can pass `PopperProps={{ usePortal: true }}` to `Tooltip` to make it positioned on the document root. - -```jsx - - Hover Me - -``` - -### Automatically adjusting tooltip placement with the `flip` modifier - -The `flip` modifier is a useful feature that allows for automatic adjustment of tooltip placement when it is at risk of overflowing the specified boundary. To learn more about utilizing the `flip` modifier, please refer to [Popper.js documentation](https://popper.js.org/docs/v2/modifiers/flip/). - -In the following example, the tooltip's placement is initially set to `top`. However, if the placement is not suitable due to space constraints, the opposite `bottom` placement will be used instead. - -```jsx noInline -const FormGroup = (props) => ( - -); - -render(() => { - const [colorMode] = useColorMode(); - const [colorStyle] = useColorStyle({ colorMode }); - const [isFlipModifierEnabled, toggleIsFlipModifierEnabled] = useToggle(true); - - return ( - <> - - - Modifiers - - - - - toggleIsFlipModifierEnabled()} - /> - - Enable flip modifier - - - - - - - - Reference - - - - - - ); -}); -``` - -## Props - -### Tooltip - -| Name | Type | Default | Description | -| :--- | :--- | :------ | :---------- | -| PopperComponent | ElementType | Popper | The component used for the popper. | -| PopperProps | object | | Props applied to the Popper component. | -| TooltipArrowComponent | ElementType | TooltipArrow | The component used for the tooltip arrow. | -| TooltipArrowProps | object | | Props applied to the `TooltipArrow` component. | -| TransitionComponent | ElementType | Grow | The component used for the transition. | -| TransitionProps | object | | Props applied to the [Transition](http://reactcommunity.org/react-transition-group/transition#Transition-props) element. | -| TransitionProps.appear | boolean | true | | -| arrow | boolean | true | If `true`, adds an arrow to the tooltip. | -| children | ReactNode \| `(context) => ReactNode` | | | -| closeOnClick | boolean | true | If `true`, the tooltip will close upon clicking. | -| closeOnEsc | boolean | true | If `true`, the tooltip will close upon pressing the escape key. | -| closeOnPointerDown | boolean | true | If `true`, the tooltip will close while the pointer is pressed down. | -| defaultIsOpen | boolean | false | Whether the tooltip will be open by default. | -| disabled | boolean | | If `true`, the tooltip will not display. | -| enterDelay | number | 100 | The delay in milliseconds before the tooltip appears. | -| followCursor | boolean | | If `true`, the tooltip will follow the cursor. | -| isOpen | boolean | | If `true`, show the tooltip. | -| label | string \| ReactNode | | If the tooltip label is a blank or empty string, the tooltip will not display. | -| leaveDelay | number | 0 | The delay in milliseconds before the tooltip disappears. | -| nextToCursor | boolean | | If `true`, the tooltip will be positioned next to the cursor. | -| offset | [skidding, distance] | [0, 8] | The skidding and distance of the tooltip. | -| onClose| function | | Callback fired when the tooltip is closed. | -| onOpen | function | | Callback fired when the tooltip is opened. | -| openOnFocus | boolean | true | If `true`, the tooltip will open upon receiving focus. | -| placement | PopperJS.Placement | 'bottom' | Position the tooltip relative to the trigger element as well as surrounding elements. One of: 'top', 'bottom', 'right', 'left', 'top-start', 'top-end', 'bottom-start', 'bottom-end', 'right-start', 'right-end', 'left-start', 'left-end' | -| shouldWrapChildren | boolean | false | If `true`, the tooltip will be wrapped in a `Box` component. Otherwise, you have to ensure tooltip has only one child node. | diff --git a/packages/react-docs/pages/components/tooltip/controlled.js b/packages/react-docs/pages/components/tooltip/controlled.js new file mode 100644 index 0000000000..e42b2e7ceb --- /dev/null +++ b/packages/react-docs/pages/components/tooltip/controlled.js @@ -0,0 +1,23 @@ +import { useToggle } from '@tonic-ui/react-hooks'; +import { Flex, Switch, Text, Tooltip } from '@tonic-ui/react'; +import React from 'react'; + +const App = () => { + const [on, toggle] = useToggle(false); + + return ( + <> + + + + + Text content + + + ); +}; + +export default App; diff --git a/packages/react-docs/pages/components/tooltip/custom-appearance.js b/packages/react-docs/pages/components/tooltip/custom-appearance.js new file mode 100644 index 0000000000..2dc2224ece --- /dev/null +++ b/packages/react-docs/pages/components/tooltip/custom-appearance.js @@ -0,0 +1,43 @@ +import { Box, Divider, Text, Tooltip, useColorMode } from '@tonic-ui/react'; +import React from 'react'; + +const App = () => { + const [colorMode] = useColorMode(); + const backgroundColor = { + dark: 'gray:80', + light: 'white', + }[colorMode]; + const color = { + dark: 'white:primary', + light: 'black:primary', + }[colorMode]; + + return ( + <> + + Hover Me + + + + Title + + Content + + )} + backgroundColor={backgroundColor} + color={color} + > + Hover Me + + + ); +}; + +export default App; diff --git a/packages/react-docs/pages/components/tooltip/faq-flip-modifier.js b/packages/react-docs/pages/components/tooltip/faq-flip-modifier.js new file mode 100644 index 0000000000..25f3417627 --- /dev/null +++ b/packages/react-docs/pages/components/tooltip/faq-flip-modifier.js @@ -0,0 +1,79 @@ +import { useToggle } from '@tonic-ui/react-hooks'; +import { + Box, + Checkbox, + Divider, + Flex, + Scrollbar, + Space, + Text, + TextLabel, + Tooltip, + useColorMode, + useColorStyle, +} from '@tonic-ui/react'; +import React from 'react'; + +const FormGroup = (props) => ( + +); + +const App = () => { + const [colorMode] = useColorMode(); + const [colorStyle] = useColorStyle({ colorMode }); + const [isFlipModifierEnabled, toggleIsFlipModifierEnabled] = useToggle(true); + + return ( + <> + + + Modifiers + + + + + toggleIsFlipModifierEnabled()} + /> + + Enable flip modifier + + + + + + + + Reference + + + + + + ); +}; + +export default App; diff --git a/packages/react-docs/pages/components/tooltip/faq-use-portal.js b/packages/react-docs/pages/components/tooltip/faq-use-portal.js new file mode 100644 index 0000000000..e91b57f621 --- /dev/null +++ b/packages/react-docs/pages/components/tooltip/faq-use-portal.js @@ -0,0 +1,13 @@ +import { Text, Tooltip } from '@tonic-ui/react'; +import React from 'react'; + +const App = () => ( + + Hover Me + +); + +export default App; diff --git a/packages/react-docs/pages/components/tooltip/focusable-content.js b/packages/react-docs/pages/components/tooltip/focusable-content.js new file mode 100644 index 0000000000..d6089c93e1 --- /dev/null +++ b/packages/react-docs/pages/components/tooltip/focusable-content.js @@ -0,0 +1,10 @@ +import { Button, Tooltip } from '@tonic-ui/react'; +import React from 'react'; + +const App = () => ( + + + +); + +export default App; diff --git a/packages/react-docs/pages/components/tooltip/index.page.mdx b/packages/react-docs/pages/components/tooltip/index.page.mdx new file mode 100644 index 0000000000..e470f2f1c7 --- /dev/null +++ b/packages/react-docs/pages/components/tooltip/index.page.mdx @@ -0,0 +1,187 @@ +# Tooltip + +A tooltip is a brief, informative message that appears when a user interacts with an element. Tooltips are usually initiated in one of two ways: through a mouse-hover or keyboard-hover action. + +The `Tooltip` component follows the [WAI-ARIA](https://www.w3.org/TR/wai-aria-practices/#tooltip) Tooltip pattern. + +## Import + +```js +import { Tooltip } from '@tonic-ui/react'; +``` + +## Usage + +By default, you have to pass a single React element child to the `Tooltip` component. + +```jsx disabled + + Text content + +``` + +If you need to pass more than one child element or non-element children, wrap them in an element or pass the `shouldWrapChildren` prop. + +```jsx disabled + + Text content + + + + Text content + + + + + Text content + +``` + +### Controlled and uncontrolled tooltip + +Pass `isOpen` to the `Tooltip` component to control the state of the tooltip. + +{render('./controlled')} + +Tooltip is uncontrolled by default. You can set `defaultIsOpen` to `true` to have the tooltip open for the first render. + +{render('./uncontrolled')} + +### Hide the tooltip + +To hide a tooltip in your app, you can use the `label` and `disabled` props. To hide the tooltip, set the `label` prop to an empty value represented by `""`, or set the `disabled` prop to `true`. Here are examples: + +{render('./tooltip-disabled')} + +### Hide the arrow of the tooltip + +To remove the arrow from a tooltip, you can pass `arrow={false}` to the `Tooltip` component. + +{render('./tooltip-arrow-disabled')} + +### Tooltip around disabled button + +By default the `Tooltip` is not shown when it is around a disabled `Button`. + +{render('./tooltip-around-disabled-button')} + +To show the `Tooltip` on a disabled `Button`, pass the `shouldWrapChildren` prop. + +{render('./tooltip-around-wrapped-disabled-button')} + +### Tooltip with focusable content + +If the children of the tooltip is a focusable element, the tooltip will show when you focus or hover on the element, and will hide when you blur or move cursor out of the element. + +{render('./focusable-content')} + +### Tooltip with non-focusable content + +If the tooltip anchor is not a focusable content, just like the text string, you can wrap it with a `Text` component and set `tabIndex="0"` to make it tabbable. + +{render('./unfocusable-content')} + +### Adjust tooltip positioning + +#### Using the `offset` prop + +The `offset` prop allows you to adjust the positioning of the tooltip by accepting an array of two numbers in the format `[skidding, distance]`. + +**Skidding** + +The `skidding` number determines the displacement of the `TooltipContent` from the `TooltipTrigger` and is measured in pixels. + +**Distance** + +The `distance` number controls the distance between the `TooltipContent` and the `TooltipTrigger` and is measured in pixels. + +{render('./positioning-offset')} + +#### Using the `nextToCursor` prop + +The `nextToCursor` prop positions the tooltip next to the cursor. + +{render('./positioning-next-cursor')} + +#### Using the `followCursor` prop + +The `followCursor` prop makes the tooltip follow the cursor as it moves. + +{render('./positioning-follow-cursor')} + +### Customizing tooltip appearance + +To customize the appearance of a tooltip, you can pass style props to the Tooltip component. Here's an example that shows how to use the `useColorMode` and `useColorStyle` Hooks to determine the current color mode and color style, and then set the background and text colors of the tooltip accordingly. This can help ensure the tooltip looks consistent with your app's color scheme. Additionally, you can use a component as the label of the tooltip to render more complex content. + +{render('./custom-appearance')} + +### Placement + +Use the `placement` prop to control the placement of the tooltip. + +```jsx disabled + + Tooltip anchor + +``` + +{render('./placement')} + +### More examples + +#### `Menu` component with `Tooltip` + +This example demonstrates how to use the `Menu` component along with a tooltip for navigating through a set of options or actions. + +{render('./menu-with-tooltip')} + +## Commonly Asked Questions + +### Resolving tooltip content cut-off with `PopperProps` + +By default, the `Tooltip` component positions the tooltip relative to its parent container. In some cases, the tooltip content might be cut off when it extends outside the container that holds it. + +To address this issue, you can pass `PopperProps={{ usePortal: true }}` to `Tooltip` to make it positioned on the document root. + +{render('./faq-use-portal')} + +### Automatically adjusting tooltip placement with the `flip` modifier + +The `flip` modifier is a useful feature that allows for automatic adjustment of tooltip placement when it is at risk of overflowing the specified boundary. To learn more about utilizing the `flip` modifier, please refer to [Popper.js documentation](https://popper.js.org/docs/v2/modifiers/flip/). + +In the following example, the tooltip's placement is initially set to `top`. However, if the placement is not suitable due to space constraints, the opposite `bottom` placement will be used instead. + +{render('./faq-flip-modifier')} + +## Props + +### Tooltip + +| Name | Type | Default | Description | +| :--- | :--- | :------ | :---------- | +| PopperComponent | ElementType | Popper | The component used for the popper. | +| PopperProps | object | | Props applied to the Popper component. | +| TooltipArrowComponent | ElementType | TooltipArrow | The component used for the tooltip arrow. | +| TooltipArrowProps | object | | Props applied to the `TooltipArrow` component. | +| TransitionComponent | ElementType | Grow | The component used for the transition. | +| TransitionProps | object | | Props applied to the [Transition](http://reactcommunity.org/react-transition-group/transition#Transition-props) element. | +| TransitionProps.appear | boolean | true | | +| arrow | boolean | true | If `true`, adds an arrow to the tooltip. | +| children | ReactNode \| `(context) => ReactNode` | | | +| closeOnClick | boolean | true | If `true`, the tooltip will close upon clicking. | +| closeOnEsc | boolean | true | If `true`, the tooltip will close upon pressing the escape key. | +| closeOnPointerDown | boolean | true | If `true`, the tooltip will close while the pointer is pressed down. | +| defaultIsOpen | boolean | false | Whether the tooltip will be open by default. | +| disabled | boolean | | If `true`, the tooltip will not display. | +| enterDelay | number | 100 | The delay in milliseconds before the tooltip appears. | +| followCursor | boolean | | If `true`, the tooltip will follow the cursor. | +| isOpen | boolean | | If `true`, show the tooltip. | +| label | string \| ReactNode | | If the tooltip label is a blank or empty string, the tooltip will not display. | +| leaveDelay | number | 0 | The delay in milliseconds before the tooltip disappears. | +| nextToCursor | boolean | | If `true`, the tooltip will be positioned next to the cursor. | +| offset | [skidding, distance] | [0, 8] | The skidding and distance of the tooltip. | +| onClose| function | | Callback fired when the tooltip is closed. | +| onOpen | function | | Callback fired when the tooltip is opened. | +| openOnFocus | boolean | true | If `true`, the tooltip will open upon receiving focus. | +| placement | PopperJS.Placement | 'bottom' | Position the tooltip relative to the trigger element as well as surrounding elements. One of: 'top', 'bottom', 'right', 'left', 'top-start', 'top-end', 'bottom-start', 'bottom-end', 'right-start', 'right-end', 'left-start', 'left-end' | +| shouldWrapChildren | boolean | false | If `true`, the tooltip will be wrapped in a `Box` component. Otherwise, you have to ensure tooltip has only one child node. | diff --git a/packages/react-docs/pages/components/tooltip/menu-with-tooltip.js b/packages/react-docs/pages/components/tooltip/menu-with-tooltip.js new file mode 100644 index 0000000000..b32e43f60d --- /dev/null +++ b/packages/react-docs/pages/components/tooltip/menu-with-tooltip.js @@ -0,0 +1,79 @@ +import { + Input, + InputGroup, + InputGroupPrepend, + Menu, + MenuButton, + MenuItem, + MenuList, + Text, + Tooltip, + useColorMode, + useColorStyle, +} from '@tonic-ui/react'; +import React, { useCallback, useRef, useState } from 'react'; + +const App = () => { + const [colorMode] = useColorMode(); + const [colorStyle] = useColorStyle({ colorMode }); + const inputRef = useRef(); + const [menuItem, setMenuItem] = useState('hostname'); + const handleMenuClick = useCallback((event) => { + // [optional] persist `Synthetic Event` for React v16 and earlier versions + event.persist(); + + const { value } = event.target.attributes.value; + setMenuItem(value); + + setTimeout(() => { + if (inputRef.current) { + inputRef.current.focus(); + } + }, 0); + }, []); + const buttonText = { + 'hostname': 'Search by: Endpoint name', + 'filename': 'Search by: File name', + }[menuItem]; + + return ( + + + + + + {buttonText} + + + + Endpoint name + File name + + + + + + + + ); +}; + +export default App; diff --git a/packages/react-docs/pages/components/tooltip/placement.js b/packages/react-docs/pages/components/tooltip/placement.js new file mode 100644 index 0000000000..19b1684a87 --- /dev/null +++ b/packages/react-docs/pages/components/tooltip/placement.js @@ -0,0 +1,30 @@ +import { Box, Button, Grid, Tooltip } from '@tonic-ui/react'; +import React from 'react'; + +const App = () => ( + + + {['top-start', 'top', 'top-end', + 'right-start', 'right', 'right-end', + 'bottom-start', 'bottom', 'bottom-end', + 'left-start', 'left', 'left-end' + ].map(placement => ( + + + + ))} + + +); + +export default App; diff --git a/packages/react-docs/pages/components/tooltip/positioning-follow-cursor.js b/packages/react-docs/pages/components/tooltip/positioning-follow-cursor.js new file mode 100644 index 0000000000..2f2f3dd3d4 --- /dev/null +++ b/packages/react-docs/pages/components/tooltip/positioning-follow-cursor.js @@ -0,0 +1,10 @@ +import { Text, Tooltip } from '@tonic-ui/react'; +import React from 'react'; + +const App = () => ( + + Hover Me + +); + +export default App; diff --git a/packages/react-docs/pages/components/tooltip/positioning-next-cursor.js b/packages/react-docs/pages/components/tooltip/positioning-next-cursor.js new file mode 100644 index 0000000000..84ded72a48 --- /dev/null +++ b/packages/react-docs/pages/components/tooltip/positioning-next-cursor.js @@ -0,0 +1,10 @@ +import { Text, Tooltip } from '@tonic-ui/react'; +import React from 'react'; + +const App = () => ( + + Hover Me + +); + +export default App; diff --git a/packages/react-docs/pages/components/tooltip/positioning-offset.js b/packages/react-docs/pages/components/tooltip/positioning-offset.js new file mode 100644 index 0000000000..191299a7cc --- /dev/null +++ b/packages/react-docs/pages/components/tooltip/positioning-offset.js @@ -0,0 +1,49 @@ +import { Box, Flex, Text, TextLabel, Tooltip } from '@tonic-ui/react'; +import React, { useState } from 'react'; + +const App = () => { + const [skidding, setSkidding] = useState(0); + const [distance, setDistance] = useState(8); + + return ( + <> + + + skidding + + + setSkidding(Number(e.target.value))} + /> + {skidding} + + + + + distance + + + setDistance(Number(e.target.value))} + /> + {distance} + + + + Text content + + + ); +}; + +export default App; diff --git a/packages/react-docs/pages/components/tooltip/tooltip-around-disabled-button.js b/packages/react-docs/pages/components/tooltip/tooltip-around-disabled-button.js new file mode 100644 index 0000000000..7c2aaa5244 --- /dev/null +++ b/packages/react-docs/pages/components/tooltip/tooltip-around-disabled-button.js @@ -0,0 +1,10 @@ +import { Button, Tooltip } from '@tonic-ui/react'; +import React from 'react'; + +const App = () => ( + + + +); + +export default App; diff --git a/packages/react-docs/pages/components/tooltip/tooltip-around-wrapped-disabled-button.js b/packages/react-docs/pages/components/tooltip/tooltip-around-wrapped-disabled-button.js new file mode 100644 index 0000000000..47b3dfc80e --- /dev/null +++ b/packages/react-docs/pages/components/tooltip/tooltip-around-wrapped-disabled-button.js @@ -0,0 +1,12 @@ +import { Button, Flex, Tooltip } from '@tonic-ui/react'; +import React from 'react'; + +const App = () => ( + + + + + +); + +export default App; diff --git a/packages/react-docs/pages/components/tooltip/tooltip-arrow-disabled.js b/packages/react-docs/pages/components/tooltip/tooltip-arrow-disabled.js new file mode 100644 index 0000000000..e674f1b60f --- /dev/null +++ b/packages/react-docs/pages/components/tooltip/tooltip-arrow-disabled.js @@ -0,0 +1,10 @@ +import { Icon, Tooltip } from '@tonic-ui/react'; +import React from 'react'; + +const App = () => ( + + + +); + +export default App; diff --git a/packages/react-docs/pages/components/tooltip/tooltip-disabled.js b/packages/react-docs/pages/components/tooltip/tooltip-disabled.js new file mode 100644 index 0000000000..5324fabfdd --- /dev/null +++ b/packages/react-docs/pages/components/tooltip/tooltip-disabled.js @@ -0,0 +1,16 @@ +import { Divider, Flex, Icon, Tooltip } from '@tonic-ui/react'; +import React from 'react'; + +const App = () => ( + + + + + + + + + +); + +export default App; diff --git a/packages/react-docs/pages/components/tooltip/uncontrolled.js b/packages/react-docs/pages/components/tooltip/uncontrolled.js new file mode 100644 index 0000000000..8a248fc1a8 --- /dev/null +++ b/packages/react-docs/pages/components/tooltip/uncontrolled.js @@ -0,0 +1,13 @@ +import { Text, Tooltip } from '@tonic-ui/react'; +import React from 'react'; + +const App = () => ( + + Text content + +); + +export default App; diff --git a/packages/react-docs/pages/components/tooltip/unfocusable-content.js b/packages/react-docs/pages/components/tooltip/unfocusable-content.js new file mode 100644 index 0000000000..5b6af94ffc --- /dev/null +++ b/packages/react-docs/pages/components/tooltip/unfocusable-content.js @@ -0,0 +1,10 @@ +import { Text, Tooltip } from '@tonic-ui/react'; +import React from 'react'; + +const App = () => ( + + Text content + +); + +export default App; From a6ef610940622ff3eb3166922a48daa8110915aa Mon Sep 17 00:00:00 2001 From: cheton Date: Wed, 15 Nov 2023 17:26:09 +0800 Subject: [PATCH 6/6] docs: update popover examples --- .../pages/components/popover/controlled.js | 30 ++ .../components/popover/faq-flip-modifier.js | 83 ++++ .../components/popover/faq-use-portal.js | 19 + .../focus-control-initial-focus-ref.js | 41 ++ .../focus-control-return-focus-on-close.js | 47 +++ .../popover/function-as-child-component.js | 41 ++ .../index.page.mdx} | 398 ++---------------- .../pages/components/popover/placement.js | 35 ++ .../popover/popover-around-disabled-button.js | 17 + .../popover-around-wrapped-disabled-button.js | 17 + .../popover/popover-arrow-disabled.js | 15 + .../components/popover/popover-disabled.js | 25 ++ .../popover/popover-trigger-hover.js | 15 + .../popover/positioning-follow-cursor.js | 15 + .../popover/positioning-next-cursor.js | 15 + .../components/popover/positioning-offset.js | 54 +++ .../pages/components/popover/uncontrolled.js | 15 + .../pages/components/tooltip/controlled.js | 2 +- 18 files changed, 509 insertions(+), 375 deletions(-) create mode 100644 packages/react-docs/pages/components/popover/controlled.js create mode 100644 packages/react-docs/pages/components/popover/faq-flip-modifier.js create mode 100644 packages/react-docs/pages/components/popover/faq-use-portal.js create mode 100644 packages/react-docs/pages/components/popover/focus-control-initial-focus-ref.js create mode 100644 packages/react-docs/pages/components/popover/focus-control-return-focus-on-close.js create mode 100644 packages/react-docs/pages/components/popover/function-as-child-component.js rename packages/react-docs/pages/components/{popover.page.mdx => popover/index.page.mdx} (57%) create mode 100644 packages/react-docs/pages/components/popover/placement.js create mode 100644 packages/react-docs/pages/components/popover/popover-around-disabled-button.js create mode 100644 packages/react-docs/pages/components/popover/popover-around-wrapped-disabled-button.js create mode 100644 packages/react-docs/pages/components/popover/popover-arrow-disabled.js create mode 100644 packages/react-docs/pages/components/popover/popover-disabled.js create mode 100644 packages/react-docs/pages/components/popover/popover-trigger-hover.js create mode 100644 packages/react-docs/pages/components/popover/positioning-follow-cursor.js create mode 100644 packages/react-docs/pages/components/popover/positioning-next-cursor.js create mode 100644 packages/react-docs/pages/components/popover/positioning-offset.js create mode 100644 packages/react-docs/pages/components/popover/uncontrolled.js diff --git a/packages/react-docs/pages/components/popover/controlled.js b/packages/react-docs/pages/components/popover/controlled.js new file mode 100644 index 0000000000..827230b41d --- /dev/null +++ b/packages/react-docs/pages/components/popover/controlled.js @@ -0,0 +1,30 @@ +import { Button, Flex, Popover, PopoverContent, PopoverTrigger, Switch, Text } from '@tonic-ui/react'; +import { useToggle } from '@tonic-ui/react-hooks'; +import React from 'react'; + +const App = () => { + const [on, toggle] = useToggle(false); + + return ( + <> + + + + + + + + + This is a controlled popover + + + + ); +}; + +export default App; diff --git a/packages/react-docs/pages/components/popover/faq-flip-modifier.js b/packages/react-docs/pages/components/popover/faq-flip-modifier.js new file mode 100644 index 0000000000..5b2166dfd7 --- /dev/null +++ b/packages/react-docs/pages/components/popover/faq-flip-modifier.js @@ -0,0 +1,83 @@ +import { + Box, + Checkbox, + Divider, + Flex, + Popover, + PopoverContent, + PopoverTrigger, + Scrollbar, + Space, + Text, + TextLabel, + useColorMode, + useColorStyle, +} from '@tonic-ui/react'; +import { useToggle } from '@tonic-ui/react-hooks'; +import React from 'react'; + +const FormGroup = (props) => ( + +); + +const App = () => { + const [colorMode] = useColorMode(); + const [colorStyle] = useColorStyle({ colorMode }); + const [isFlipModifierEnabled, toggleIsFlipModifierEnabled] = useToggle(true); + + return ( + <> + + + Modifiers + + + + + toggleIsFlipModifierEnabled()} + /> + + Enable flip modifier + + + + + + + + + Reference + + + + Popover + + + + + + ); +}; + +export default App; diff --git a/packages/react-docs/pages/components/popover/faq-use-portal.js b/packages/react-docs/pages/components/popover/faq-use-portal.js new file mode 100644 index 0000000000..5f904698b3 --- /dev/null +++ b/packages/react-docs/pages/components/popover/faq-use-portal.js @@ -0,0 +1,19 @@ +import { Button, Popover, PopoverContent, PopoverTrigger } from '@tonic-ui/react'; +import React from 'react'; + +const App = () => ( + + + + + + Popover + + +); + +export default App; diff --git a/packages/react-docs/pages/components/popover/focus-control-initial-focus-ref.js b/packages/react-docs/pages/components/popover/focus-control-initial-focus-ref.js new file mode 100644 index 0000000000..a7bf75f096 --- /dev/null +++ b/packages/react-docs/pages/components/popover/focus-control-initial-focus-ref.js @@ -0,0 +1,41 @@ +import { Button, Input, Popover, PopoverBody, PopoverContent, PopoverTrigger, Stack, Text } from '@tonic-ui/react'; +import React, { useRef } from 'react'; + +const App = () => { + const initialFocusRef1 = useRef(); + const initialFocusRef2 = useRef(); + + return ( + + + + + + + + + + + + + + + Non-interactive Trigger + + + + + + + + + + ); +}; + +export default App; diff --git a/packages/react-docs/pages/components/popover/focus-control-return-focus-on-close.js b/packages/react-docs/pages/components/popover/focus-control-return-focus-on-close.js new file mode 100644 index 0000000000..9adf55e1f0 --- /dev/null +++ b/packages/react-docs/pages/components/popover/focus-control-return-focus-on-close.js @@ -0,0 +1,47 @@ +import { Button, Input, Popover, PopoverBody, PopoverContent, PopoverTrigger, Stack, Text } from '@tonic-ui/react'; +import React, { useRef } from 'react'; + +const App = () => { + const initialFocusRef1 = useRef(); + const initialFocusRef2 = useRef(); + + return ( + + + + + + + + + + + + + + + Non-interactive Trigger + + + + + + + + + + ); +}; + +export default App; diff --git a/packages/react-docs/pages/components/popover/function-as-child-component.js b/packages/react-docs/pages/components/popover/function-as-child-component.js new file mode 100644 index 0000000000..4c70e7ae9f --- /dev/null +++ b/packages/react-docs/pages/components/popover/function-as-child-component.js @@ -0,0 +1,41 @@ +import { Button, Flex, Grid, Link, Popover, PopoverBody, PopoverContent, PopoverFooter, PopoverHeader, PopoverTrigger } from '@tonic-ui/react'; +import React from 'react'; + +const App = () => ( + + {({ isOpen, onClose }) => ( + <> + + + + + + Popover Header + + + Popover Body + + + + Learn more + + + + + + + + + )} + +); + +export default App; diff --git a/packages/react-docs/pages/components/popover.page.mdx b/packages/react-docs/pages/components/popover/index.page.mdx similarity index 57% rename from packages/react-docs/pages/components/popover.page.mdx rename to packages/react-docs/pages/components/popover/index.page.mdx index 2a73e3337b..d088d76ca2 100644 --- a/packages/react-docs/pages/components/popover.page.mdx +++ b/packages/react-docs/pages/components/popover/index.page.mdx @@ -79,45 +79,11 @@ If you need to pass more than one child element or non-element children, wrap th Pass `isOpen` to the `Popover` component to control the state of the popover. -```jsx -function Example() { - const [on, toggle] = useToggle(false); - - return ( - <> - - - - - - - - - This is a controlled popover - - - - ); -} -``` +{render('./controlled')} Popover is uncontrolled by default. You can set `defaultIsOpen` to `true` to have the popover open for the first render. -```jsx - - - - - - This is an uncontrolled popover - - -``` +{render('./uncontrolled')} ### Focus control @@ -125,133 +91,35 @@ Popover is uncontrolled by default. You can set `defaultIsOpen` to `true` to hav The focus status will send to `PopoverContent` when `Popover` opens. You can send the focus status to a specific element when it opens by passing the `initialFocusRef` prop. -```jsx -function Example() { - const initialFocusRef1 = React.useRef(); - const initialFocusRef2 = React.useRef(); - - return ( - - - - - - - - - - - - - - - Non-interactive Trigger - - - - - - - - - - ); -} -``` +{render('./focus-control-initial-focus-ref')} #### Focus an element when popover closes When the `Popover` is closed, the focus status is sent to the `PopoverTrigger`. If you need to prevent the `Popover` from returning the focus status to `PopoverTrigger`, you can set the `returnFocusOnClose` prop to `false`. -```jsx -function Example() { - const initialFocusRef1 = React.useRef(); - const initialFocusRef2 = React.useRef(); - - return ( - - - - - - - - - - - - - - - Non-interactive Trigger - - - - - - - - - - ); -} -``` +{render('./focus-control-return-focus-on-close')} ### Hide the popover To hide a popover in your app, you can pass an empty value to the children of the `PopoverContent` component or use the `disabled` prop. Here are examples: -```jsx - - - - - - - - - - - - - - - Popover - - - -``` +{render('./popover-disabled')} ### Hide the arrow of the popover To remove the arrow from a popover, you can pass `arrow={false}` to the `Popover` component. -```jsx - - - - - - Popover - - -``` +{render('./popover-arrow-disabled')} + +### Popover around disabled button + +By default the `Popover` is not shown when it is around a disabled `Button`. + +{render('./popover-around-disabled-button')} + +To show the `Popover` on a disabled `Button`, pass the `shouldWrapChildren` prop. + +{render('./popover-around-wrapped-disabled-button')} ### Activating a popover on hover @@ -259,16 +127,7 @@ To activate a popover on hover, set the `trigger` prop to `"hover"`. This will d Once the popover is displayed, the cursor can be moved freely between the `PopoverTrigger` and `PopoverContent`. The popover will remain open until the cursor is moved away from the `PopoverContent`. -```jsx - - - Text content - - - Popover - - -``` +{render('./popover-trigger-hover')} ### Adjust popover positioning @@ -284,57 +143,7 @@ The `skidding` number determines the displacement of the `PopoverContent` from t The `distance` number controls the distance between the `PopoverContent` and the `PopoverTrigger` and is measured in pixels. -```jsx noInline -render(() => { - const [skidding, setSkidding] = React.useState(0); - const [distance, setDistance] = React.useState(12); - - return ( - <> - - - skidding - - - setSkidding(Number(e.target.value))} - /> - {skidding} - - - - - distance - - - setDistance(Number(e.target.value))} - /> - {distance} - - - - - Hover Me - - - Popover - - - - ); -}); -``` +{render('./positioning-offset')} #### Using the `nextToCursor` prop @@ -342,16 +151,7 @@ The `nextToCursor` prop positions the popover next to the cursor. Note that this prop only works when the `trigger` prop is set to `"hover"`. -```jsx - - - Hover Me - - - Popover - - -``` +{render('./positioning-next-cursor')} #### Using the `followCursor` prop @@ -359,57 +159,13 @@ The `followCursor` prop makes the popover follow the cursor as it moves. Note that this prop only works when the `trigger` prop is set to `"hover"`. -```jsx - - - Hover Me - - - Popover - - -``` +{render('./positioning-follow-cursor')} ### Context API To access the internal state of the popover, you can use the Function as Child Component (FaCC) pattern or use the `usePopover` hook. -```jsx - - {({ isOpen, onClose }) => ( - <> - - - - - - Popover Header - - - Popover Body - - - - Learn more - - - - - - - - - )} - -``` +{render('./function-as-child-component')} ### Placement @@ -426,36 +182,7 @@ Use the `placement` prop to control the placement of the popover. ``` -```jsx - - - {['top-start', 'top', 'top-end', - 'right-start', 'right', 'right-end', - 'bottom-start', 'bottom', 'bottom-end', - 'left-start', 'left', 'left-end' - ].map(placement => ( - - - - - - Popover - - - ))} - - -``` +{render('./placement')} ## Commonly Asked Questions @@ -465,20 +192,7 @@ By default, the `Popover` component positions the popover relative to its parent To address this issue, you can pass `PopperProps={{ usePortal: true }}` to `PopoverContent` to make it positioned on the document root. -```jsx - - - - - - Popover - - -``` +{render('./faq-use-portal')} ### Automatically adjusting popover placement with the `flip` modifier @@ -486,71 +200,7 @@ The `flip` modifier is a useful feature that allows for automatic adjustment of In the following example, the popover's placement is initially set to `top`. However, if the placement is not suitable due to space constraints, the opposite `bottom` placement will be used instead. -```jsx noInline -const FormGroup = (props) => ( - -); - -render(() => { - const [colorMode] = useColorMode(); - const [colorStyle] = useColorStyle({ colorMode }); - const [isFlipModifierEnabled, toggleIsFlipModifierEnabled] = useToggle(true); - - return ( - <> - - - Modifiers - - - - - toggleIsFlipModifierEnabled()} - /> - - Enable flip modifier - - - - - - - - - Reference - - - - Popover - - - - - - ); -}); -``` +{render('./faq-flip-modifier')} ## Accessibility diff --git a/packages/react-docs/pages/components/popover/placement.js b/packages/react-docs/pages/components/popover/placement.js new file mode 100644 index 0000000000..bb673fe7ea --- /dev/null +++ b/packages/react-docs/pages/components/popover/placement.js @@ -0,0 +1,35 @@ +import { Box, Button, Grid, Popover, PopoverContent, PopoverTrigger } from '@tonic-ui/react'; +import React from 'react'; + +const App = () => ( + + + {['top-start', 'top', 'top-end', + 'right-start', 'right', 'right-end', + 'bottom-start', 'bottom', 'bottom-end', + 'left-start', 'left', 'left-end' + ].map(placement => ( + + + + + + Popover + + + ))} + + +); + +export default App; diff --git a/packages/react-docs/pages/components/popover/popover-around-disabled-button.js b/packages/react-docs/pages/components/popover/popover-around-disabled-button.js new file mode 100644 index 0000000000..05a05a946e --- /dev/null +++ b/packages/react-docs/pages/components/popover/popover-around-disabled-button.js @@ -0,0 +1,17 @@ +import { Button, Flex, Popover, PopoverContent, PopoverTrigger } from '@tonic-ui/react'; +import React from 'react'; + +const App = () => ( + + + + + + + Popover + + + +); + +export default App; diff --git a/packages/react-docs/pages/components/popover/popover-around-wrapped-disabled-button.js b/packages/react-docs/pages/components/popover/popover-around-wrapped-disabled-button.js new file mode 100644 index 0000000000..a177385d13 --- /dev/null +++ b/packages/react-docs/pages/components/popover/popover-around-wrapped-disabled-button.js @@ -0,0 +1,17 @@ +import { Button, Flex, Popover, PopoverContent, PopoverTrigger } from '@tonic-ui/react'; +import React from 'react'; + +const App = () => ( + + + + + + + Popover + + + +); + +export default App; diff --git a/packages/react-docs/pages/components/popover/popover-arrow-disabled.js b/packages/react-docs/pages/components/popover/popover-arrow-disabled.js new file mode 100644 index 0000000000..59f14de36a --- /dev/null +++ b/packages/react-docs/pages/components/popover/popover-arrow-disabled.js @@ -0,0 +1,15 @@ +import { Button, Popover, PopoverContent, PopoverTrigger } from '@tonic-ui/react'; +import React from 'react'; + +const App = () => ( + + + + + + Popover + + +); + +export default App; diff --git a/packages/react-docs/pages/components/popover/popover-disabled.js b/packages/react-docs/pages/components/popover/popover-disabled.js new file mode 100644 index 0000000000..795ea9219f --- /dev/null +++ b/packages/react-docs/pages/components/popover/popover-disabled.js @@ -0,0 +1,25 @@ +import { Button, Divider, Flex, Popover, PopoverContent, PopoverTrigger } from '@tonic-ui/react'; +import React from 'react'; + +const App = () => ( + + + + + + + + + + + + + + + Popover + + + +); + +export default App; diff --git a/packages/react-docs/pages/components/popover/popover-trigger-hover.js b/packages/react-docs/pages/components/popover/popover-trigger-hover.js new file mode 100644 index 0000000000..77f2328d53 --- /dev/null +++ b/packages/react-docs/pages/components/popover/popover-trigger-hover.js @@ -0,0 +1,15 @@ +import { Popover, PopoverContent, PopoverTrigger, Text } from '@tonic-ui/react'; +import React from 'react'; + +const App = () => ( + + + Text content + + + Popover + + +); + +export default App; diff --git a/packages/react-docs/pages/components/popover/positioning-follow-cursor.js b/packages/react-docs/pages/components/popover/positioning-follow-cursor.js new file mode 100644 index 0000000000..c834d8df43 --- /dev/null +++ b/packages/react-docs/pages/components/popover/positioning-follow-cursor.js @@ -0,0 +1,15 @@ +import { Popover, PopoverContent, PopoverTrigger, Text } from '@tonic-ui/react'; +import React from 'react'; + +const App = () => ( + + + Hover Me + + + Popover + + +); + +export default App; diff --git a/packages/react-docs/pages/components/popover/positioning-next-cursor.js b/packages/react-docs/pages/components/popover/positioning-next-cursor.js new file mode 100644 index 0000000000..d656de8684 --- /dev/null +++ b/packages/react-docs/pages/components/popover/positioning-next-cursor.js @@ -0,0 +1,15 @@ +import { Popover, PopoverContent, PopoverTrigger, Text } from '@tonic-ui/react'; +import React from 'react'; + +const App = () => ( + + + Hover Me + + + Popover + + +); + +export default App; diff --git a/packages/react-docs/pages/components/popover/positioning-offset.js b/packages/react-docs/pages/components/popover/positioning-offset.js new file mode 100644 index 0000000000..ade0608ac7 --- /dev/null +++ b/packages/react-docs/pages/components/popover/positioning-offset.js @@ -0,0 +1,54 @@ +import { Box, Flex, Popover, PopoverContent, PopoverTrigger, Text, TextLabel } from '@tonic-ui/react'; +import React, { useState } from 'react'; + +const App = () => { + const [skidding, setSkidding] = useState(0); + const [distance, setDistance] = useState(12); + + return ( + <> + + + skidding + + + setSkidding(Number(e.target.value))} + /> + {skidding} + + + + + distance + + + setDistance(Number(e.target.value))} + /> + {distance} + + + + + Hover Me + + + Popover + + + + ); +}; + +export default App; diff --git a/packages/react-docs/pages/components/popover/uncontrolled.js b/packages/react-docs/pages/components/popover/uncontrolled.js new file mode 100644 index 0000000000..e987077e56 --- /dev/null +++ b/packages/react-docs/pages/components/popover/uncontrolled.js @@ -0,0 +1,15 @@ +import { Button, Popover, PopoverContent, PopoverTrigger, Text } from '@tonic-ui/react'; +import React from 'react'; + +const App = () => ( + + + + + + This is an uncontrolled popover + + +); + +export default App; diff --git a/packages/react-docs/pages/components/tooltip/controlled.js b/packages/react-docs/pages/components/tooltip/controlled.js index e42b2e7ceb..82a6fefbf1 100644 --- a/packages/react-docs/pages/components/tooltip/controlled.js +++ b/packages/react-docs/pages/components/tooltip/controlled.js @@ -1,5 +1,5 @@ -import { useToggle } from '@tonic-ui/react-hooks'; import { Flex, Switch, Text, Tooltip } from '@tonic-ui/react'; +import { useToggle } from '@tonic-ui/react-hooks'; import React from 'react'; const App = () => {