From e538a3ace939a02122fd37b066e2e6b33a63128a Mon Sep 17 00:00:00 2001 From: Tobias Date: Thu, 25 Aug 2022 11:49:09 +0200 Subject: [PATCH] feat(Tooltip): add skip_portal property --- .../uilib/components/tooltip/properties.md | 29 ++- .../__snapshots__/Dialog.test.tsx.snap | 3 + .../__snapshots__/Drawer.test.tsx.snap | 3 + .../__snapshots__/Modal.test.tsx.snap | 3 + .../src/components/tooltip/Tooltip.js | 18 +- .../components/tooltip/TooltipContainer.js | 6 +- .../tooltip/__tests__/Tooltip.test.js | 211 ++++++++------- .../__snapshots__/Tooltip.test.js.snap | 241 ++++++++++-------- 8 files changed, 296 insertions(+), 218 deletions(-) diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/tooltip/properties.md b/packages/dnb-design-system-portal/src/docs/uilib/components/tooltip/properties.md index 819e9c14305..ea00333918a 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/components/tooltip/properties.md +++ b/packages/dnb-design-system-portal/src/docs/uilib/components/tooltip/properties.md @@ -4,20 +4,21 @@ showTabs: true ## Tooltip properties -| Properties | Description | -| ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `children` | _(optional)_ Provide a string or a React Element to be shown as the tooltip content. | -| `active` | _(optional)_ set to `true` the tooltip will show up. | +| Properties | Description | +| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| `children` | _(optional)_ Provide a string or a React Element to be shown as the tooltip content. | +| `active` | _(optional)_ set to `true` the tooltip will show up. | | `position` | _(optional)_ defines the offset position to the target element the arrow appears. Can be `top`, `right`, `left` and `bottom`. Defaults to `top`. | | `position` | _(optional)_ defines the offset position to the target element the arrow appears. Can be `top`, `right`, `left` and `bottom`. Defaults to `top`. | | `align` | _(optional)_ defines the offset alignment to the target element the arrow appears. Can be `center`, `right` and `left`.Defaults to `center`. | -| `arrow` | _(optional)_ defines the direction where the arrow appears. Can be `center`, `top`, `right`, `bottom` and `left`. Defaults to `center`. | -| `fixed_position` | _(optional)_ If set to `true`, the Tooltip will be fixed in its scroll position by using CSS `position: fixed;`. Defaults to `false`. | -| `no_animation` | _(optional)_ set to `true` if no fade-in animation should be used. | -| `show_delay` | _(optional)_ define the delay until the tooltip should show up after the initial hover / active state. | -| `hide_delay` | _(optional)_ define the delay until the tooltip should disappear up after initial visibility. | -| `target_element` | _(optional)_ provide an element directly as a React Node or a React Ref that will be wrapped and rendered. | -| `target_selector` | _(optional)_ specify a vanilla HTML selector by a string as the target element. | -| `size` | _(optional)_ defines the spacing size of the tooltip. Can be `large` or `basis`. Defaults to `basis`. | -| `group` | _(optional)_ if the tooltip should animate from one target to the next, define a unique ID. | -| [Space](/uilib/components/space/properties) | _(optional)_ spacing properties like `top` or `bottom` are supported. | +| `arrow` | _(optional)_ defines the direction where the arrow appears. Can be `center`, `top`, `right`, `bottom` and `left`. Defaults to `center`. | +| `fixed_position` | _(optional)_ If set to `true`, the Tooltip will be fixed in its scroll position by using CSS `position: fixed;`. Defaults to `false`. | +| `skip_portal` | _(optional)_ set to `true` to disable the React Portal behavior. Defaults to `false`. | +| `no_animation` | _(optional)_ set to `true` if no fade-in animation should be used. | +| `show_delay` | _(optional)_ define the delay until the tooltip should show up after the initial hover / active state. | +| `hide_delay` | _(optional)_ define the delay until the tooltip should disappear up after initial visibility. | +| `target_element` | _(optional)_ provide an element directly as a React Node or a React Ref that will be wrapped and rendered. | +| `target_selector` | _(optional)_ specify a vanilla HTML selector by a string as the target element. | +| `size` | _(optional)_ defines the spacing size of the tooltip. Can be `large` or `basis`. Defaults to `basis`. | +| `group` | _(optional)_ if the tooltip should animate from one target to the next, define a unique ID. | +| [Space](/uilib/components/space/properties) | _(optional)_ spacing properties like `top` or `bottom` are supported. | diff --git a/packages/dnb-eufemia/src/components/dialog/__tests__/__snapshots__/Dialog.test.tsx.snap b/packages/dnb-eufemia/src/components/dialog/__tests__/__snapshots__/Dialog.test.tsx.snap index b14fd3871c0..2f063c5ebd3 100644 --- a/packages/dnb-eufemia/src/components/dialog/__tests__/__snapshots__/Dialog.test.tsx.snap +++ b/packages/dnb-eufemia/src/components/dialog/__tests__/__snapshots__/Dialog.test.tsx.snap @@ -439,6 +439,7 @@ exports[`Dialog component snapshot should match component snapshot 1`] = ` position="top" show_delay={300} size="basis" + skip_portal={null} target_element={ Object { "current": } - > - Text - - ) - it('have to match default tooltip snapshot', () => { - const Comp = mount() - expect(toJson(Comp)).toMatchSnapshot() - }) - - it('should validate with ARIA rules as a tooltip', async () => { - const Comp = mount() - expect(await axeComponent(Comp)).toHaveNoViolations() - }) +beforeEach(() => { + document.body.innerHTML = '' }) -describe('Tooltip component with target', () => { - const Component = (props = {}) => ( - <> - - - Text - - - ) - - it('has to be in the DOM so aria-describedby is valid', () => { - const Comp = mount( - - text - - ) - - const id = Comp.find('a').instance().getAttribute('aria-describedby') - expect(document.body.querySelectorAll('#' + id).length).toBe(1) - }) - - it('has to be visible on hover', async () => { - const Comp = mount( - - text - +describe('Tooltip', () => { + describe('with target', () => { + const Component = (props = {}) => ( + <> + + + Text + + ) - // hover - Comp.find('a').instance().dispatchEvent(new MouseEvent('mouseenter')) - - await wait(100) - - const id = Comp.find('a').instance().getAttribute('aria-describedby') - expect( - document.body - .querySelector('#' + id) - .parentElement.classList.contains('dnb-tooltip--active') - ).toBe(true) - - // leave hover - Comp.find('a').instance().dispatchEvent(new MouseEvent('mouseleave')) - - await wait(600) - - expect( - document.body - .querySelector('#' + id) - .parentElement.classList.contains('dnb-tooltip--active') - ).toBe(false) + it('creates a React Portal', () => { + mount(, { + attachTo: attachToBody(), + }) + + expect( + document.body.querySelectorAll('.dnb-tooltip__portal') + ).toHaveLength(1) + expect(document.body.querySelectorAll('.dnb-tooltip')).toHaveLength( + 1 + ) + }) + + it('will skip React Portal when skip_portal is true', () => { + const Comp = mount(, { + attachTo: attachToBody(), + }) + + expect( + document.body.querySelectorAll('.dnb-tooltip__portal') + ).toHaveLength(0) + + expect(toJson(Comp.find('.dnb-tooltip'))).toMatchSnapshot() + }) + + it('should validate with ARIA rules as a tooltip', async () => { + const Comp = mount() + expect(await axeComponent(Comp)).toHaveNoViolations() + }) }) - it('has to be visible on focus event dispatch', async () => { - const Comp = mount( - - text - + describe('with target_element', () => { + const Component = (props = {}) => ( + Button} + > + Text + ) - document.documentElement.setAttribute('data-whatintent', 'keyboard') - const inst = Comp.find('a').instance() - inst.dispatchEvent(new Event('focus')) + it('have to match default tooltip snapshot', () => { + const Comp = mount() + expect(toJson(Comp)).toMatchSnapshot() + }) - await wait(400) // because of visibility delay - - const id = inst.getAttribute('aria-describedby') - expect( - document.body - .querySelector('#' + id) - .parentElement.classList.contains('dnb-tooltip--active') - ).toBe(true) + it('should validate with ARIA rules as a tooltip', async () => { + const Comp = mount() + expect(await axeComponent(Comp)).toHaveNoViolations() + }) }) - it('should validate with ARIA rules as a tooltip', async () => { - const Comp = mount() - expect(await axeComponent(Comp)).toHaveNoViolations() + describe('Anchor with tooltip', () => { + it('has to be in the DOM so aria-describedby is valid', () => { + const Comp = mount( + + text + + ) + + const id = Comp.find('a').instance().getAttribute('aria-describedby') + expect(document.body.querySelectorAll('#' + id).length).toBe(1) + }) + + it('has to be visible on hover', async () => { + const Comp = mount( + + text + + ) + + // hover + Comp.find('a').instance().dispatchEvent(new MouseEvent('mouseenter')) + + await wait(100) + + const id = Comp.find('a').instance().getAttribute('aria-describedby') + expect( + document.body + .querySelector('#' + id) + .parentElement.classList.contains('dnb-tooltip--active') + ).toBe(true) + + // leave hover + Comp.find('a').instance().dispatchEvent(new MouseEvent('mouseleave')) + + await wait(600) + + expect( + document.body + .querySelector('#' + id) + .parentElement.classList.contains('dnb-tooltip--active') + ).toBe(false) + }) + + it('has to be visible on focus event dispatch', async () => { + const Comp = mount( + + text + + ) + + document.documentElement.setAttribute('data-whatintent', 'keyboard') + const inst = Comp.find('a').instance() + inst.dispatchEvent(new Event('focus')) + + await wait(400) // because of visibility delay + + const id = inst.getAttribute('aria-describedby') + expect( + document.body + .querySelector('#' + id) + .parentElement.classList.contains('dnb-tooltip--active') + ).toBe(true) + }) }) }) @@ -130,6 +166,7 @@ describe('Tooltip scss', () => { const scss = loadScss(require.resolve('../style/dnb-tooltip.scss')) expect(scss).toMatchSnapshot() }) + it('have to match default theme snapshot', () => { const scss = loadScss( require.resolve('../style/themes/dnb-tooltip-theme-ui.scss') diff --git a/packages/dnb-eufemia/src/components/tooltip/__tests__/__snapshots__/Tooltip.test.js.snap b/packages/dnb-eufemia/src/components/tooltip/__tests__/__snapshots__/Tooltip.test.js.snap index 9bec5ef08ee..6e3924e69a2 100644 --- a/packages/dnb-eufemia/src/components/tooltip/__tests__/__snapshots__/Tooltip.test.js.snap +++ b/packages/dnb-eufemia/src/components/tooltip/__tests__/__snapshots__/Tooltip.test.js.snap @@ -1,112 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Tooltip component with target_element have to match default tooltip snapshot 1`] = ` - - - Button - - } - target_selector={null} - tooltip={null} - > - - Button - - } - target_element={ - - } - target_selector={null} - tooltip={null} - > - - - Button - - } - target_element={ - - } - target_selector={null} - tooltip={null} - /> - - - -`; - exports[`Tooltip scss have to match default theme snapshot 1`] = ` "/* * Tooltip theme @@ -288,3 +181,137 @@ exports[`Tooltip scss have to match snapshot 1`] = ` visibility: hidden; } } " `; + +exports[`Tooltip with target will skip React Portal when skip_portal is true 1`] = ` + + + + Text + + +`; + +exports[`Tooltip with target_element have to match default tooltip snapshot 1`] = ` + + + Button + + } + target_selector={null} + tooltip={null} + > + + Button + + } + target_element={ + + } + target_selector={null} + tooltip={null} + > + + + Button + + } + target_element={ + + } + target_selector={null} + tooltip={null} + /> + + + +`;