@sanity/ui 1.8.3 → 1.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/ui",
3
- "version": "1.8.3",
3
+ "version": "1.9.1",
4
4
  "sideEffects": false,
5
5
  "types": "./dist/index.d.ts",
6
6
  "source": "./src/index.ts",
@@ -44,8 +44,8 @@
44
44
  "test": "jest",
45
45
  "test:browser": "start-server-and-test 'run-s workshop:build workshop:start' http://localhost:1337 'run-s cypress:run'",
46
46
  "watch": "pkg watch --strict",
47
- "workshop:build": "node -r esbuild-register -r tsconfig-paths/register scripts/build",
48
- "workshop:dev": "node -r esbuild-register -r tsconfig-paths/register scripts/dev",
47
+ "workshop:build": "workshop build",
48
+ "workshop:dev": "workshop dev",
49
49
  "workshop:start": "http-server -a localhost -c-0 -p 1337 -s -P http://localhost:1337/index.html? dist"
50
50
  },
51
51
  "dependencies": {
@@ -66,7 +66,7 @@
66
66
  "@juggle/resize-observer": "^3.4.0",
67
67
  "@sanity/pkg-utils": "^2.4.8",
68
68
  "@sanity/semantic-release-preset": "^4.1.4",
69
- "@sanity/ui-workshop": "^1.2.11",
69
+ "@sanity/ui-workshop": "^2.0.0",
70
70
  "@testing-library/dom": "^9.3.1",
71
71
  "@testing-library/jest-dom": "^5.17.0",
72
72
  "@testing-library/react": "^14.0.0",
@@ -84,8 +84,6 @@
84
84
  "cypress": "^13.1.0",
85
85
  "cypress-real-events": "^1.10.1",
86
86
  "cz-conventional-changelog": "^3.3.0",
87
- "esbuild": "^0.19.2",
88
- "esbuild-register": "^3.4.2",
89
87
  "eslint": "^8.48.0",
90
88
  "eslint-config-prettier": "^9.0.0",
91
89
  "eslint-plugin-import": "^2.28.1",
@@ -10,5 +10,15 @@ export default defineScope({
10
10
  title: 'Props',
11
11
  component: lazy(() => import('./props')),
12
12
  },
13
+ {
14
+ name: 'resizableBoundary',
15
+ title: 'Resizable Boundary',
16
+ component: lazy(() => import('./resizableBoundary')),
17
+ },
18
+ {
19
+ name: 'overflowBoundary',
20
+ title: 'Overflowing Boundary',
21
+ component: lazy(() => import('./overflowingBoundary')),
22
+ },
13
23
  ],
14
24
  })
