@sanity/ui 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs.mjs +1 -0
- package/dist/index.d.ts +12 -4
- package/dist/index.esm.js +305 -177
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +305 -176
- package/dist/index.js.map +1 -1
- package/package.json +15 -15
- package/src/components/dialog/__workshop__/activate.tsx +134 -0
- package/src/components/dialog/__workshop__/index.ts +3 -0
- package/src/components/dialog/__workshop__/nested.tsx +14 -3
- package/src/components/dialog/__workshop__/panes.tsx +86 -0
- package/src/components/dialog/__workshop__/wrapped.tsx +78 -0
- package/src/components/dialog/dialog.tsx +110 -25
- package/src/helpers/element.ts +7 -0
- package/src/utils/layer/__workshop__/_debug.tsx +9 -3
- package/src/utils/layer/__workshop__/index.ts +3 -3
- package/src/utils/layer/__workshop__/multipleRoots.tsx +2 -1
- package/src/utils/layer/__workshop__/{plain.tsx → nested.tsx} +14 -4
- package/src/utils/layer/getLayerContext.test.ts +30 -0
- package/src/utils/layer/getLayerContext.ts +21 -0
- package/src/utils/layer/layer.test.tsx +1 -0
- package/src/utils/layer/layer.tsx +49 -7
- package/src/utils/layer/layerContext.ts +2 -2
- package/src/utils/layer/layerProvider.tsx +73 -12
- package/src/utils/layer/types.ts +3 -4
- package/src/utils/layer/useLayer.ts +9 -8
package/src/helpers/element.ts
CHANGED
|
@@ -46,3 +46,10 @@ export function isHTMLSelectElement(element: unknown): element is HTMLSelectElem
|
|
|
46
46
|
export function isHTMLTextAreaElement(element: unknown): element is HTMLTextAreaElement {
|
|
47
47
|
return isHTMLElement(element) && element.nodeName === 'TEXTAREA'
|
|
48
48
|
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @internal
|
|
52
|
+
*/
|
|
53
|
+
export function containsOrEqualsElement(element: HTMLElement, node: Node): boolean {
|
|
54
|
+
return element.contains(node) || element === node
|
|
55
|
+
}
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import {Code, useLayer} from '@sanity/ui'
|
|
2
2
|
|
|
3
|
-
export function LayerDebugInfo() {
|
|
3
|
+
export function LayerDebugInfo(props: {id?: string}) {
|
|
4
|
+
const {id} = props
|
|
4
5
|
const layer = useLayer()
|
|
5
6
|
|
|
6
7
|
return (
|
|
7
|
-
<Code>
|
|
8
|
-
|
|
8
|
+
<Code id={id}>
|
|
9
|
+
{[
|
|
10
|
+
//
|
|
11
|
+
`isTopLayer=${layer.isTopLayer}`,
|
|
12
|
+
`size=${layer.size}`,
|
|
13
|
+
`zIndex=${layer.zIndex}`,
|
|
14
|
+
].join('\n')}
|
|
9
15
|
</Code>
|
|
10
16
|
)
|
|
11
17
|
}
|
|
@@ -6,9 +6,9 @@ export default defineScope({
|
|
|
6
6
|
title: 'Layer',
|
|
7
7
|
stories: [
|
|
8
8
|
{
|
|
9
|
-
name: '
|
|
10
|
-
title: '
|
|
11
|
-
component: lazy(() => import('./
|
|
9
|
+
name: 'nested',
|
|
10
|
+
title: 'Nested',
|
|
11
|
+
component: lazy(() => import('./nested')),
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
14
|
name: 'multiple-roots',
|
|
@@ -23,8 +23,9 @@ export default function MultipleRootsStory() {
|
|
|
23
23
|
</Stack>
|
|
24
24
|
</Card>
|
|
25
25
|
</LayerProvider>
|
|
26
|
+
|
|
26
27
|
<LayerProvider zOffset={200}>
|
|
27
|
-
<Card as={Layer
|
|
28
|
+
<Card as={Layer} padding={3} shadow={5} style={{top: -50, left: 30}}>
|
|
28
29
|
<Stack space={3}>
|
|
29
30
|
<LayerDebugInfo />
|
|
30
31
|
<Layer>
|
|
@@ -4,13 +4,23 @@ import {useCallback, useState} from 'react'
|
|
|
4
4
|
import {LayerDebugInfo} from './_debug'
|
|
5
5
|
|
|
6
6
|
export default function PlainStory() {
|
|
7
|
+
return (
|
|
8
|
+
<Box padding={3}>
|
|
9
|
+
<Root />
|
|
10
|
+
<Root />
|
|
11
|
+
<Root />
|
|
12
|
+
</Box>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function Root() {
|
|
7
17
|
const [open, setOpen] = useState(false)
|
|
8
18
|
const handleOpen = useCallback(() => setOpen(true), [])
|
|
9
19
|
const handleClose = useCallback(() => setOpen(false), [])
|
|
10
20
|
|
|
11
21
|
return (
|
|
12
22
|
<LayerProvider>
|
|
13
|
-
<Card radius={2}>
|
|
23
|
+
<Card radius={2} shadow={1}>
|
|
14
24
|
<Box padding={3}>
|
|
15
25
|
<Text>
|
|
16
26
|
<strong>Root layer</strong>
|
|
@@ -18,8 +28,8 @@ export default function PlainStory() {
|
|
|
18
28
|
</Box>
|
|
19
29
|
<Box padding={3}>
|
|
20
30
|
<Stack space={3}>
|
|
21
|
-
<LayerDebugInfo />
|
|
22
|
-
<Button mode="ghost" onClick={handleOpen} text="Open layer 1" />
|
|
31
|
+
<LayerDebugInfo id="layer-debug-info-1" />
|
|
32
|
+
<Button id="open-layer-1" mode="ghost" onClick={handleOpen} text="Open layer 1" />
|
|
23
33
|
{open && (
|
|
24
34
|
<Layer style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
|
|
25
35
|
<Layer1 onClose={handleClose} />
|
|
@@ -52,7 +62,7 @@ function Layer1({onClose}: {onClose: () => void}) {
|
|
|
52
62
|
<Box padding={3}>
|
|
53
63
|
<Stack space={3}>
|
|
54
64
|
<LayerDebugInfo />
|
|
55
|
-
<Button mode="ghost" onClick={handleOpen} text="Open layer 2" />
|
|
65
|
+
<Button id="open-layer-2" mode="ghost" onClick={handleOpen} text="Open layer 2" />
|
|
56
66
|
{open && (
|
|
57
67
|
<Layer style={{display: 'flex', alignItems: 'center', justifyContent: 'center'}}>
|
|
58
68
|
<Layer2 onClose={handleClose} />
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {getLayerContext} from './getLayerContext'
|
|
2
|
+
import {LayerContextValue} from './types'
|
|
3
|
+
|
|
4
|
+
describe('getLayerContext', () => {
|
|
5
|
+
describe('0.0', () => {
|
|
6
|
+
it('should convert 0.0 to 0.0', () => {
|
|
7
|
+
const contextValue: LayerContextValue = {
|
|
8
|
+
version: 0.0,
|
|
9
|
+
isTopLayer: true,
|
|
10
|
+
level: 0,
|
|
11
|
+
registerChild: () => () => {
|
|
12
|
+
//
|
|
13
|
+
},
|
|
14
|
+
size: 0,
|
|
15
|
+
zIndex: 0,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const result = getLayerContext(contextValue)
|
|
19
|
+
|
|
20
|
+
expect(result).toEqual({
|
|
21
|
+
version: 0.0,
|
|
22
|
+
isTopLayer: true,
|
|
23
|
+
level: 0,
|
|
24
|
+
registerChild: expect.any(Function),
|
|
25
|
+
size: 0,
|
|
26
|
+
zIndex: 0,
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {isRecord} from '../../lib/isRecord'
|
|
2
|
+
import {LayerContextValue} from './types'
|
|
3
|
+
|
|
4
|
+
export function getLayerContext(contextValue: LayerContextValue): LayerContextValue {
|
|
5
|
+
// NOTE: This check is for future-compatiblity
|
|
6
|
+
// - If the value is not an object, it’s not compatible with the current version
|
|
7
|
+
// - If the value is an object, but doesn’t have `version: 0.0`, it’s not compatible with the current version
|
|
8
|
+
if (!isRecord(contextValue) || contextValue.version !== 0.0) {
|
|
9
|
+
throw new Error('the context value is not compatible')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (!contextValue) {
|
|
13
|
+
throw new Error('components using `useLayer()` should be wrapped in a <LayerProvider>.')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (contextValue.version === 0.0) {
|
|
17
|
+
return contextValue
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
throw new Error('could not get layer context')
|
|
21
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {forwardRef} from 'react'
|
|
1
|
+
import {FocusEvent, forwardRef, useCallback, useEffect, useRef} from 'react'
|
|
2
2
|
import styled from 'styled-components'
|
|
3
3
|
import {EMPTY_RECORD} from '../../constants'
|
|
4
|
+
import {containsOrEqualsElement, isHTMLElement} from '../../helpers'
|
|
5
|
+
import {useForwardedRef} from '../../hooks'
|
|
4
6
|
import {LayerProvider} from './layerProvider'
|
|
5
7
|
import {useLayer} from './useLayer'
|
|
6
8
|
|
|
@@ -9,26 +11,66 @@ import {useLayer} from './useLayer'
|
|
|
9
11
|
*/
|
|
10
12
|
export interface LayerProps {
|
|
11
13
|
as?: React.ElementType | keyof JSX.IntrinsicElements
|
|
14
|
+
/** A callback that fires when the layer becomes the top layer when it was not the top layer before. */
|
|
15
|
+
onActivate?: (props: {activeElement: HTMLElement | null}) => void
|
|
12
16
|
zOffset?: number | number[]
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
interface LayerChildrenProps {
|
|
16
20
|
as?: React.ElementType | keyof JSX.IntrinsicElements
|
|
21
|
+
onActivate?: LayerProps['onActivate']
|
|
17
22
|
}
|
|
18
23
|
|
|
19
|
-
const Root = styled.div
|
|
20
|
-
position: relative;
|
|
21
|
-
`
|
|
24
|
+
const Root = styled.div({position: 'relative'})
|
|
22
25
|
|
|
23
26
|
const LayerChildren = forwardRef(function LayerChildren(
|
|
24
27
|
props: LayerChildrenProps & Omit<React.HTMLProps<HTMLDivElement>, 'as'>,
|
|
25
28
|
ref: React.Ref<HTMLDivElement>
|
|
26
29
|
) {
|
|
27
|
-
const {children, style = EMPTY_RECORD, ...restProps} = props
|
|
28
|
-
const {zIndex} = useLayer()
|
|
30
|
+
const {children, onActivate, onFocus, style = EMPTY_RECORD, ...restProps} = props
|
|
31
|
+
const {zIndex, isTopLayer} = useLayer()
|
|
32
|
+
const lastFocusedRef = useRef<HTMLElement | null>(null)
|
|
33
|
+
const forwardedRef = useForwardedRef(ref)
|
|
34
|
+
const isTopLayerRef = useRef<boolean>(isTopLayer)
|
|
35
|
+
|
|
36
|
+
// When the layer very first mounts, it will be the top layer, but we don't want to fire
|
|
37
|
+
// the callback in that case. We use a ref to track the previous value of isTopLayer to
|
|
38
|
+
// determine if the layer has become the top layer since the last render.
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const becameTopLayer = isTopLayerRef.current !== isTopLayer && isTopLayer
|
|
41
|
+
|
|
42
|
+
if (becameTopLayer) {
|
|
43
|
+
onActivate?.({activeElement: lastFocusedRef.current})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
isTopLayerRef.current = isTopLayer
|
|
47
|
+
}, [isTopLayer, onActivate])
|
|
48
|
+
|
|
49
|
+
const handleFocus = useCallback(
|
|
50
|
+
(event: FocusEvent<HTMLDivElement, Element>) => {
|
|
51
|
+
// Call the user-provided onFocus handler if any
|
|
52
|
+
onFocus?.(event)
|
|
53
|
+
|
|
54
|
+
const rootElement = forwardedRef.current
|
|
55
|
+
const target = document.activeElement
|
|
56
|
+
|
|
57
|
+
if (!isTopLayer || !rootElement || !target) return
|
|
58
|
+
|
|
59
|
+
if (isHTMLElement(target) && containsOrEqualsElement(rootElement, target)) {
|
|
60
|
+
lastFocusedRef.current = target
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
[forwardedRef, isTopLayer, onFocus]
|
|
64
|
+
)
|
|
29
65
|
|
|
30
66
|
return (
|
|
31
|
-
<Root
|
|
67
|
+
<Root
|
|
68
|
+
{...restProps}
|
|
69
|
+
data-ui="Layer"
|
|
70
|
+
onFocus={handleFocus}
|
|
71
|
+
ref={forwardedRef}
|
|
72
|
+
style={{...style, zIndex}}
|
|
73
|
+
>
|
|
32
74
|
{children}
|
|
33
75
|
</Root>
|
|
34
76
|
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {createContext} from 'react'
|
|
1
|
+
import {Context, createContext} from 'react'
|
|
2
2
|
import {globalScope} from '../../lib/globalScope'
|
|
3
3
|
import {LayerContextValue} from './types'
|
|
4
4
|
|
|
@@ -6,4 +6,4 @@ const key = Symbol.for('@sanity/ui/context/layer')
|
|
|
6
6
|
|
|
7
7
|
globalScope[key] = globalScope[key] || createContext<LayerContextValue | null>(null)
|
|
8
8
|
|
|
9
|
-
export const LayerContext:
|
|
9
|
+
export const LayerContext: Context<LayerContextValue | null> = globalScope[key]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {useCallback, useContext, useEffect, useMemo, useState} from 'react'
|
|
2
2
|
import {useMediaIndex, useArrayProp} from '../../hooks'
|
|
3
|
+
import {getLayerContext} from './getLayerContext'
|
|
3
4
|
import {LayerContext} from './layerContext'
|
|
4
5
|
import {LayerContextValue} from './types'
|
|
5
6
|
|
|
@@ -16,30 +17,90 @@ export interface LayerProviderProps {
|
|
|
16
17
|
*/
|
|
17
18
|
export function LayerProvider(props: LayerProviderProps): React.ReactElement {
|
|
18
19
|
const {children, zOffset: zOffsetProp = 0} = props
|
|
19
|
-
|
|
20
|
+
|
|
21
|
+
// Get parent context values
|
|
22
|
+
const parentContextValue = useContext(LayerContext)
|
|
23
|
+
const parent = parentContextValue && getLayerContext(parentContextValue)
|
|
24
|
+
const parentRegisterChild = parent?.registerChild
|
|
25
|
+
const parentLevel = parent?.level ?? 0
|
|
26
|
+
|
|
27
|
+
// Get level
|
|
28
|
+
const level = parentLevel + 1
|
|
29
|
+
|
|
30
|
+
// Get z-index offset
|
|
20
31
|
const zOffset = useArrayProp(zOffsetProp)
|
|
32
|
+
|
|
33
|
+
// Get responsive z-index value
|
|
21
34
|
const maxMediaIndex = zOffset.length - 1
|
|
22
35
|
const mediaIndex = Math.min(useMediaIndex(), maxMediaIndex)
|
|
23
36
|
const zIndex = parent ? parent.zIndex + zOffset[mediaIndex] : zOffset[mediaIndex]
|
|
37
|
+
|
|
38
|
+
// A state value that is used to keep track of the number of child layers on each level
|
|
39
|
+
const [, setChildLayers] = useState<Record<number, number>>({})
|
|
40
|
+
|
|
41
|
+
// A state value that is used to keep track of the number of child levels
|
|
24
42
|
const [size, setSize] = useState(0)
|
|
25
43
|
|
|
26
|
-
const
|
|
27
|
-
setSize((v) => v + 1)
|
|
44
|
+
const isTopLayer = size === 0
|
|
28
45
|
|
|
29
|
-
|
|
30
|
-
|
|
46
|
+
const registerChild = useCallback(
|
|
47
|
+
(childLevel?: number) => {
|
|
48
|
+
// Register child layers to the parent layer
|
|
49
|
+
const parentDispose = parentRegisterChild?.(childLevel)
|
|
31
50
|
|
|
32
|
-
|
|
51
|
+
if (childLevel !== undefined) {
|
|
52
|
+
setChildLayers((state) => {
|
|
53
|
+
const prevLen = state[childLevel] ?? 0
|
|
54
|
+
const nextState = {...state, [childLevel]: prevLen + 1}
|
|
33
55
|
|
|
34
|
-
|
|
35
|
-
|
|
56
|
+
setSize(Object.keys(nextState).length)
|
|
57
|
+
|
|
58
|
+
return nextState
|
|
59
|
+
})
|
|
60
|
+
} else {
|
|
61
|
+
// Legacy behavior: if no child level is provided, increment the size by 1
|
|
62
|
+
setSize((v) => v + 1)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return () => {
|
|
66
|
+
if (childLevel !== undefined) {
|
|
67
|
+
setChildLayers((state) => {
|
|
68
|
+
const nextState = {...state}
|
|
69
|
+
|
|
70
|
+
if (nextState[childLevel] === 1) {
|
|
71
|
+
delete nextState[childLevel]
|
|
72
|
+
|
|
73
|
+
setSize(Object.keys(nextState).length)
|
|
74
|
+
} else {
|
|
75
|
+
nextState[childLevel] -= 1
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return nextState
|
|
79
|
+
})
|
|
80
|
+
} else {
|
|
81
|
+
// Legacy behavior: if no child level is provided, decrement the size by 1
|
|
82
|
+
setSize((v) => v - 1)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
parentDispose?.()
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
[parentRegisterChild]
|
|
89
|
+
)
|
|
36
90
|
|
|
37
|
-
|
|
38
|
-
|
|
91
|
+
// Register this layer on mount
|
|
92
|
+
useEffect(() => parentRegisterChild?.(level), [level, parentRegisterChild])
|
|
39
93
|
|
|
40
94
|
const value: LayerContextValue = useMemo(
|
|
41
|
-
() => ({
|
|
42
|
-
|
|
95
|
+
() => ({
|
|
96
|
+
version: 0.0,
|
|
97
|
+
isTopLayer,
|
|
98
|
+
level,
|
|
99
|
+
registerChild,
|
|
100
|
+
size,
|
|
101
|
+
zIndex,
|
|
102
|
+
}),
|
|
103
|
+
[isTopLayer, level, registerChild, size, zIndex]
|
|
43
104
|
)
|
|
44
105
|
|
|
45
106
|
return <LayerContext.Provider value={value}>{children}</LayerContext.Provider>
|
package/src/utils/layer/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {useContext} from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import {getLayerContext} from './getLayerContext'
|
|
3
3
|
import {LayerContext} from './layerContext'
|
|
4
4
|
import {LayerContextValue} from './types'
|
|
5
5
|
|
|
@@ -13,12 +13,13 @@ export function useLayer(): LayerContextValue {
|
|
|
13
13
|
throw new Error('useLayer(): missing context value')
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
try {
|
|
17
|
+
return getLayerContext(value)
|
|
18
|
+
} catch (err) {
|
|
19
|
+
if (err instanceof Error) {
|
|
20
|
+
throw new Error(`useLayer(): ${err.message}`)
|
|
21
|
+
} else {
|
|
22
|
+
throw new Error(`useLayer(): ${err}`)
|
|
23
|
+
}
|
|
21
24
|
}
|
|
22
|
-
|
|
23
|
-
return value
|
|
24
25
|
}
|