Skip to content

Commit 96b8d4b

Browse files
committed
feat(Portal): New component Portal
remove usePortal, related components use Portal refactoring (include Command Dialog Drawer Tooltip ...) re #1
1 parent 820b6a3 commit 96b8d4b

File tree

22 files changed

+190
-130
lines changed

22 files changed

+190
-130
lines changed

packages/fluent-ui-hooks/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"react-dom": ">= 16.8.0"
3939
},
4040
"dependencies": {
41+
"@fluent-ui/core": "^0.8.0",
4142
"json2mq": "^0.2.0",
4243
"popper.js": "^1.15.0"
4344
},

packages/fluent-ui-hooks/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
export * from './useAction'
22
export * from './useClickOutside'
3-
export * from './usePortal'
43
export * from './useReveal'
54
export * from './usePopper'
65
export * from './useHover'
76
export * from './useClick'
87
export * from './useTouch'
98
export * from './useFocus'
109
export * from './useMedia'
10+
export * from './useGlobal'

packages/fluent-ui-hooks/src/useClickOutside.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useEffect, RefObject } from 'react'
22

33
export function useClickOutside(
44
ref: RefObject<HTMLDivElement>,
5-
handler: (e?: MouseEvent | TouchEvent) => void
5+
handler: (e: MouseEvent | TouchEvent) => void
66
): void {
77
useEffect((): (() => void) => {
88
const listener = (event: MouseEvent | TouchEvent): void => {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function useGlobal(): Window | NodeJS.Global {
2+
return typeof window !== 'undefined' ? window : global
3+
}

packages/fluent-ui-hooks/src/useMedia/useMedia.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react'
22
import * as json2mq from 'json2mq'
3-
import { global } from '../utils'
3+
import { useGlobal } from '../useGlobal'
44

55
type AllBreakpoints = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
66
export type MediaQuery = json2mq.QueryObject | json2mq.QueryObject[] | AllBreakpoints | string
@@ -51,14 +51,15 @@ export function useMedia(
5151
)
5252
const transformedModel = React.useMemo((): TransformedModel => model(breakpoints), [breakpoints])
5353

54+
const global = useGlobal() as Window
5455
const query = isBreakpoint
5556
? transformedModel[mediaQuery as keyof Breakpoints]
5657
: typeof mediaQuery === 'string'
5758
? mediaQuery
5859
: json2mq(mediaQuery)
5960
const mql = global && global.matchMedia && global.matchMedia(query)
6061

61-
if (mql === false) return false
62+
if (!mql) return false
6263

6364
// eslint-disable-next-line
6465
const [state, setState] = React.useState((): boolean => mql.matches)

packages/fluent-ui-hooks/src/usePopper.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ import PopperJS from 'popper.js'
44
// eslint-disable-next-line
55
export interface usePropperOptions extends PopperJS.PopperOptions {}
66

7-
export function usePopper({
7+
export function usePopper<Reference, Popper>({
88
placement = 'bottom',
99
positionFixed = true,
1010
eventsEnabled = true,
1111
...otherOptions
12-
}: usePropperOptions): [React.MutableRefObject<null>, React.MutableRefObject<null>] {
12+
}: usePropperOptions): [React.RefObject<Reference>, React.RefObject<Popper>] {
1313
const popperInstance = React.useRef<PopperJS>(null)
14-
const referenceRef = React.useRef(null)
15-
const popperRef = React.useRef(null)
14+
const referenceRef = React.useRef<Reference>(null)
15+
const popperRef = React.useRef<Popper>(null)
1616

1717
React.useEffect((): (() => void) | void => {
1818
if (popperInstance.current !== null) {

packages/fluent-ui-hooks/src/usePortal.ts

Lines changed: 0 additions & 58 deletions
This file was deleted.

packages/fluent-ui-hooks/src/utils.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

packages/fluent-ui.com/src/docs/components/Navigation/api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import Navigation from '@fluent-ui/core/Navigation'
1212
import { Navigation } from '@fluent-ui/core'
1313
```
1414

15-
Use `Navigation.Header` `Navigation.Footer` inside Navigation so that f knows how to render to the target location.
15+
Use `Navigation.Header` `Navigation.Footer` inside Navigation so that we knows how to render to the target location.
1616

1717
### Props
1818

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
title: Portal
3+
components: Portal
4+
type: Utils
5+
---
6+
7+
# Portal
8+
9+
<p class="description">The portal component renders its children into a new "subtree" outside of current component hierarchy.
10+
</p>
11+
12+
## Simple Portal
13+
14+
```jsx
15+
() => {
16+
const [show, setShow] = React.useState(false)
17+
const container = React.useRef(null)
18+
19+
function handleClick() {
20+
setShow(!show)
21+
}
22+
23+
return (
24+
<Box>
25+
<Button onClick={handleClick}>{show ? 'Unmount' : 'Mount'}</Button>
26+
<Box>
27+
<Typography>It looks like I will render here?</Typography>
28+
{show ? (
29+
<Portal container={container.current}>
30+
<Typography>Actually render here!</Typography>
31+
</Portal>
32+
) : null}
33+
</Box>
34+
<Box ref={container} />
35+
</Box>
36+
)
37+
}
38+
```
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
title: Portal
3+
components: Portal
4+
api: true
5+
---
6+
7+
## API
8+
9+
```
10+
import Portal from '@fluent-ui/core/Portal'
11+
// or
12+
import { Portal } from '@fluent-ui/core'
13+
```
14+
15+
### Props
16+
17+
| Name | Type | Default | Description |
18+
| --- | --- | --- | --- |
19+
| children&nbsp;* | React.ReactElement | | The children to render into the `container`. |
20+
| container | Element &or; (() => Element) &or; null | | A node, component instance, or function that returns either. The `container` will have the portal children appended to it. By default, it uses the body of the top-level document object, so it's simply `document.body` most of the time. |
21+
| disablePortal | boolean | false | Disable the portal behavior. The children stay within it's parent DOM hierarchy. |

packages/fluent-ui.com/src/docs/components/Tooltip/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ type: DataDisplay
9898
color="white.default"
9999
boxShadow="1"
100100
textAlign="center"
101+
zIndex="1001"
101102
>
102103
hello
103104
</Box>

packages/fluent-ui/src/Command/Command.styled.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,5 @@ export const StyledSecondaryContainer = styled(Box).attrs(
3232
display: flex;
3333
flex-direction: column;
3434
width: 130px;
35+
z-index: 1001;
3536
`

packages/fluent-ui/src/Command/Command.tsx

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react'
22
import { More as MoreIcon } from '@fluent-ui/icons' // TODO treeShaking
3-
import { usePortal, useClickOutside, useReveal } from '@fluent-ui/hooks' // TODO treeShaking
3+
import { useClickOutside, useReveal, usePopper } from '@fluent-ui/hooks' // TODO treeShaking
44
import { omit } from '../utils'
55
import {
66
StyledContent,
@@ -11,6 +11,8 @@ import {
1111
import Secondary from './components/Secondary'
1212
import Content from './components/Content'
1313
import CommandButton from '../CommandButton'
14+
import Portal from '../Portal'
15+
import Transition from '../Transition'
1416
import { CommandProps, CommandContainer, CommandChild, CommandType } from './Command.type'
1517

1618
export const CommandContext = React.createContext(false)
@@ -35,34 +37,24 @@ const Command = React.forwardRef<HTMLDivElement, CommandProps>(
3537
}
3638
)
3739

38-
// Secondary Popup related
39-
const {
40-
// visible: secondaryVisible,
41-
setVisible: setSecondaryVisible,
42-
Portal: SecondaryPortal
43-
} = usePortal()
44-
const [portalStyle, setPortalStyle] = React.useState()
45-
function handleSecondaryVisible(e: React.MouseEvent<HTMLButtonElement>): void {
46-
const rect = e.currentTarget.getBoundingClientRect()
47-
const position = {
48-
position: 'absolute',
49-
left: `${rect.left + window.scrollX}px`,
50-
top: `${rect.top + window.scrollY + rect.height}px`,
51-
zIndex: 9999
52-
}
53-
setSecondaryVisible((visible: boolean): boolean => !visible)
54-
setPortalStyle(position)
55-
}
56-
5740
// Reveal does not take effect when using acrylic
5841
reveal = acrylic ? false : reveal
5942
const [RevealWrapper] = useReveal()
60-
43+
// Secondary Popup related
44+
const [secondaryVisible, setSecondaryVisible] = React.useState(false)
45+
function handleSecondaryVisible(): void {
46+
if (secondaryVisible) return
47+
setSecondaryVisible((visible: boolean): boolean => !visible)
48+
}
6149
// Click on the area outside the More menu to close the More menu.
62-
const secondaryRef = React.useRef<HTMLDivElement>(null)
50+
const [referenceRef, popperRef] = usePopper<HTMLButtonElement, HTMLDivElement>({
51+
placement: 'bottom'
52+
})
6353
useClickOutside(
64-
secondaryRef,
65-
(): void => {
54+
popperRef,
55+
(event): void => {
56+
// @ts-ignore
57+
if (!referenceRef.current || referenceRef.current.contains(event.target)) return
6658
setSecondaryVisible((visible: boolean): boolean => !visible)
6759
}
6860
)
@@ -82,21 +74,27 @@ const Command = React.forwardRef<HTMLDivElement, CommandProps>(
8274
{!!container.secondary.length &&
8375
(reveal ? (
8476
<RevealWrapper>
85-
<CommandButton style={{ height: '100%' }} onClick={handleSecondaryVisible}>
77+
<CommandButton
78+
ref={referenceRef}
79+
style={{ height: '100%' }}
80+
onClick={handleSecondaryVisible}
81+
>
8682
<MoreIcon />
8783
</CommandButton>
8884
</RevealWrapper>
8985
) : (
90-
<CommandButton onClick={handleSecondaryVisible}>
86+
<CommandButton ref={referenceRef} onClick={handleSecondaryVisible}>
9187
<MoreIcon />
9288
</CommandButton>
9389
))}
9490

95-
<SecondaryPortal style={portalStyle}>
96-
<StyledSecondaryContainer ref={secondaryRef} acrylic={acrylic}>
97-
{container.secondary}
98-
</StyledSecondaryContainer>
99-
</SecondaryPortal>
91+
<Portal>
92+
<Transition visible={secondaryVisible} wrapper={false} mountOnEnter unmountOnExit>
93+
<StyledSecondaryContainer ref={popperRef} acrylic={acrylic}>
94+
{container.secondary}
95+
</StyledSecondaryContainer>
96+
</Transition>
97+
</Portal>
10098
</CommandContext.Provider>
10199
</StyledContainer>
102100
)

packages/fluent-ui/src/Dialog/Dialog.tsx

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react'
2-
import { usePortal } from '@fluent-ui/hooks'
32
import { StyledDialogMask, StyledDialog, StyledDialogContent } from './Dialog.styled'
3+
import Portal from '../Portal'
44
import Transition from '../Transition'
55
import Title from './components/Title'
66
import Content from './components/Content'
@@ -19,7 +19,6 @@ export const DialogContext = React.createContext<DialogContextType>({
1919

2020
const Dialog = React.forwardRef<HTMLDivElement, DialogProps>(
2121
({ children, visible, onChange }: DialogProps, ref): React.ReactElement | null => {
22-
const { Portal } = usePortal({ defaultVisible: true })
2322
function handleClose(): void {
2423
onChange && onChange(false)
2524
}
@@ -48,13 +47,13 @@ const Dialog = React.forwardRef<HTMLDivElement, DialogProps>(
4847

4948
return (
5049
<>
51-
<Transition visible={visible} wrapper={false} mountOnEnter unmountOnExit>
52-
<Portal>
50+
<Portal>
51+
<Transition visible={visible} wrapper={false} mountOnEnter unmountOnExit>
5352
<StyledDialogMask onClick={handleClose} />
54-
</Portal>
55-
</Transition>
56-
<Transition visible={visible} wrapper={false} mountOnEnter unmountOnExit>
57-
<Portal>
53+
</Transition>
54+
</Portal>
55+
<Portal>
56+
<Transition visible={visible} wrapper={false} mountOnEnter unmountOnExit>
5857
<StyledDialog
5958
ref={ref}
6059
boxShadow="5"
@@ -68,8 +67,8 @@ const Dialog = React.forwardRef<HTMLDivElement, DialogProps>(
6867
{!!container.actions && container.actions}
6968
</DialogContext.Provider>
7069
</StyledDialog>
71-
</Portal>
72-
</Transition>
70+
</Transition>
71+
</Portal>
7372
</>
7473
)
7574
}

0 commit comments

Comments
 (0)