@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/ui",
3
- "version": "1.3.2",
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": "^17.0.3",
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",
@@ -109,6 +109,7 @@ const DialogCardRoot = styled(Card)`
109
109
  min-height: 0;
110
110
  max-height: 100%;
111
111
  overflow: hidden;
112
+ overflow: clip;
112
113
  `
113
114
 
114
115
  const DialogLayout = styled(Flex)`
@@ -6,6 +6,7 @@ const SanityUploadButton = styled(Button).attrs({forwardedAs: 'label'})`
6
6
  & input {
7
7
  -webkit-appearance: none;
8
8
  overflow: hidden;
9
+ overflow: clip;
9
10
  top: 0;
10
11
  left: 0;
11
12
  width: 100%;
@@ -39,6 +39,7 @@ const SpanWithTextOverflow = styled.span`
39
39
  white-space: nowrap;
40
40
  text-overflow: ellipsis;
41
41
  overflow: hidden;
42
+ overflow: clip;
42
43
  `
43
44
 
44
45
  /**
@@ -36,6 +36,7 @@ const SpanWithTextOverflow = styled.span`
36
36
  white-space: nowrap;
37
37
  text-overflow: ellipsis;
38
38
  overflow: hidden;
39
+ overflow: clip;
39
40
  `
40
41
 
41
42
  /**
@@ -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
+ }
@@ -40,5 +40,10 @@ export default defineScope({
40
40
  title: 'Open on mount',
41
41
  component: lazy(() => import('./OpenOnMountStory')),
42
42
  },
43
+ {
44
+ name: 'positionStory',
45
+ title: 'Position',
46
+ component: lazy(() => import('./PositionStory')),
47
+ },
43
48
  ],
44
49
  })
@@ -1 +1,4 @@
1
1
  export * from './popover'
2
+ export * from './popoverProvider'
3
+ export * from './usePopover'
4
+ export * from './types'
@@ -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,8 @@
1
+ /**
2
+ * @public
3
+ */
4
+ export interface PopoverContextValue {
5
+ version: number
6
+ forceUpdate: () => void
7
+ lastUpdateId: string | null
8
+ }
@@ -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
+ }
@@ -39,6 +39,7 @@ const SpanWithTextOverflow = styled.span`
39
39
  white-space: nowrap;
40
40
  text-overflow: ellipsis;
41
41
  overflow: hidden;
42
+ overflow: clip;
42
43
  `
43
44
 
44
45
  /**
@@ -15,6 +15,7 @@ const Root = styled.div`
15
15
  height: 0;
16
16
  position: absolute;
17
17
  overflow: hidden;
18
+ overflow: clip;
18
19
  `
19
20
 
20
21
  /**