@@ -0,0 +1,53 @@
1
+ import {BoundaryElementProvider, Button, Card, Code, Flex, Text, Tooltip} from '@sanity/ui'
2
+ import {useBoolean, useSelect, useText} from '@sanity/ui-workshop'
3
+ import {useState} from 'react'
4
+ import {WORKSHOP_PLACEMENT_OPTIONS} from '../../../__workshop__/constants'
5
+
6
+ export default function OverflowingBoundaryStory() {
7
+ const content = useText(
8
+ 'Content',
9
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut mollis consectetur malesuada. Sed lobortis est dolor, eget imperdiet velit placerat et. Aenean posuere mi non aliquet iaculis. Donec fermentum pulvinar purus at sagittis. Ut tincidunt massa odio, sed finibus justo ullamcorper id. Nam venenatis justo non ligula elementum cursus. Pellentesque laoreet justo in mollis sagittis. In lacinia ornare ultrices. Suspendisse potenti.',
10
+ )
11
+ const placement = useSelect('Placement', WORKSHOP_PLACEMENT_OPTIONS, 'top')
12
+ const portal = useBoolean('Portal', false)
13
+ const useBoundaryElement = useBoolean('Use boundary element', true)
14
+
15
+ const [boundaryElement, setBoundaryElement] = useState<HTMLDivElement | null>(null)
16
+
17
+ return (
18
+ <Flex align="center" height="fill" justify="center" style={{height: '200vh'}}>
19
+ <BoundaryElementProvider element={useBoundaryElement ? boundaryElement : null}>
20
+ <Card
21
+ border
22
+ ref={setBoundaryElement}
23
+ style={{
24
+ height: 'calc(100vh - 100px)',
25
+ overflow: 'hidden',
26
+ position: 'relative',
27
+ resize: 'both',
28
+ width: '500px',
29
+ }}
30
+ >
31
+ <Flex align="center" height="fill" justify="center">
32
+ <Flex justify="center">
33
+ <Code size={1}>Placement: {placement}</Code>
34
+ </Flex>
35
+ </Flex>
36
+
37
+ <Tooltip
38
+ content={<Text size={1}>{content}</Text>}
39
+ padding={2}
40
+ placement={placement}
41
+ portal={portal}
42
+ >
43
+ <Button
44
+ mode="bleed"
45
+ style={{position: 'absolute', top: 10, right: 10}}
46
+ text="Tooltip"
47
+ />
48
+ </Tooltip>
49
+ </Card>
50
+ </BoundaryElementProvider>
51
+ </Flex>
52
+ )
53
+ }
@@ -0,0 +1,85 @@
1
+ import {BoundaryElementProvider, Button, Card, Code, Flex, Text, Tooltip} from '@sanity/ui'
2
+ import {useBoolean, useSelect, useText} from '@sanity/ui-workshop'
3
+ import {useState} from 'react'
4
+ import {WORKSHOP_PLACEMENT_OPTIONS} from '../../../__workshop__/constants'
5
+
6
+ export default function ResizableBoundaryStory() {
7
+ const content = useText(
8
+ 'Content',
9
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut mollis consectetur malesuada. Sed lobortis est dolor, eget imperdiet velit placerat et. Aenean posuere mi non aliquet iaculis. Donec fermentum pulvinar purus at sagittis. Ut tincidunt massa odio, sed finibus justo ullamcorper id. Nam venenatis justo non ligula elementum cursus. Pellentesque laoreet justo in mollis sagittis. In lacinia ornare ultrices. Suspendisse potenti.',
10
+ )
11
+ const placement = useSelect('Placement', WORKSHOP_PLACEMENT_OPTIONS, 'right')
12
+ const portal = useBoolean('Portal', true)
13
+ const useBoundaryElement = useBoolean('Use boundary element', true)
14
+
15
+ const [boundaryElement, setBoundaryElement] = useState<HTMLDivElement | null>(null)
16
+
17
+ return (
18
+ <Flex align="center" height="fill" justify="center">
19
+ <BoundaryElementProvider element={useBoundaryElement ? boundaryElement : null}>
20
+ <Card
21
+ border
22
+ ref={setBoundaryElement}
23
+ style={{
24
+ height: 'calc(100vh - 100px)',
25
+ overflow: 'hidden',
26
+ position: 'relative',
27
+ resize: 'both',
28
+ width: '500px',
29
+ }}
30
+ >
31
+ <Flex align="center" height="fill" justify="center">
32
+ <Flex justify="center">
33
+ <Code size={1}>Placement: {placement}</Code>
34
+ </Flex>
35
+ </Flex>
36
+
37
+ <Tooltip
38
+ content={<Text size={1}>{content}</Text>}
39
+ padding={2}
40
+ placement={placement}
41
+ portal={portal}
42
+ >
43
+ <Button mode="bleed" style={{position: 'absolute', top: 10, left: 10}} text="Tooltip" />
44
+ </Tooltip>
45
+ <Tooltip
46
+ content={<Text size={1}>{content}</Text>}
47
+ padding={2}
48
+ placement={placement}
49
+ portal={portal}
50
+ >
51
+ <Button
52
+ mode="bleed"
53
+ style={{position: 'absolute', top: 10, right: 10}}
54
+ text="Tooltip"
55
+ />
56
+ </Tooltip>
57
+ <Tooltip
58
+ content={<Text size={1}>{content}</Text>}
59
+ padding={2}
60
+ placement={placement}
61
+ portal={portal}
62
+ >
63
+ <Button
64
+ mode="bleed"
65
+ style={{position: 'absolute', bottom: 10, left: 10}}
66
+ text="Tooltip"
67
+ />
68
+ </Tooltip>
69
+ <Tooltip
70
+ content={<Text size={1}>{content}</Text>}
71
+ padding={2}
72
+ placement={placement}
73
+ portal={portal}
74
+ >
75
+ <Button
76
+ mode="bleed"
77
+ style={{position: 'absolute', bottom: 10, right: 10}}
78
+ text="Tooltip"
79
+ />
80
+ </Tooltip>
81
+ </Card>
82
+ </BoundaryElementProvider>
83
+ </Flex>
84
+ )
85
+ }
@@ -1,5 +1,6 @@
1
1
  import {Placement} from '@floating-ui/react-dom'
2
2
 
