@sanity/ui 0.37.5 → 0.37.6

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.
Files changed (41) hide show
  1. package/lib/dts/components/autocomplete/autocomplete.d.ts.map +1 -1
  2. package/lib/dts/components/autocomplete/autocomplete.styles.d.ts.map +1 -1
  3. package/lib/dts/components/menu/useMenuController.d.ts.map +1 -1
  4. package/lib/dts/components/tab/tab.d.ts +1 -1
  5. package/lib/dts/components/tab/tab.d.ts.map +1 -1
  6. package/lib/dts/components/tree/tree.d.ts +1 -1
  7. package/lib/dts/components/tree/tree.d.ts.map +1 -1
  8. package/lib/dts/components/tree/treeGroup.d.ts +1 -1
  9. package/lib/dts/components/tree/treeGroup.d.ts.map +1 -1
  10. package/lib/dts/components/tree/treeItem.d.ts +1 -1
  11. package/lib/dts/components/tree/treeItem.d.ts.map +1 -1
  12. package/lib/dts/hooks/usePrefersDark.d.ts.map +1 -1
  13. package/lib/dts/primitives/textInput/textInput.d.ts.map +1 -1
  14. package/lib/dts/styles/input/textInputStyle.d.ts.map +1 -1
  15. package/lib/dts/utils/errorBoundary.d.ts.map +1 -1
  16. package/lib/sanity-ui.js +64 -37
  17. package/lib/sanity-ui.js.map +1 -1
  18. package/lib/sanity-ui.modern.js +76 -38
  19. package/lib/sanity-ui.modern.js.map +1 -1
  20. package/lib/sanity-ui.module.js +65 -38
  21. package/lib/sanity-ui.module.js.map +1 -1
  22. package/package.json +10 -10
  23. package/src/components/autocomplete/__workshop__/custom.tsx +10 -4
  24. package/src/components/autocomplete/__workshop__/example.tsx +38 -15
  25. package/src/components/autocomplete/__workshop__/fullscreen.tsx +31 -30
  26. package/src/components/autocomplete/autocomplete.styles.tsx +1 -0
  27. package/src/components/autocomplete/autocomplete.tsx +24 -3
  28. package/src/components/menu/useMenuController.ts +4 -8
  29. package/src/components/tab/tab.tsx +1 -1
  30. package/src/components/tree/__workshop__/basic.perf.ts +32 -0
  31. package/src/components/tree/__workshop__/basic.tsx +92 -56
  32. package/src/components/tree/__workshop__/index.ts +6 -1
  33. package/src/components/tree/tree.tsx +168 -144
  34. package/src/components/tree/treeGroup.tsx +3 -3
  35. package/src/components/tree/treeItem.tsx +4 -4
  36. package/src/hooks/usePrefersDark.ts +10 -4
  37. package/src/primitives/textInput/__workshop__/plain.tsx +49 -18
  38. package/src/primitives/textInput/textInput.tsx +23 -3
  39. package/src/styles/input/textInputStyle.ts +2 -0
  40. package/src/theme/{themeContext.ts → ThemeContext.ts} +0 -0
  41. package/src/utils/errorBoundary.tsx +3 -1
