Skip to content
This repository was archived by the owner on Mar 4, 2020. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Add `color` prop to `Segment` typings @layershifter ([#1702](https://github.com/stardust-ui/react/pull/1702))
- Remove `color` prop from `Dialog` typings @layershifter ([#1702](https://github.com/stardust-ui/react/pull/1702))
- `Loader` uses `Text` component for `label` slot instead of `Box` @layershifter ([#1701](https://github.com/stardust-ui/react/pull/1701))
- Fix nesting issues with `Dialog` component inside `Popup` @layershifter ([#1706](https://github.com/stardust-ui/react/pull/1706))

### Documentation
- Make sidebar categories collapsible @lucivpav ([#1611](https://github.com/stardust-ui/react/pull/1611))
Expand Down
9 changes: 9 additions & 0 deletions docs/src/components/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,15 @@ class Sidebar extends React.Component<any, any> {
title: { content: 'Popups', as: NavLink, to: '/prototype-popups' },
public: false,
},
{
key: 'nested-popups-and-dialogs',
title: {
content: 'Nested Popups & Dialogs',
as: NavLink,
to: '/prototype-nested-popups-and-dialogs',
},
public: true,
},
{
key: 'iconviewer',
title: { content: 'Processed Icons', as: NavLink, to: '/icon-viewer' },
Expand Down
123 changes: 123 additions & 0 deletions docs/src/prototypes/NestedPopupsAndDialogs/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { Button, Dialog, Popup } from '@stardust-ui/react'
import * as React from 'react'

import { ComponentPrototype, PrototypeSection } from 'docs/src/prototypes/Prototypes'

const PopupAndDialog: React.FC = () => (
<Popup
content={
<>
<p>
This <code>Popup</code> will be kept open after <code>Dialog</code> will be opened.
</p>
<Dialog
cancelButton="Close"
header="A dialog"
trigger={<Button content="Open a dialog" />}
/>
</>
}
trigger={<Button content="Open a popup" />}
/>
)

const ControlledPopupAndDialog: React.FC = () => {
const [dialogOpen, setDialogOpen] = React.useState(false)
const [popupOpen, setPopupOpen] = React.useState(false)

return (
<>
<Popup
content={
<>
<p>
This <code>Popup</code> will be close after <code>Dialog</code> will be opened.
</p>
<Button
content="Open a dialog & close popup"
onClick={() => {
setPopupOpen(false)
setDialogOpen(true)
}}
/>
</>
}
onOpenChange={(e, data) => setPopupOpen(data.open)}
open={popupOpen}
trigger={<Button content="Open a popup" />}
/>
<Dialog
cancelButton="Close"
header="A dialog"
onCancel={() => setDialogOpen(false)}
open={dialogOpen}
/>
</>
)
}

const NestedDialogs: React.FC = () => (
<Dialog
cancelButton="Close"
header="An outer dialog"
content={
<>
<p>
This <code>Dialog</code> contains another <code>Dialog</code> inside.
</p>
<blockquote>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur
sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id
est laborum.
</blockquote>

<Dialog
cancelButton="Close"
header="An inner dialog"
content={
<>
<p>
This <code>Dialog</code> is nested ヽ(^o^)ノ, if you will on an overlay only this{' '}
<code>Dialog</code> will be closed.
</p>

<Popup
content="You can have also Popups inside dialogs!"
trigger={<Button content="Open a popup" />}
/>
</>
}
trigger={<Button content="Open a dialog" />}
/>
</>
}
trigger={<Button content="Open a dialog" />}
/>
)

const NestedPopupsAndDialogs: React.FC = () => {
return (
<PrototypeSection title="Nested Popups & Dialogs">
<ComponentPrototype
title="A popup with dialog"
description="Popup will be kept open after Dialog"
>
<PopupAndDialog />
</ComponentPrototype>
<ComponentPrototype
title="A closable popup with dialog"
description="Popup will be closed once Dialog will be opened"
>
<ControlledPopupAndDialog />
</ComponentPrototype>
<ComponentPrototype title="Nested dialogs" description="An example with nested dialogs">
<NestedDialogs />
</ComponentPrototype>
</PrototypeSection>
)
}

export default NestedPopupsAndDialogs
6 changes: 6 additions & 0 deletions docs/src/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import PopupsPrototype from './prototypes/popups'
import IconViewerPrototype from './prototypes/IconViewer'
import MenuButtonPrototype from './prototypes/MenuButton'
import AlertsPrototype from './prototypes/alerts'
import NestedPopupsAndDialogsPrototype from './prototypes/NestedPopupsAndDialogs'

const Routes = () => (
<BrowserRouter basename={__BASENAME__}>
Expand All @@ -61,6 +62,11 @@ const Routes = () => (
<Route exact path="/icon-viewer" component={IconViewerPrototype} />
<Route exact path="/menu-button" component={MenuButtonPrototype} />
<Route exact path="/prototype-alerts" component={AlertsPrototype} />
<Route
exact
path="/prototype-nested-popups-and-dialogs"
component={NestedPopupsAndDialogsPrototype}
/>
<Route exact path="/faq" component={FAQ} />
<Route exact path="/accessibility" component={Accessibility} />
<Route exact path="/accessibility-behaviors" component={AccessibilityBehaviors} />
Expand Down
7 changes: 7 additions & 0 deletions e2e/e2eApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ export class E2EApi {

public clickOn = async (selector: string) => await (await this.getElement(selector)).click()

public clickOnPosition = async (selector: string, x: number, y: number) => {
const elementHandle = await this.getElement(selector)
const boundingBox = await elementHandle.boundingBox()

await this.page.mouse.click(Math.round(boundingBox.x) + x, Math.round(boundingBox.y) + y)
}

public textOf = async (selector: string) => {
const element = await this.getElement(selector)
return await (await element.getProperty('textContent')).jsonValue()
Expand Down
33 changes: 33 additions & 0 deletions e2e/tests/dialogInDialog-example.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as React from 'react'
import { Button, Dialog } from '@stardust-ui/react'

export const selectors = {
outerClose: 'outer-close',
outerHeader: 'outer-header',
outerTrigger: 'outer-trigger',
outerOverlay: 'outer-overlay',

innerClose: 'inner-close',
innerHeader: 'inner-header',
innerTrigger: 'inner-trigger',
innerOverlay: 'inner-overlay',
}

const DialogInPopupExample = () => (
<Dialog
cancelButton={{ content: 'Close', id: selectors.outerClose }}
content={
<Dialog
cancelButton={{ content: 'Close', id: selectors.innerClose }}
header={{ content: 'An inner', id: selectors.innerHeader }}
overlay={{ id: selectors.innerOverlay }}
trigger={<Button id={selectors.innerTrigger} content="Open a dialog" />}
/>
}
header={{ content: 'An outer', id: selectors.outerHeader }}
overlay={{ id: selectors.outerOverlay }}
trigger={<Button id={selectors.outerTrigger} content="Open a dialog" />}
/>
)

export default DialogInPopupExample
62 changes: 62 additions & 0 deletions e2e/tests/dialogInDialog-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { selectors } from './dialogInDialog-example'

const outerClose = `#${selectors.outerClose}`
const outerHeader = `#${selectors.outerHeader}`
const outerOverlay = `#${selectors.outerOverlay}`
const outerTrigger = `#${selectors.outerTrigger}`
const innerClose = `#${selectors.innerClose}`
const innerHeader = `#${selectors.innerHeader}`
const innerTrigger = `#${selectors.innerTrigger}`
const innerOverlay = `#${selectors.innerOverlay}`

// https://github.com/stardust-ui/react/issues/1674
describe('Dialog in Dialog', () => {
beforeEach(async () => {
await e2e.gotoTestCase(__filename, outerTrigger)
})

it('An outer "Dialog" should be open after inner "Dialog" will be opened', async () => {
await e2e.clickOn(outerTrigger)
expect(await e2e.exists(outerHeader)).toBe(true)

await e2e.clickOn(innerTrigger)
expect(await e2e.exists(outerHeader)).toBe(true)
expect(await e2e.exists(innerHeader)).toBe(true)
})

it('A click inside inner "Dialog" should not close dialogs', async () => {
await e2e.clickOn(outerTrigger)
await e2e.clickOn(innerTrigger)
await e2e.clickOn(innerHeader)

expect(await e2e.exists(outerHeader)).toBe(true)
expect(await e2e.exists(innerHeader)).toBe(true)
})

it('A click on overlay should close only the last opened "Dialog"', async () => {
await e2e.clickOn(outerTrigger)
await e2e.clickOn(innerTrigger)

await e2e.clickOnPosition(innerOverlay, 0, 0)

expect(await e2e.exists(outerHeader)).toBe(true)
expect(await e2e.exists(innerHeader)).toBe(false)

await e2e.clickOnPosition(outerOverlay, 0, 0)
expect(await e2e.exists(outerHeader)).toBe(false)
expect(await e2e.exists(innerHeader)).toBe(false)
})

it('A click on cancel button should close only matching "Dialog"', async () => {
await e2e.clickOn(outerTrigger)
await e2e.clickOn(innerTrigger)

await e2e.clickOn(innerClose)
expect(await e2e.exists(outerHeader)).toBe(true)
expect(await e2e.exists(innerHeader)).toBe(false)

await e2e.clickOn(outerClose)
expect(await e2e.exists(outerHeader)).toBe(false)
expect(await e2e.exists(innerHeader)).toBe(false)
})
})
26 changes: 26 additions & 0 deletions e2e/tests/dialogInPopup-example.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as React from 'react'
import { Button, Dialog, Popup } from '@stardust-ui/react'

export const selectors = {
dialogCancel: 'dialog-cancel',
dialogHeader: Dialog.slotClassNames.header,
dialogOverlay: Dialog.slotClassNames.overlay,
dialogTrigger: 'dialog-trigger',
popupContent: Popup.Content.className,
popupTrigger: 'popup-trigger',
}

const DialogInPopupExample = () => (
<Popup
content={
<Dialog
cancelButton={{ content: 'Close', id: selectors.dialogCancel }}
header="A dialog"
trigger={<Button id={selectors.dialogTrigger} content="Open a dialog" />}
/>
}
trigger={<Button id={selectors.popupTrigger} content="Open a popup" />}
/>
)

export default DialogInPopupExample
51 changes: 51 additions & 0 deletions e2e/tests/dialogInPopup-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { selectors } from './dialogInPopup-example'

const dialogCancel = `#${selectors.dialogCancel}`
const dialogHeader = `.${selectors.dialogHeader}`
const dialogOverlay = `.${selectors.dialogOverlay}`
const dialogTrigger = `#${selectors.dialogTrigger}`
const popupContent = `.${selectors.popupContent}`
const popupTrigger = `#${selectors.popupTrigger}`

// https://github.com/stardust-ui/react/issues/1674
describe('Dialog in Popup', () => {
beforeEach(async () => {
await e2e.gotoTestCase(__filename, popupTrigger)
})

it('"Popup" should be open after "Dialog" will be opened', async () => {
await e2e.clickOn(popupTrigger)
expect(await e2e.exists(popupContent)).toBe(true)

await e2e.clickOn(dialogTrigger)
expect(await e2e.exists(popupContent)).toBe(true)
expect(await e2e.exists(dialogHeader)).toBe(true)
})

it('"Popup" should be open after "Dialog" will be closed', async () => {
await e2e.clickOn(popupTrigger)
await e2e.clickOn(dialogTrigger)
await e2e.clickOn(dialogCancel)

expect(await e2e.exists(popupContent)).toBe(true)
expect(await e2e.exists(dialogHeader)).toBe(false)
})

it('"Popup" and "Dialog" will be kept open on a click inside "Dialog"', async () => {
await e2e.clickOn(popupTrigger)
await e2e.clickOn(dialogTrigger)
await e2e.clickOn(dialogHeader)

expect(await e2e.exists(popupContent)).toBe(true)
expect(await e2e.exists(dialogHeader)).toBe(true)
})

it('"Popup" will be kept open on a click inside "Dialog" overlay', async () => {
await e2e.clickOn(popupTrigger)
await e2e.clickOn(dialogTrigger)
await e2e.clickOnPosition(dialogOverlay, 0, 0)

expect(await e2e.exists(popupContent)).toBe(true)
expect(await e2e.exists(dialogHeader)).toBe(false)
})
})
7 changes: 7 additions & 0 deletions e2e/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "../build/tsconfig.common.json",
"compilerOptions": {
"module": "esnext"
},
"include": ["../packages/react/src", "../e2e", "../types"]
}
Copy link
Member Author

Choose a reason for hiding this comment

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

Add tsconfig.json to get proper autocomplete

2 changes: 1 addition & 1 deletion packages/react-component-nesting-registry/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react'

export type GetContextRefs = (needleRef: NodeRef) => NodeRef[]
export type GetRefs = () => NodeRef[]
export type NodeRef<T extends Node = Node> = React.RefObject<T>
export type NodeRef<T extends Node = Node> = React.MutableRefObject<T>
Copy link
Member Author

Choose a reason for hiding this comment

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

There refs are mutable, it allows to avoid usage of handleRef or @tsignore


export type NestingContextValue = {
getContextRefs: GetContextRefs
Expand Down
Loading