3
+ export const DEFAULT_TOOLTIP_PADDING = 4
3
4
  export const DEFAULT_FALLBACK_PLACEMENTS: Record<Placement, Placement[]> = {
4
5
  top: ['bottom', 'left', 'right'],
5
6
  'top-start': ['bottom-start', 'left-start', 'right-start'],
@@ -275,4 +275,71 @@ describe('Tooltip', () => {
275
275
  jest.clearAllMocks()
276
276
  })
277
277
  })
278
+ describe('Closing the <Tooltip /> with the Escape key', () => {
279
+ it('Standalone tooltip closes immediately with Escape key', () => {
280
+ const delay = 150
281
+
282
+ jest.useFakeTimers()
283
+
284
+ render(
285
+ <Tooltip
286
+ content={<Text size={1}>{'Tooltip content'}</Text>}
287
+ placement={'top'}
288
+ delay={delay}
289
+ >
290
+ <Button mode="bleed" text="Hover me" />
291
+ </Tooltip>,
292
+ )
293
+
294
+ const button = screen.getByText('Hover me')
295
+
296
+ // Validate tooltip content is not rendered
297
+ expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument()
298
+ fireEvent.focus(button)
299
+ act(() => jest.advanceTimersByTime(delay))
300
+
301
+ // Validate tooltip content is rendered
302
+ screen.getByText('Tooltip content')
303
+
304
+ act(() => {
305
+ fireEvent.keyDown(button, {key: 'Escape', code: 'Escape'})
306
+ })
307
+ // Validate tooltip content is not rendered anymore
308
+ expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument()
309
+ })
310
+ it('With <TooltipDelayGroupProvider /> closes immediately with Escape key', () => {
311
+ const delay = 150
312
+
313
+ jest.useFakeTimers()
314
+
315
+ render(
316
+ <TooltipDelayGroupProvider delay={{close: delay}}>
317
+ <Tooltip
318
+ content={<Text size={1}>{'Tooltip content'}</Text>}
319
+ placement={'top'}
320
+ delay={{close: delay}}
321
+ >
322
+ <Button mode="bleed" text="Hover me" />
323
+ </Tooltip>
324
+ </TooltipDelayGroupProvider>,
325
+ )
326
+
327
+ const button = screen.getByText('Hover me')
328
+
329
+ // Validate tooltip content is not rendered
330
+ expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument()
331
+ fireEvent.focus(button)
332
+
333
+ act(() => jest.advanceTimersByTime(delay))
334
+
335
+ // Validate tooltip content is rendered
336
+ screen.getByText('Tooltip content')
337
+
338
+ act(() => {
339
+ fireEvent.keyDown(button, {key: 'Escape', code: 'Escape'})
340
+ })
341
+ // Validate tooltip content is not rendered anymore
342
+ expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument()
343
+ })
344
+ })
278
345
  })
@@ -8,7 +8,6 @@ import {
8
8
  useFloating,
9
9
  Middleware,
10
10
  RootBoundary,
11
- size,
12
11
  } from '@floating-ui/react-dom'
