@sanity/ui 1.1.0-beta.2 → 1.1.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.
@@ -0,0 +1,56 @@
1
+ /** @jest-environment jsdom */
2
+ /**
3
+ * As this hook is used for top-level theming it's likely to be called while server-rendering
4
+ * and that's why it's worth it to have a testing suite for hydration
5
+ */
6
+ import {waitFor} from '@testing-library/dom'
7
+ import {hydrateRoot} from 'react-dom/client'
8
+
9
+ import {usePrefersReducedMotion} from './usePrefersReducedMotion'
10
+
11
+ function Log() {
12
+ const reduceMotion = usePrefersReducedMotion()
13
+
14
+ return <>prefers-motion: {JSON.stringify(reduceMotion)}</>
15
+ }
16
+
17
+ const originalMatchMedia = window.matchMedia
18
+
19
+ describe('usePrefersReducedMotion SSR hydration', () => {
20
+ beforeAll(() => {
21
+ window.matchMedia = () =>
22
+ ({
23
+ addEventListener: jest.fn(),
24
+ removeEventListener: jest.fn(),
25
+ matches: true,
26
+ } as any)
27
+ })
28
+
29
+ afterAll(() => {
30
+ window.matchMedia = originalMatchMedia
31
+ })
32
+
33
+ it(`hydrates without any warnings`, async () => {
34
+ const spy = jest.spyOn(console, 'error').mockImplementation()
35
+
36
+ const node = document.createElement('div')
37
+
38
+ document.body.appendChild(node)
39
+
40
+ node.innerHTML = `prefers-motion: <!-- -->false`
41
+
42
+ hydrateRoot(node, <Log />)
43
+
44
+ // It's false initially
45
+ await waitFor(() => expect(node.innerHTML).toBe('prefers-motion: <!-- -->false'))
46
+
47
+ // After hydration it should switch to true
48
+ await waitFor(() => expect(node.innerHTML).toBe('prefers-motion: <!-- -->true'))
49
+
50
+ // eslint-disable-next-line no-console
51
+ expect(console.error).not.toHaveBeenCalled()
52
+
53
+ spy.mockReset()
54
+ spy.mockRestore()
55
+ })
56
+ })
@@ -0,0 +1,18 @@
1
+ /** @jest-environment node */
2
+ import {renderToString, renderToStaticMarkup} from 'react-dom/server'
3
+ import {usePrefersReducedMotion} from './usePrefersReducedMotion'
4
+
5
+ function Log() {
6
+ const reduceMotion = usePrefersReducedMotion()
7
+
8
+ return <>prefers-motion: {JSON.stringify(reduceMotion)}</>
9
+ }
10
+
11
+ describe('usePrefersReducedMotion', () => {
12
+ it(`SSR to static markup returns false`, () => {
13
+ expect(renderToStaticMarkup(<Log />)).toBe('prefers-motion: false')
14
+ })
15
+ it(`SSR to markup for hydration doesn't throw`, () => {
16
+ expect(renderToString(<Log />)).toMatchInlineSnapshot(`"prefers-motion: <!-- -->false"`)
17
+ })
18
+ })
@@ -0,0 +1,62 @@
1
+ import {useSyncExternalStore} from 'react'
2
+
3
+ let MEDIA_QUERY_CACHE: MediaQueryList | undefined
4
+
5
+ /**
6
+ * Lazy init the matchMedia instance
7
+ */
8
+ function getMatchMedia(): MediaQueryList {
9
+ if (!MEDIA_QUERY_CACHE) {
10
+ // As this function is only called during `subscribe` and `getSnapshot`, we can assume that the
11
+ // the `window` global is available and we're in a browser environment
12
+ MEDIA_QUERY_CACHE = window.matchMedia('(prefers-reduced-motion: reduce)')
13
+ }
14
+
15
+ return MEDIA_QUERY_CACHE
16
+ }
17
+
18
+ /**
19
+ * As the query is the same for all instances of this hook, we can cache the matchMedia instance
20
+ * and have cheap `change` event listeners, while getSnapshot always reads from the same
21
+ * matchMedia instance and we don't get any tearing.
22
+ * Tearing in this context means the bad edge case in React concurrent render mdoe
23
+ * where you sometimes would end up with some components doing render while seeing `usePrefersDark() === true` while others would see `usePrefersDark() === false`
24
+ * during the same render.
25
+ * By using `useSyncExternalStore` every component only sees the same value during the same render, and always re-render when it changes no matter
26
+ * what React.memo boundaries there might be between the layers..
27
+ */
28
+ function subscribe(onStoreChange: () => void): () => void {
29
+ const matchMedia = getMatchMedia()
30
+
31
+ matchMedia.addEventListener('change', onStoreChange)
32
+
33
+ return () => matchMedia.removeEventListener('change', onStoreChange)
34
+ }
35
+
36
+ /**
37
+ * Only called client-side, when using createRoot, or after hydration is complete when using hydrateRoot.
38
+ * It's important that this function does not create new objects or arrays when called:
39
+ * https://beta.reactjs.org/apis/react/useSyncExternalStore#im-getting-an-error-the-result-of-getsnapshot-should-be-cached
40
+ */
41
+ function getSnapshot() {
42
+ return getMatchMedia().matches
43
+ }
44
+
45
+ /**
46
+ * Only called during server-side rendering, and hydration if using hydrateRoot
47
+ * Since the server environment doesn't have access to the DOM, we can't determine the current value of the media query
48
+ * and we assume `(prefers-reduced-motion: no-preference)` since it's the most common scheme
49
+ *
50
+ * @link https://beta.reactjs.org/apis/react/useSyncExternalStore#adding-support-for-server-rendering
51
+ */
52
+ function getServerSnapshot() {
53
+ return false
54
+ }
55
+
56
+ /**
57
+ * Returns true if motion should be reduced
58
+ * @public
59
+ */
60
+ export function usePrefersReducedMotion(): boolean {
61
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
62
+ }
@@ -4,6 +4,6 @@ import {ResizeObserver as ResizeObserverPolyfill} from '@juggle/resize-observer'
4
4
  * @internal