@@ -1,4 +1,5 @@
1
- import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'
1
+ import React, {forwardRef, memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'
2
+ import {useForwardedRef} from '../../hooks'
2
3
  import {Stack} from '../../primitives'
3
4
  import {_findNextItemElement, _findPrevItemElement, _focusItemElement} from './helpers'
4
5
  import {TreeContext} from './treeContext'
@@ -16,174 +17,197 @@ export interface TreeProps {
16
17
  * This API might change. DO NOT USE IN PRODUCTION.
17
18
  * @beta
18
19
  */
19
- export function Tree(
20
- props: TreeProps &
21
- Omit<React.HTMLProps<HTMLDivElement>, 'align' | 'as' | 'height' | 'ref' | 'role' | 'wrap'>
22
- ): React.ReactElement {
23
- const {children, space = 1, ...restProps} = props
24
- const [focusedElement, setFocusedElement] = useState<HTMLElement | null>(null)
25
- const path: string[] = useMemo(() => [], [])
26
- const rootRef = useRef<HTMLDivElement | null>(null)
27
- const [itemElements, setItemElements] = useState<HTMLElement[]>([])
28
- const [state, setState] = useState<TreeState>({})
29
-
30
- const registerItem = useCallback(
31
- (element: HTMLElement, path: string, expanded: boolean, selected: boolean) => {
32
- setState((s) => ({...s, [path]: {element, expanded}}))
33
-
34
- if (selected) {
35
- setFocusedElement(element)
36
- }
37
-
38
- return () => {
39
- setState((s) => {
40
- const newState = {...s}
41
-
42
- delete newState[path]
43
-
44
- return newState
45
- })
46
- }
47
- },
48
- []
49
- )
50
-
51
- const setExpanded = useCallback((path: string, expanded: boolean) => {
52
- setState((s) => {
53
- const itemState = s[path]
54
-
55
- if (!itemState) return s
56
-
57
- return {...s, [path]: {...itemState, expanded}}
58
- })
59
- }, [])
60
-
61
- const contextValue: TreeContextValue = useMemo(
62
- () => ({
63
- version: 0.0,
64
- focusedElement: focusedElement || itemElements[0] || null,
65
- level: 0,
66
- path,
67
- registerItem,
68
- setExpanded,
69
- setFocusedElement,
70
- space,
71
- state,
72
- }),
73
- [focusedElement, itemElements, path, registerItem, setExpanded, space, state]
74
- )
75
-
76
- const handleKeyDown = useCallback(
77
- (event: React.KeyboardEvent<HTMLDivElement>) => {
78
- if (!focusedElement) return
79
-
80
- if (event.key === 'ArrowDown') {
81
- event.preventDefault()
82
-
83
- const nextEl = _findNextItemElement(state, itemElements, focusedElement)
84
-
85
- if (nextEl) {
86
- _focusItemElement(nextEl)
87
- setFocusedElement(nextEl)
20
+ export const Tree = memo(
21
+ forwardRef(function Tree(
22
+ props: TreeProps &
23
+ Omit<React.HTMLProps<HTMLDivElement>, 'align' | 'as' | 'height' | 'ref' | 'role' | 'wrap'>,
24
+ ref: React.ForwardedRef<HTMLDivElement>
25
+ ): React.ReactElement {
26
+ const {children, space = 1, ...restProps} = props
27
+ const forwardedRef = useForwardedRef(ref)
28
+ const [focusedElement, setFocusedElement] = useState<HTMLElement | null>(null)
29
+ const focusedElementRef = useRef(focusedElement)
30
+ const path: string[] = useMemo(() => [], [])
31
+ const [itemElements, setItemElements] = useState<HTMLElement[]>([])
32
+ const [state, setState] = useState<TreeState>({})
33
+ const stateRef = useRef(state)
34
+
35
+ useEffect(() => {
36
+ focusedElementRef.current = focusedElement
37
+ }, [focusedElement])
38
+
39
+ useEffect(() => {
40
+ stateRef.current = state
41
+ }, [state])
42
+
43
+ const registerItem = useCallback(
44
+ (element: HTMLElement, path: string, expanded: boolean, selected: boolean) => {
45
+ setState((s) => ({...s, [path]: {element, expanded}}))
46
+
47
+ if (selected) {
48
+ setFocusedElement(element)
88
49
  }
89
50
 
90
- return
91
- }
51
+ return () => {
52
+ setState((s) => {
53
+ const newState = {...s}
92
54
 
93
- if (event.key === 'ArrowUp') {
94
- event.preventDefault()
55
+ delete newState[path]
95
56
 
96
- const prevEl = _findPrevItemElement(state, itemElements, focusedElement)
57
+ return newState
58
+ })
59
+ }
60
+ },
61
+ []
62
+ )
63
+
64
+ const setExpanded = useCallback((path: string, expanded: boolean) => {
65
+ setState((s) => {
66
+ const itemState = s[path]
67
+
68
+ if (!itemState) return s
69
+
70
+ return {...s, [path]: {...itemState, expanded}}
71
+ })
72
+ }, [])
73
+
74
+ const contextValue: TreeContextValue = useMemo(
75
+ () => ({
76
+ version: 0.0,
77
+ focusedElement: focusedElement || itemElements[0] || null,
78
+ level: 0,
79
+ path,
80
+ registerItem,
81
+ setExpanded,
82
+ setFocusedElement,
83
+ space,
84
+ state,
85
+ }),
86
+ [focusedElement, itemElements, path, registerItem, setExpanded, space, state]
87
+ )
88
+
89
+ const handleKeyDown = useCallback(
90
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
91
+ if (!focusedElementRef.current) return
92
+
93
+ if (event.key === 'ArrowDown') {
94
+ event.preventDefault()
95
+
96
+ const nextEl = _findNextItemElement(
97
+ stateRef.current,
98
+ itemElements,
99
+ focusedElementRef.current
100
+ )
101
+
102
+ if (nextEl) {
103
+ _focusItemElement(nextEl)
104
+ setFocusedElement(nextEl)
105
+ }
97
106
 
98
- if (prevEl) {
99
- _focusItemElement(prevEl)
100
- setFocusedElement(prevEl)
107
+ return
101
108
  }
102
109
 
103
- return
104
- }
110
+ if (event.key === 'ArrowUp') {
111
+ event.preventDefault()
105
112
 
106
- if (event.key === 'ArrowLeft') {
107
- event.preventDefault()
113
+ const prevEl = _findPrevItemElement(
114
+ stateRef.current,
115
+ itemElements,
116
+ focusedElementRef.current
117
+ )
108
118
 
109
- const itemKey = focusedElement.getAttribute('data-tree-key')
119
+ if (prevEl) {
120
+ _focusItemElement(prevEl)
121
+ setFocusedElement(prevEl)
122
+ }
110
123
 
111
- if (!itemKey) return
124
+ return
125
+ }
112
126
 
113
- const itemState = state[itemKey]
127
+ if (event.key === 'ArrowLeft') {
128
+ event.preventDefault()
114
129
 
115
- if (!itemState) return
130
+ const itemKey = focusedElementRef.current.getAttribute('data-tree-key')
116
131
 
117
- if (itemState.expanded) {
118
- setState((s) => {
119
- const itemState = s[itemKey]
132
+ if (!itemKey) return
120
133
 
121
- if (!itemState) return s
134
+ const itemState = stateRef.current[itemKey]
122
135
 
123
- return {...s, [itemKey]: {...itemState, expanded: false}}
124
- })
125
- } else {
126
- const itemPath = itemKey.split('/')
136
+ if (!itemState) return
137
+
138
+ if (itemState.expanded) {
139
+ setState((s) => {
140
+ const itemState = s[itemKey]
127
141
 
128
- itemPath.pop()
142
+ if (!itemState) return s
129
143
 
130
- const parentKey = itemPath.join('/')
131
- const parentState = parentKey && state[parentKey]
144
+ return {...s, [itemKey]: {...itemState, expanded: false}}
145
+ })
146
+ } else {
147
+ const itemPath = itemKey.split('/')
132
148
 
133
- if (parentState) {
134
- parentState.element.focus()
135
- setFocusedElement(parentState.element)
149
+ itemPath.pop()
150
+
151
+ const parentKey = itemPath.join('/')
152
+ const parentState = parentKey && stateRef.current[parentKey]
153
+
154
+ if (parentState) {
155
+ parentState.element.focus()
156
+ setFocusedElement(parentState.element)
157
+ }
136
158
  }
159
+
160
+ return
137
161
  }
138
162
 
139
- return
140
- }
163
+ if (event.key === 'ArrowRight') {
164
+ event.preventDefault()
141
165
 
142
- if (event.key === 'ArrowRight') {
143
- event.preventDefault()
166
+ const focusedKey = focusedElementRef.current.getAttribute('data-tree-key')
144
167
 
145
- const focusedKey = focusedElement.getAttribute('data-tree-key')
168
+ if (!focusedKey) return
146
169
 
147
- if (!focusedKey) return
170
+ if (!stateRef.current[focusedKey]?.expanded) {
171
+ setState((s) => {
172
+ const itemState = s[focusedKey]
148
173
 
149
- if (!state[focusedKey]?.expanded) {
150
- setState((s) => {
151
- const itemState = s[focusedKey]
174
+ if (!itemState) return s
152
175
 
153
- if (!itemState) return s
176
+ return {...s, [focusedKey]: {...itemState, expanded: true}}
177
+ })
178
+ }
154
179
 
155
- return {...s, [focusedKey]: {...itemState, expanded: true}}
156
- })
180
+ return
157
181
  }
158
-
159
- return
160
- }
161
- },
162
- [focusedElement, itemElements, state]
163
- )
164
-
165
- useEffect(() => {
166
- if (!rootRef.current) return
167
- const _itemElements = Array.from(
168
- rootRef.current.querySelectorAll('[data-ui="TreeItem"]')
169
- ) as HTMLElement[]
170
-
171
- setItemElements(_itemElements)
172
- }, [children])
173
-
174
- return (
175
- <TreeContext.Provider value={contextValue}>
176
- <Stack
177
- as="ul"
178
- data-ui="Tree"
179
- {...restProps}
180
- onKeyDown={handleKeyDown}
181
- ref={rootRef}
182
- role="tree"
183
- space={space}
184
- >
185
- {children}
186
- </Stack>
187
- </TreeContext.Provider>
188
- )
189
- }
182
+ },
183
+ [itemElements]
184
+ )
185
+
186
+ useEffect(() => {
187
+ if (!forwardedRef.current) return
188
+ const _itemElements = Array.from(
189
+ forwardedRef.current.querySelectorAll('[data-ui="TreeItem"]')
190
+ ) as HTMLElement[]
191
+
192
+ setItemElements(_itemElements)
193
+ }, [children, forwardedRef])
194
+
195
+ return (
196
+ <TreeContext.Provider value={contextValue}>
197
+ <Stack
198
+ as="ul"
199
+ data-ui="Tree"
200
+ {...restProps}
201
+ onKeyDown={handleKeyDown}
202
+ ref={forwardedRef}
203
+ role="tree"
204
+ space={space}
205
+ >
206
+ {children}
207
+ </Stack>
208
+ </TreeContext.Provider>
209
+ )
210
+ })
211
+ )
212
+
213
+ Tree.displayName = 'Tree'
@@ -1,4 +1,4 @@
1
- import React from 'react'
1
+ import React, {memo} from 'react'
2
2
  import {Stack} from '../../primitives'
3
3
  import {useTree} from './useTree'
4
4
 
@@ -6,7 +6,7 @@ export interface TreeGroupProps {
6
6
  expanded?: boolean
7
7
  }
8
8
 
9
- export function TreeGroup(
9
+ export const TreeGroup = memo(function TreeGroup(
10
10
  props: TreeGroupProps &
11
11
  Omit<React.HTMLProps<HTMLDivElement>, 'as' | 'height' | 'ref' | 'role' | 'wrap'>
12
12
  ): React.ReactElement {
@@ -26,4 +26,4 @@ export function TreeGroup(
26
26
  {children}
27
27
  </Stack>
28
28
  )
29
- }
29
+ })
@@ -1,6 +1,6 @@
1
1
  import {useId} from '@reach/auto-id'
2
2
  import {ToggleArrowRightIcon} from '@sanity/icons'
3
- import React, {createElement, useCallback, useEffect, useMemo, useRef} from 'react'
3
+ import React, {createElement, memo, useCallback, useEffect, useMemo, useRef} from 'react'
4
4
  import styled from 'styled-components'
5
5
  import {Box, Flex, Text} from '../../primitives'
6
6
  import {ThemeFontWeightKey} from '../../theme'
@@ -27,7 +27,7 @@ export interface TreeItemProps {
27
27
  weight?: ThemeFontWeightKey
28
28
  }
29
29
 
30
- const Root = styled.li(treeItemRootStyle, treeItemRootColorStyle)
30
+ const Root = memo(styled.li(treeItemRootStyle, treeItemRootColorStyle))
31
31
 
32
32
  const TreeItemBox = styled(Box).attrs({forwardedAs: 'a'})<TreeItemBoxStyleProps>(treeItemBoxStyle)
33
33
 
@@ -41,7 +41,7 @@ const ToggleArrowText = styled(Text)`
41
41
  * This API might change. DO NOT USE IN PRODUCTION.
42
42
  * @beta
43
43
  */
44
- export function TreeItem(
44
+ export const TreeItem = memo(function TreeItem(
45
45
  props: TreeItemProps & Omit<React.HTMLProps<HTMLLIElement>, 'as' | 'ref' | 'role'>
46
46
  ): React.ReactElement {
47
47
  const {
@@ -191,4 +191,4 @@ export function TreeItem(
191
191
  </TreeContext.Provider>
192
192
  </Root>
193
193
  )
194
- }
194
+ })
@@ -1,13 +1,19 @@
1
- import {useEffect, useState} from 'react'
1
+ import {useEffect, useMemo, useState} from 'react'
2
2
 
3
3
  /**
4
4
  * @public
5
5
  */
6
6
  export function usePrefersDark(): boolean {
7
- const [dark, setDark] = useState(false)
7
+ const mq = useMemo(() => {
8
+ if (typeof window === 'undefined') return undefined
9
+
10
+ return window.matchMedia('(prefers-color-scheme: dark)')
11
+ }, [])
12
+
13
+ const [dark, setDark] = useState(mq?.matches || false)
8
14
 
9
15
  useEffect(() => {
10
- const mq = window.matchMedia('(prefers-color-scheme: dark)')
16
+ if (!mq) return undefined
11
17
 
12
18
  setDark(mq.matches)
13
19
 
@@ -16,7 +22,7 @@ export function usePrefersDark(): boolean {
16
22
  mq.addEventListener('change', handleChange)
17
23
 
18
24
  return () => mq.removeEventListener('change', handleChange)
19
- }, [])
25
+ }, [mq])
20
26
 
21
27
  return dark
22
28
  }
@@ -1,7 +1,8 @@
1
1
  import {icons, IconSymbol} from '@sanity/icons'
2
2
  import {Container, Flex, Stack, Text, TextInput} from '@sanity/ui'
3
- import {useBoolean, useSelect, useText} from '@sanity/ui-workshop'
4
- import React from 'react'
3
+ import {PerfTestProps, useBoolean, usePerfTest, useSelect, useText} from '@sanity/ui-workshop'
4
+ import {fireEvent} from '@testing-library/dom'
5
+ import React, {useCallback, useState} from 'react'
5
6
  import {
6
7
  WORKSHOP_TEXT_FONT_SIZE_OPTIONS,
7
8
  WORKSHOP_FONT_WEIGHT_OPTIONS,
@@ -10,7 +11,25 @@ import {
10
11
  WORKSHOP_SPACE_OPTIONS,
11
12
  } from '../../../__workshop__/constants'
12
13
 
14
+ const typingPerfTest: PerfTestProps<HTMLInputElement> = {
15
+ name: 'typing',
16
+ title: 'Typing',
17
+ description: 'This test types one character at a time',
18
+ run({target}) {
19
+ const text = 'Hello, world & Hello, world & Hello, world'
20
+ const len = text.length
21
+
22
+ for (let i = 0; i < len; i += 1) {
23
+ const value = text.slice(0, i + 1)
24
+
25
+ fireEvent.change(target, {target: {value}})
26
+ }
27
+ },
28
+ }
29
+
13
30
  export default function PlainStory() {
31
+ const {ref, Wrapper} = usePerfTest(typingPerfTest)
32
+
14
33
  const border = useBoolean('Border', true, 'Props')
15
34
  const customValidity = useText('Custom validity', '', 'Props') || undefined
16
35
  const disabled = useBoolean('Disabled', false, 'Props')
@@ -29,6 +48,13 @@ export default function PlainStory() {
29
48
  const space = useSelect('Space', WORKSHOP_SPACE_OPTIONS, 3, 'Props')
30
49
  const weight = useSelect('Weight', WORKSHOP_FONT_WEIGHT_OPTIONS, '', 'Props') || undefined
31
50
 
51
+ const [value, setValue] = useState('')
52
+
53
+ const handleChange = useCallback(
54
+ (event: React.ChangeEvent<HTMLInputElement>) => setValue(event.currentTarget.value),
55
+ []
56
+ )
57
+
32
58
  return (
33
59
  <Flex align="center" height="fill" justify="center" padding={[4, 5, 6]} sizing="border">
34
60
  <Container width={0}>
@@ -36,22 +62,27 @@ export default function PlainStory() {
36
62
  <Text as="label" htmlFor="text-input-example" size={1} weight="semibold">
37
63
  TextInput
38
64
  </Text>
39
- <TextInput
40
- border={border}
41
- customValidity={customValidity}
42
- disabled={disabled}
43
- fontSize={fontSize}
44
- icon={icon && icons[icon]}
45
- iconRight={iconRight && icons[iconRight]}
46
- id="text-input-example"
47
- name="email"
48
- padding={padding}
49
- placeholder={placeholder}
50
- radius={radius}
51
- readOnly={readOnly}
52
- space={space}
53
- weight={weight}
54
- />
65
+ <Wrapper>
66
+ <TextInput
67
+ border={border}
68
+ customValidity={customValidity}
69
+ disabled={disabled}
70
+ fontSize={fontSize}
71
+ icon={icon && icons[icon]}
72
+ iconRight={iconRight && icons[iconRight]}
73
+ id="text-input-example"
74
+ name="email"
75
+ onChange={handleChange}
76
+ padding={padding}
77
+ placeholder={placeholder}
78
+ radius={radius}
79
+ readOnly={readOnly}
80
+ ref={ref}
81
+ space={space}
82
+ value={value}
83
+ weight={weight}
84
+ />
85
+ </Wrapper>
55
86
  </Stack>
56
87
  </Container>
57
88
  </Flex>
@@ -226,8 +226,28 @@ export const TextInput = forwardRef(function TextInput(
226
226
  )
227
227
 
228
228
  // Render clear button (memoized)
229
- const clearButtonBoxPadding = useMemo(() => padding.map((v) => v - 2), [padding])
230
- const clearButtonPadding = useMemo(() => padding.map((v) => v - 1), [padding])
229
+ const clearButtonBoxPadding = useMemo(
230
+ () =>
231
+ padding.map((v) => {
232
+ if (v === 0) return 0
233
+ if (v === 1) return 1
234
+ if (v === 2) return 1
235
+
236
+ return v - 2
237
+ }),
238
+ [padding]
239
+ )
240
+ const clearButtonPadding = useMemo(
241
+ () =>
242
+ padding.map((v) => {
243
+ if (v === 0) return 0
244
+ if (v === 1) return 0
245
+ if (v === 2) return 1
246
+
247
+ return v - 1
248
+ }),
249
+ [padding]
250
+ )
231
251
  const clearButtonProps: TextInputClearButtonProps = useMemo(
232
252
  () => (typeof clearButton === 'object' ? clearButton : EMPTY_RECORD),
233
253
  [clearButton]
@@ -249,7 +269,7 @@ export const TextInput = forwardRef(function TextInput(
249
269
  icon={CloseIcon}
250
270
  mode="bleed"
251
271
  padding={clearButtonPadding}
252
- radius={radius.map((r) => r - 1)}
272
+ radius={radius}
253
273
  {...clearButtonProps}
254
274
  onClick={handleClearClick}
255
275
  onMouseDown={handleClearMouseDown}
@@ -25,6 +25,8 @@ const ROOT_STYLE = css`
25
25
  &:not([hidden]) {
26
26
  display: flex;
27
27
  }
28
+
29
+ align-items: center;
28
30
  `
29
31
 
30
32
  export function textInputRootStyle(): FlattenSimpleInterpolation {
File without changes
@@ -37,7 +37,9 @@ export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoun
37
37
  const {error} = this.state
38
38
 
39
39
  if (error) {
40
- return <Code>{error.message}</Code>
40
+ const message = typeof error?.message === 'string' ? error.message : 'Error'
41
+
42
+ return <Code>{message}</Code>
41
43
  }
42
44
 
43
45
  return this.props.children