13
12
  import {
14
13
  cloneElement,
@@ -31,7 +30,7 @@ import {Placement} from '../../types'
31
30
  import {Layer, LayerProps, Portal, useBoundaryElement} from '../../utils'
32
31
  import {Card} from '../card'
33
32
  import {Delay} from '../types'
34
- import {DEFAULT_FALLBACK_PLACEMENTS} from './constants'
33
+ import {DEFAULT_FALLBACK_PLACEMENTS, DEFAULT_TOOLTIP_PADDING} from './constants'
35
34
  import {TooltipArrow} from './tooltipArrow'
36
35
  import {useTooltipDelayGroup} from './tooltipDelayGroup'
37
36
 
@@ -63,6 +62,9 @@ export interface TooltipProps extends Omit<LayerProps, 'as'> {
63
62
 
64
63
  const Root = styled(Layer)`
65
64
  pointer-events: none;
65
+ // Note that 100vw doesn't exclude system scrollbars.
66
+ // This should be sufficiently small enough to not trigger overflow-x scrollbars when portalled.
67
+ max-width: calc(100vw - ${DEFAULT_TOOLTIP_PADDING * 10}px);
66
68
  `
67
69
 
68
70
  /**
@@ -102,35 +104,22 @@ export const Tooltip = forwardRef(function Tooltip(
102
104
  // Flip the floating element when leaving the boundary box
103
105
  ret.push(
104
106
  flip({
105
- boundary: boundaryElement || undefined,
107
+ boundary: portal ? undefined : boundaryElement || undefined,
106
108
  fallbackPlacements,
107
- padding: 4,
109
+ padding: DEFAULT_TOOLTIP_PADDING,
108
110
  rootBoundary,
109
- mainAxis: false,
110
111
  }),
111
112
  )
112
113
 
113
114
  // Define distance between reference and floating element
114
115
  ret.push(offset({mainAxis: 3}))
115
116
 
116
- // Set width and height on the floating element
117
- ret.push(
118
- size({
119
- apply({availableWidth, availableHeight, elements}) {
120
- Object.assign(elements.floating.style, {
121
- maxWidth: `${availableWidth - 4 * 2}px`, // the padding is `4px`
122
- maxHeight: `${availableHeight - 4 * 2}px`, // the padding is `4px`
123
- })
124
- },
125
- }),
126
- )
127
-
128
- // Shift the tooltip so its sits with the boundary eleement
117
+ // Shift the tooltip so its sits with the boundary element
129
118
  ret.push(
130
119
  shift({
131
- boundary: boundaryElement || undefined,
120
+ boundary: portal ? undefined : boundaryElement || undefined,
132
121
  rootBoundary,
133
- padding: 4,
122
+ padding: DEFAULT_TOOLTIP_PADDING,
134
123
  }),
135
124
  )
136
125
 
@@ -138,7 +127,7 @@ export const Tooltip = forwardRef(function Tooltip(
138
127
  ret.push(arrow({element: arrowRef, padding: 2}))
139
128
 
140
129
  return ret
141
- }, [boundaryElement, fallbackPlacements])
130
+ }, [boundaryElement, fallbackPlacements, portal])
142
131
 
143
132
  const {floatingStyles, placement, middlewareData, refs, update} = useFloating({
144
133
  middleware,
@@ -177,23 +166,27 @@ export const Tooltip = forwardRef(function Tooltip(
177
166
  const closeDelay = isInsideGroup ? delayGroupContext.closeDelay : closeDelayProp
178
167
 
179
168
  const handleIsOpenChange = useCallback(
180
- (open: boolean) => {
169
+ (open: boolean, immediate?: boolean) => {
181
170
  if (isInsideGroup) {
182
171
  // When it's inside a group, the open or close status will be handled by the group.
183
172
  if (open) {
184
- delayGroupContext.setIsGroupActive(open, openDelay)
185
- delayGroupContext.setOpenTooltipId(tooltipId, openDelay)
173
+ const groupedOpenDelay = immediate ? 0 : openDelay
174
+
175
+ delayGroupContext.setIsGroupActive(open, groupedOpenDelay)
176
+ delayGroupContext.setOpenTooltipId(tooltipId, groupedOpenDelay)
186
177
  } else {
187
178
  const minimumGroupDeactivateDelay = 200 // We should provide some delay to allow the user to reach the next tooltip.
188
179
  const groupDeactivateDelay =
189
180
  closeDelay > minimumGroupDeactivateDelay ? closeDelay : minimumGroupDeactivateDelay
190
181
 
191
182
  delayGroupContext.setIsGroupActive(open, groupDeactivateDelay)
192
- delayGroupContext.setOpenTooltipId(null, closeDelay)
183
+ delayGroupContext.setOpenTooltipId(null, immediate ? 0 : closeDelay)
193
184
  }
194
185
  } else {
186
+ const standaloneDelay = immediate ? 0 : open ? openDelay : closeDelay
187
+
195
188
  // When it's not inside a group, the open or close status will be handled by the tooltip itself.
196
- setIsOpen(open, open ? openDelay : closeDelay)
189
+ setIsOpen(open, standaloneDelay)
197
190
  }
198
191
  },
199
192
  [isInsideGroup, delayGroupContext, openDelay, tooltipId, closeDelay, setIsOpen],
@@ -243,6 +236,22 @@ export const Tooltip = forwardRef(function Tooltip(
243
236
  // Update reference
244
237
  useEffect(() => refs.setReference(referenceElement), [referenceElement, refs])
245
238
 
239
+ useEffect(() => {
240
+ // If the user clicks on escape key, close the tooltip.
241
+ if (!showTooltip) return
242
+
243
+ function handleWindowKeyDown(event: KeyboardEvent) {
244
+ if (event.key === 'Escape') {
245
+ handleIsOpenChange(false, true)
246
+ }
247
+ }
248
+
249
+ window.addEventListener('keydown', handleWindowKeyDown)
250
+
251
+ return () => {
252
+ window.removeEventListener('keydown', handleWindowKeyDown)
253
+ }
254
+ }, [handleIsOpenChange, showTooltip])
246
255
  const setArrow = useCallback(
247
256
  (arrowEl: HTMLDivElement | null) => {
248
257
  arrowRef.current = arrowEl