5
5
  */
6
6
  export const _ResizeObserver: typeof ResizeObserver =
7
- typeof window !== 'undefined' && window.ResizeObserver
7
+ typeof document !== 'undefined' && window.ResizeObserver
8
8
  ? window.ResizeObserver
9
9
  : ResizeObserverPolyfill
@@ -63,6 +63,12 @@ export function codeBaseStyle(): FlattenInterpolation<StyledThemeProps<Theme>> {
63
63
  border-radius: 1px;
64
64
  }
65
65
 
66
+ & svg {
67
+ /* Certain popular CSS libraries changes the defaults for SVG display */
68
+ /* Make sure SVGs are rendered as inline elements */
69
+ display: inline;
70
+ }
71
+
66
72
  & [data-sanity-icon] {
67
73
  vertical-align: baseline;
68
74
  }
@@ -44,6 +44,12 @@ export function headingBaseStyle(
44
44
  }
45
45
  }
46
46
 
47
+ & svg {
48
+ /* Certain popular CSS libraries changes the defaults for SVG display */
49
+ /* Make sure SVGs are rendered as inline elements */
50
+ display: inline;
51
+ }
52
+
47
53
  & [data-sanity-icon] {
48
54
  vertical-align: baseline;
49
55
  }
@@ -30,6 +30,12 @@ export function labelBaseStyle(
30
30
  border-radius: 1px;
31
31
  }
32
32
 
33
+ & svg {
34
+ /* Certain popular CSS libraries changes the defaults for SVG display */
35
+ /* Make sure SVGs are rendered as inline elements */
36
+ display: inline;
37
+ }
38
+
33
39
  & [data-sanity-icon] {
34
40
  vertical-align: baseline;
35
41
  }
@@ -0,0 +1,11 @@
1
+ import {Button, Card, Popover, Text} from '@sanity/ui'
2
+
3
+ export default function OpenOnMountStory() {
4
+ return (
5
+ <Card height="fill" padding={3} sizing="border">
6
+ <Popover content={<Text>popover content</Text>} open padding={3}>
7
+ <Button text="This button is the popover reference" />
8
+ </Popover>
9
+ </Card>
10
+ )
11
+ }
@@ -35,5 +35,10 @@ export default defineScope({
35
35
  title: 'Aligned',
36
36
  component: lazy(() => import('./AlignedStory')),
37
37
  },
38
+ {
39
+ name: 'openOnMountStory',
40
+ title: 'Open on mount',
41
+ component: lazy(() => import('./OpenOnMountStory')),
42
+ },
38
43
  ],
39
44
  })
@@ -52,6 +52,12 @@ export function textBaseStyle(
52
52
  font-weight: ${weights.bold};
53
53
  }
54
54
 
55
+ & svg {
56
+ /* Certain popular CSS libraries changes the defaults for SVG display */
57
+ /* Make sure SVGs are rendered as inline elements */
58
+ display: inline;
59
+ }
60
+
55
61
  & [data-sanity-icon] {
56
62
  vertical-align: baseline;
57
63
  }
@@ -11,7 +11,7 @@ export const defaultContextValue: PortalContextValue = {
11
11
  version: 0.0,
12
12
  boundaryElement: null,
13
13
  get element() {
14
- if (typeof window === 'undefined') {
14
+ if (typeof document === 'undefined') {
15
15
  return null
16
16
  }
17
17
 
@@ -19,7 +19,7 @@ export interface PortalProviderProps {
19
19
  __unstable_elements?: Record<string, HTMLElement | null | undefined>
20
20
  }
21
21
 
22
- const __BROWSER__ = typeof window !== 'undefined'
22
+ const __BROWSER__ = typeof document !== 'undefined'
23
23
 
24
24
  /**
25
25
  * @public