@sanity/ui 1.3.2 → 1.4.0
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 +2 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.esm.js +75 -30
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +76 -29
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/dialog/dialog.tsx +1 -0
- package/src/primitives/button/__workshop__/sanityUploadButton.tsx +1 -0
- package/src/primitives/heading/heading.tsx +1 -0
- package/src/primitives/label/label.tsx +1 -0
- package/src/primitives/popover/__workshop__/PositionStory.tsx +103 -0
- package/src/primitives/popover/__workshop__/index.ts +5 -0
- package/src/primitives/popover/index.ts +3 -0
- package/src/primitives/popover/popover.test.tsx +164 -0
- package/src/primitives/popover/popover.tsx +8 -0
- package/src/primitives/popover/popoverContext.tsx +9 -0
- package/src/primitives/popover/popoverProvider.tsx +29 -0
- package/src/primitives/popover/types.ts +8 -0
- package/src/primitives/popover/useOnForcedUpdate.ts +15 -0
- package/src/primitives/popover/usePopover.ts +24 -0
- package/src/primitives/text/text.tsx +1 -0
- package/src/utils/srOnly/srOnly.tsx +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"sideEffects": false,
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"source": "./src/index.ts",
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"@types/node": "^18.14.0",
|
|
76
76
|
"@types/react": "^18.0.28",
|
|
77
77
|
"@types/react-dom": "^18.0.11",
|
|
78
|
-
"@types/react-is": "^
|
|
78
|
+
"@types/react-is": "^18.0.0",
|
|
79
79
|
"@types/refractor": "^3.0.2",
|
|
80
80
|
"@types/styled-components": "^5.1.26",
|
|
81
81
|
"@types/testing-library__jest-dom": "^5.14.5",
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import {useBoolean} from '@sanity/ui-workshop'
|
|
2
|
+
import {useCallback, useMemo, useState} from 'react'
|
|
3
|
+
import {BoundaryElementProvider} from '../../../utils'
|
|
4
|
+
import {Button} from '../../button'
|
|
5
|
+
import {Card} from '../../card'
|
|
6
|
+
import {Flex} from '../../flex'
|
|
7
|
+
import {Text} from '../../text'
|
|
8
|
+
import {Popover, PopoverProps} from '../popover'
|
|
9
|
+
import {PopoverProvider} from '../popoverProvider'
|
|
10
|
+
import {usePopover} from '../usePopover'
|
|
11
|
+
|
|
12
|
+
function PopoverComponent({
|
|
13
|
+
referenceElement,
|
|
14
|
+
boundaryElement,
|
|
15
|
+
}: {
|
|
16
|
+
referenceElement: PopoverProps['referenceElement']
|
|
17
|
+
boundaryElement: PopoverProps['boundaryElement']
|
|
18
|
+
}) {
|
|
19
|
+
return (
|
|
20
|
+
<Popover
|
|
21
|
+
open
|
|
22
|
+
content={
|
|
23
|
+
<Card padding={5} radius={3}>
|
|
24
|
+
<Text weight="semibold" muted>
|
|
25
|
+
I should follow my reference element <br />
|
|
26
|
+
when the parent container is resized.
|
|
27
|
+
</Text>
|
|
28
|
+
</Card>
|
|
29
|
+
}
|
|
30
|
+
referenceElement={referenceElement}
|
|
31
|
+
boundaryElement={boundaryElement}
|
|
32
|
+
/>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default function PositionStory() {
|
|
37
|
+
return (
|
|
38
|
+
<PopoverProvider>
|
|
39
|
+
<InnerStory />
|
|
40
|
+
</PopoverProvider>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function InnerStory() {
|
|
45
|
+
const [size, setSize] = useState(0.5)
|
|
46
|
+
const [referenceElement, setReferenceElement] = useState<PopoverProps['referenceElement']>(null)
|
|
47
|
+
const [boundaryElement, setBoundaryElement] = useState<PopoverProps['boundaryElement']>(null)
|
|
48
|
+
const updatePositionOnClick = useBoolean('Force popover update on size change', false, 'Props')
|
|
49
|
+
const {forceUpdate} = usePopover()
|
|
50
|
+
|
|
51
|
+
// Random number between 0.2 and 5
|
|
52
|
+
const handleSetSize = useCallback(() => {
|
|
53
|
+
const s = Math.random() * (0.5 - 0.2) + 0.2
|
|
54
|
+
|
|
55
|
+
setSize(s)
|
|
56
|
+
|
|
57
|
+
if (updatePositionOnClick) {
|
|
58
|
+
forceUpdate()
|
|
59
|
+
}
|
|
60
|
+
}, [updatePositionOnClick])
|
|
61
|
+
|
|
62
|
+
const element = useMemo(() => {
|
|
63
|
+
return (
|
|
64
|
+
<Card
|
|
65
|
+
as="span"
|
|
66
|
+
border
|
|
67
|
+
radius={2}
|
|
68
|
+
style={{display: 'inline', padding: '0.4, 1'}}
|
|
69
|
+
ref={setReferenceElement}
|
|
70
|
+
tone="primary"
|
|
71
|
+
>
|
|
72
|
+
nunc
|
|
73
|
+
</Card>
|
|
74
|
+
)
|
|
75
|
+
}, [])
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<>
|
|
79
|
+
<PopoverComponent referenceElement={referenceElement} boundaryElement={boundaryElement} />
|
|
80
|
+
|
|
81
|
+
<BoundaryElementProvider element={boundaryElement as HTMLDivElement}>
|
|
82
|
+
<Flex height="fill">
|
|
83
|
+
<Card borderRight flex={size} tone="transparent">
|
|
84
|
+
<Flex align="center" justify="center" height="fill">
|
|
85
|
+
<Button text="Change size" onClick={handleSetSize} />
|
|
86
|
+
</Flex>
|
|
87
|
+
</Card>
|
|
88
|
+
|
|
89
|
+
<Card flex={1} ref={setBoundaryElement}>
|
|
90
|
+
<Flex height="fill" padding={5}>
|
|
91
|
+
<Text>
|
|
92
|
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam ultricies, nisl ut
|
|
93
|
+
aliquet ultricies, nunc sapien ultricies tortor, nec tincidunt elit risus ac nunc.
|
|
94
|
+
Nulla facilisi. Sed aliquam, {element} eget aliquam ultricies, diam diam aliquam
|
|
95
|
+
augue, eget ultricies nisl nunc eget nunc. Nulla facilisi. Sed aliquam,
|
|
96
|
+
</Text>
|
|
97
|
+
</Flex>
|
|
98
|
+
</Card>
|
|
99
|
+
</Flex>
|
|
100
|
+
</BoundaryElementProvider>
|
|
101
|
+
</>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/** @jest-environment jsdom */
|
|
2
|
+
|
|
3
|
+
import {fireEvent, waitFor, screen} from '@testing-library/react'
|
|
4
|
+
import {startTransition, useCallback, useEffect, useMemo, useState} from 'react'
|
|
5
|
+
import {render} from '../../../test'
|
|
6
|
+
import {PopoverContext} from './popoverContext'
|
|
7
|
+
import {PopoverContextValue} from './types'
|
|
8
|
+
import {useOnForcedUpdate} from './useOnForcedUpdate'
|
|
9
|
+
import {usePopover} from './usePopover'
|
|
10
|
+
|
|
11
|
+
describe('components/toast', () => {
|
|
12
|
+
describe('usePopover', () => {
|
|
13
|
+
it('should get context value', async () => {
|
|
14
|
+
const log = jest.fn()
|
|
15
|
+
|
|
16
|
+
function Debug() {
|
|
17
|
+
const popover = usePopover()
|
|
18
|
+
|
|
19
|
+
log(popover)
|
|
20
|
+
|
|
21
|
+
return <>debug</>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function Root() {
|
|
25
|
+
const value: PopoverContextValue = {
|
|
26
|
+
version: 0.0,
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
28
|
+
forceUpdate: () => {},
|
|
29
|
+
lastUpdateId: null,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<PopoverContext.Provider value={value}>
|
|
34
|
+
<Debug />
|
|
35
|
+
</PopoverContext.Provider>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
render(<Root />)
|
|
40
|
+
|
|
41
|
+
expect(log.mock.calls[0][0].version).toBe(0.0)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('should fail when no context value is provided', async () => {
|
|
45
|
+
const log = jest.fn()
|
|
46
|
+
|
|
47
|
+
function Debug() {
|
|
48
|
+
try {
|
|
49
|
+
usePopover()
|
|
50
|
+
} catch (err) {
|
|
51
|
+
log(err)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function Root() {
|
|
58
|
+
const value = undefined
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<PopoverContext.Provider value={value as any}>
|
|
62
|
+
<Debug />
|
|
63
|
+
</PopoverContext.Provider>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
render(<Root />)
|
|
68
|
+
|
|
69
|
+
expect(log.mock.calls[0][0].message).toEqual('usePopover(): missing context value')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should fail when context value is not compatible', async () => {
|
|
73
|
+
const log = jest.fn()
|
|
74
|
+
|
|
75
|
+
function Debug() {
|
|
76
|
+
try {
|
|
77
|
+
usePopover()
|
|
78
|
+
} catch (err) {
|
|
79
|
+
log(err)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function Root() {
|
|
86
|
+
// NOTE: we’re testing this because the context value may be a function in the future
|
|
87
|
+
const value = () => {
|
|
88
|
+
return {version: 1}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<PopoverContext.Provider value={value as any}>
|
|
93
|
+
<Debug />
|
|
94
|
+
</PopoverContext.Provider>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
render(<Root />)
|
|
99
|
+
|
|
100
|
+
expect(log.mock.calls[0][0].message).toEqual(
|
|
101
|
+
'usePopover(): the context value is not compatible'
|
|
102
|
+
)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('should trigger hook when forceUpdate is called', async () => {
|
|
106
|
+
const log = jest.fn()
|
|
107
|
+
const forcedCallback = jest.fn()
|
|
108
|
+
|
|
109
|
+
function Popover() {
|
|
110
|
+
useOnForcedUpdate(forcedCallback)
|
|
111
|
+
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function Debug() {
|
|
116
|
+
const popover = usePopover()
|
|
117
|
+
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
log(popover)
|
|
120
|
+
}, [popover])
|
|
121
|
+
|
|
122
|
+
const update = useCallback(() => {
|
|
123
|
+
popover.forceUpdate()
|
|
124
|
+
}, [popover])
|
|
125
|
+
|
|
126
|
+
return <button onClick={update}>Force position update</button>
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function Root() {
|
|
130
|
+
const [lastUpdateId, setLastUpdateId] = useState<string | null>(null)
|
|
131
|
+
const forceUpdate = useCallback(() => {
|
|
132
|
+
startTransition(() => {
|
|
133
|
+
setLastUpdateId('newid')
|
|
134
|
+
})
|
|
135
|
+
}, [])
|
|
136
|
+
const value: PopoverContextValue = useMemo(() => {
|
|
137
|
+
return {
|
|
138
|
+
version: 0.0,
|
|
139
|
+
forceUpdate,
|
|
140
|
+
lastUpdateId,
|
|
141
|
+
}
|
|
142
|
+
}, [forceUpdate, lastUpdateId])
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<PopoverContext.Provider value={value}>
|
|
146
|
+
<Debug />
|
|
147
|
+
<Popover />
|
|
148
|
+
</PopoverContext.Provider>
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
render(<Root />)
|
|
153
|
+
|
|
154
|
+
fireEvent.click(screen.getByText('Force position update'))
|
|
155
|
+
|
|
156
|
+
await waitFor(() => {
|
|
157
|
+
expect(log).toHaveBeenCalledTimes(3)
|
|
158
|
+
expect(log.mock.calls[0][0].lastUpdateId).toBeNull()
|
|
159
|
+
expect(log.mock.calls[2][0].lastUpdateId).not.toBeNull()
|
|
160
|
+
expect(forcedCallback).toHaveBeenCalledTimes(1)
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
})
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
} from './constants'
|
|
24
24
|
import {size} from './floating-ui/size'
|
|
25
25
|
import {PopoverCard} from './popoverCard'
|
|
26
|
+
import {useOnForcedUpdate} from './useOnForcedUpdate'
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* @public
|
|
@@ -200,10 +201,15 @@ export const Popover = memo(
|
|
|
200
201
|
placement,
|
|
201
202
|
reference: referenceRef,
|
|
202
203
|
floating: floatingRef,
|
|
204
|
+
update: floatingUpdate,
|
|
203
205
|
middlewareData,
|
|
204
206
|
strategy,
|
|
205
207
|
} = useFloating(floatingProps)
|
|
206
208
|
|
|
209
|
+
const handleForcedUpdate = useCallback(() => {
|
|
210
|
+
floatingUpdate()
|
|
211
|
+
}, [floatingUpdate])
|
|
212
|
+
|
|
207
213
|
const referenceHidden = middlewareData.hide?.referenceHidden
|
|
208
214
|
|
|
209
215
|
const arrowX = middlewareData.arrow?.x
|
|
@@ -247,6 +253,8 @@ export const Popover = memo(
|
|
|
247
253
|
referenceRef(referenceElement || null)
|
|
248
254
|
}, [referenceRef, referenceElement])
|
|
249
255
|
|
|
256
|
+
useOnForcedUpdate(handleForcedUpdate)
|
|
257
|
+
|
|
250
258
|
if (disabled) {
|
|
251
259
|
return childProp || <></>
|
|
252
260
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import {createContext} from 'react'
|
|
2
|
+
import {globalScope} from '../../lib/globalScope'
|
|
3
|
+
import type {PopoverContextValue} from './types'
|
|
4
|
+
|
|
5
|
+
const key = Symbol.for('@sanity/ui/context/popover')
|
|
6
|
+
|
|
7
|
+
globalScope[key] = globalScope[key] || createContext<PopoverContextValue | null>(null)
|
|
8
|
+
|
|
9
|
+
export const PopoverContext: React.Context<PopoverContextValue | null> = globalScope[key]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React, {useState, useCallback, useMemo} from 'react'
|
|
2
|
+
import {PopoverContext} from './popoverContext'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @public
|
|
6
|
+
*/
|
|
7
|
+
export interface PopoverProviderProps {
|
|
8
|
+
children?: React.ReactNode
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @public
|
|
13
|
+
*/
|
|
14
|
+
export const PopoverProvider = ({children}: PopoverProviderProps): React.ReactElement => {
|
|
15
|
+
const [lastUpdateId, setUpdateId] = useState<string | null>(null)
|
|
16
|
+
|
|
17
|
+
const forceUpdate = useCallback(() => {
|
|
18
|
+
// Generate a new random ID
|
|
19
|
+
const newId = Math.random().toString(36).substr(2, 9)
|
|
20
|
+
setUpdateId(newId)
|
|
21
|
+
}, [])
|
|
22
|
+
|
|
23
|
+
const value = useMemo(
|
|
24
|
+
() => ({version: 0.0, lastUpdateId, forceUpdate}),
|
|
25
|
+
[lastUpdateId, forceUpdate]
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
return <PopoverContext.Provider value={value}>{children}</PopoverContext.Provider>
|
|
29
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {useContext, useEffect} from 'react'
|
|
2
|
+
import {PopoverContext} from './popoverContext'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @public
|
|
6
|
+
*/
|
|
7
|
+
export const useOnForcedUpdate = (eventHandler: () => void): void => {
|
|
8
|
+
const contextValue = useContext(PopoverContext)
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (contextValue?.lastUpdateId !== null) {
|
|
12
|
+
eventHandler?.()
|
|
13
|
+
}
|
|
14
|
+
}, [contextValue?.lastUpdateId])
|
|
15
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {useContext} from 'react'
|
|
2
|
+
import {isRecord} from '../../lib/isRecord'
|
|
3
|
+
import {PopoverContext} from './popoverContext'
|
|
4
|
+
import type {PopoverContextValue} from './types'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @public
|
|
8
|
+
*/
|
|
9
|
+
export function usePopover(): PopoverContextValue {
|
|
10
|
+
const value = useContext(PopoverContext)
|
|
11
|
+
|
|
12
|
+
if (!value) {
|
|
13
|
+
throw new Error('usePopover(): missing context value')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// NOTE: This check is for future-compatiblity
|
|
17
|
+
// - If the value is not an object, it’s not compatible with the current version
|
|
18
|
+
// - If the value is an object, but doesn’t have `version: 0.0`, it’s not compatible with the current version
|
|
19
|
+
if (!isRecord(value) || value.version !== 0.0) {
|
|
20
|
+
throw new Error('usePopover(): the context value is not compatible')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return value
|
|
24
|
+
}
|