@sanity/ui 2.6.5 → 2.6.7-canary.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.d.mts +42 -6
- package/dist/index.d.ts +42 -6
- package/dist/index.esm.js +80 -147
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +79 -146
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +80 -147
- package/dist/index.mjs.map +1 -1
- package/package.json +26 -34
- package/src/core/components/breadcrumbs/breadcrumbs.tsx +15 -6
- package/src/core/components/dialog/dialog.tsx +12 -21
- package/src/core/components/menu/menu.tsx +11 -18
- package/src/core/components/menu/menuButton.tsx +1 -1
- package/src/core/components/menu/menuContext.ts +1 -1
- package/src/core/components/menu/useMenuController.ts +11 -17
- package/src/core/components/toast/styles.ts +2 -1
- package/src/core/components/toast/useToast.ts +1 -0
- package/src/core/components/tree/tree.tsx +1 -0
- package/src/core/components/tree/treeItem.tsx +1 -0
- package/src/core/helpers/focus.ts +1 -0
- package/src/core/helpers/scroll.ts +1 -0
- package/src/core/hooks/index.ts +3 -2
- package/src/core/hooks/useClickOutside.ts +38 -72
- package/src/core/hooks/useElementSize.ts +1 -0
- package/src/core/hooks/useMatchMedia.ts +46 -0
- package/src/core/hooks/useMediaIndex/useMediaIndex.ts +2 -10
- package/src/core/hooks/usePrefersDark.ts +10 -55
- package/src/core/hooks/usePrefersReducedMotion.ts +10 -56
- package/src/core/primitives/popover/__workshop__/AlignedStory.tsx +9 -7
- package/src/core/primitives/tooltip/__workshop__/customPortal.tsx +8 -6
- package/src/core/primitives/tooltip/tooltip.tsx +7 -5
- package/src/core/primitives/tooltip/tooltipDelayGroup/tooltipDelayGroupProvider.tsx +1 -0
- package/src/core/utils/layer/layerProvider.tsx +1 -0
- package/src/core/utils/portal/__workshop__/named.tsx +10 -8
- package/src/core/utils/portal/portalProvider.tsx +2 -3
- package/src/core/hooks/_internal/index.ts +0 -1
- package/src/core/hooks/_internal/useUnique.ts +0 -34
|
@@ -1,61 +1,16 @@
|
|
|
1
|
-
import {
|
|
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-color-scheme: dark)')
|
|
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
|
-
}
|
|
1
|
+
import {useMatchMedia} from './useMatchMedia'
|
|
44
2
|
|
|
45
3
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* and we assume `(prefers-color-scheme: light)` since it's the most common scheme
|
|
4
|
+
* Returns true if a dark color scheme is preferred, false if a light color scheme is preferred or the preference is not known.
|
|
5
|
+
*
|
|
6
|
+
* @param getServerSnapshot - Only called during server-side rendering, and hydration if using hydrateRoot. Since the server environment doesn't have access to the DOM, we can't determine the current value of the media query and we assume `(prefers-color-scheme: light)` since it's the most common scheme (https://react.dev/reference/react/useSyncExternalStore#adding-support-for-server-rendering)
|
|
7
|
+
*
|
|
8
|
+
* If you persist the detected preference in a cookie or a header then you may implement your own server snapshot to read it.
|
|
9
|
+
* Chrome supports reading the `prefers-color-scheme` media query from a header if the server response: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-Prefers-Color-Scheme
|
|
10
|
+
* @example https://gist.github.com/stipsan/13c0cccf8dfc34f4b44bb1b984baf7df
|
|
49
11
|
*
|
|
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
12
|
* @public
|
|
58
13
|
*/
|
|
59
|
-
export function usePrefersDark(): boolean {
|
|
60
|
-
return
|
|
14
|
+
export function usePrefersDark(getServerSnapshot = () => false): boolean {
|
|
15
|
+
return useMatchMedia('(prefers-color-scheme: dark)', getServerSnapshot)
|
|
61
16
|
}
|
|
@@ -1,62 +1,16 @@
|
|
|
1
|
-
import {
|
|
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
|
-
}
|
|
1
|
+
import {useMatchMedia} from './useMatchMedia'
|
|
55
2
|
|
|
56
3
|
/**
|
|
57
4
|
* Returns true if motion should be reduced
|
|
5
|
+
*
|
|
6
|
+
* @param getServerSnapshot - Only called during server-side rendering, and hydration if using hydrateRoot. Since the server environment doesn't have access to the DOM, we can't determine the current value of the media query and we assume `(prefers-reduced-motion: no-preference)` since it's the most common scheme (https://react.dev/reference/react/useSyncExternalStore#adding-support-for-server-rendering)
|
|
7
|
+
*
|
|
8
|
+
* If you persist the detected preference in a cookie or a header then you may implement your own server snapshot to read it.
|
|
9
|
+
* Chrome supports reading the `prefers-reduced-motion` media query from a header if the server response: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-Prefers-Reduced-Motion
|
|
10
|
+
* @example https://gist.github.com/stipsan/0c0f839a27842249cada893e9fb7767b
|
|
11
|
+
*
|
|
58
12
|
* @public
|
|
59
13
|
*/
|
|
60
|
-
export function usePrefersReducedMotion(): boolean {
|
|
61
|
-
return
|
|
14
|
+
export function usePrefersReducedMotion(getServerSnapshot = () => false): boolean {
|
|
15
|
+
return useMatchMedia('(prefers-reduced-motion: reduce)', getServerSnapshot)
|
|
62
16
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {EllipsisVerticalIcon} from '@sanity/icons'
|
|
2
2
|
import {Button, Card, Flex, Popover, Text, useClickOutside} from '@sanity/ui'
|
|
3
3
|
import {useBoolean, useSelect} from '@sanity/ui-workshop'
|
|
4
|
-
import {useCallback, useState} from 'react'
|
|
4
|
+
import {useCallback, useRef, useState} from 'react'
|
|
5
5
|
import {
|
|
6
6
|
WORKSHOP_FLEX_ALIGN_OPTIONS,
|
|
7
7
|
WORKSHOP_FLEX_JUSTIFY_OPTIONS,
|
|
@@ -20,8 +20,8 @@ export default function AlignedStory() {
|
|
|
20
20
|
|
|
21
21
|
const [open, setOpen] = useState(false)
|
|
22
22
|
const [boundaryElement, setBoundaryElement] = useState<HTMLDivElement | null>(null)
|
|
23
|
-
const
|
|
24
|
-
const
|
|
23
|
+
const buttonElementRef = useRef<HTMLButtonElement | null>(null)
|
|
24
|
+
const popoverElementRef = useRef<HTMLDivElement | null>(null)
|
|
25
25
|
|
|
26
26
|
const content = (
|
|
27
27
|
<Text>
|
|
@@ -39,9 +39,11 @@ export default function AlignedStory() {
|
|
|
39
39
|
)
|
|
40
40
|
|
|
41
41
|
const handleToggleOpen = useCallback(() => setOpen((v) => !v), [])
|
|
42
|
-
const handleClose = useCallback(() => setOpen(false), [])
|
|
43
42
|
|
|
44
|
-
useClickOutside(
|
|
43
|
+
useClickOutside(
|
|
44
|
+
() => setOpen(false),
|
|
45
|
+
() => [buttonElementRef.current, popoverElementRef.current],
|
|
46
|
+
)
|
|
45
47
|
|
|
46
48
|
return (
|
|
47
49
|
<Card height="fill" padding={[4, 5, 6]} sizing="border" tone="transparent">
|
|
@@ -56,14 +58,14 @@ export default function AlignedStory() {
|
|
|
56
58
|
padding={3}
|
|
57
59
|
portal={portal}
|
|
58
60
|
placement={placement}
|
|
59
|
-
ref={
|
|
61
|
+
ref={popoverElementRef}
|
|
60
62
|
width={width}
|
|
61
63
|
>
|
|
62
64
|
<Button
|
|
63
65
|
icon={EllipsisVerticalIcon}
|
|
64
66
|
mode="bleed"
|
|
65
67
|
onClick={handleToggleOpen}
|
|
66
|
-
ref={
|
|
68
|
+
ref={buttonElementRef}
|
|
67
69
|
selected={open}
|
|
68
70
|
/>
|
|
69
71
|
</Popover>
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
Tooltip,
|
|
11
11
|
} from '@sanity/ui'
|
|
12
12
|
import {useBoolean, useSelect, useText} from '@sanity/ui-workshop'
|
|
13
|
-
import {useState} from 'react'
|
|
13
|
+
import {useMemo, useState} from 'react'
|
|
14
14
|
import {WORKSHOP_PLACEMENT_OPTIONS} from '../../../__workshop__/constants'
|
|
15
15
|
|
|
16
16
|
const PORTAL_OPTIONS = {
|
|
@@ -30,13 +30,15 @@ export default function CustomPortalStory() {
|
|
|
30
30
|
|
|
31
31
|
const [portal1Element, setPortal1Element] = useState<HTMLDivElement | null>(null)
|
|
32
32
|
const [boundaryElement, setBoundaryElement] = useState<HTMLDivElement | null>(null)
|
|
33
|
+
const __unstable_elements = useMemo(
|
|
34
|
+
() => ({
|
|
35
|
+
portal1: portal1Element,
|
|
36
|
+
}),
|
|
37
|
+
[portal1Element],
|
|
38
|
+
)
|
|
33
39
|
|
|
34
40
|
return (
|
|
35
|
-
<PortalProvider
|
|
36
|
-
__unstable_elements={{
|
|
37
|
-
portal1: portal1Element,
|
|
38
|
-
}}
|
|
39
|
-
>
|
|
41
|
+
<PortalProvider __unstable_elements={__unstable_elements}>
|
|
40
42
|
<Flex align="center" height="fill" justify="center">
|
|
41
43
|
<BoundaryElementProvider element={useBoundaryElement ? boundaryElement : null}>
|
|
42
44
|
<Card
|
|
@@ -268,6 +268,9 @@ export const Tooltip = forwardRef(function Tooltip(
|
|
|
268
268
|
[childProp?.props, handleIsOpenChange],
|
|
269
269
|
)
|
|
270
270
|
|
|
271
|
+
// Handle closing the tooltip when the mouse leaves the referenceElement
|
|
272
|
+
useCloseOnMouseLeave({handleIsOpenChange, referenceElement, showTooltip})
|
|
273
|
+
|
|
271
274
|
// Close when `disabled` changes to `true`
|
|
272
275
|
useEffect(() => {
|
|
273
276
|
if (disabled && showTooltip) handleIsOpenChange(false)
|
|
@@ -298,9 +301,6 @@ export const Tooltip = forwardRef(function Tooltip(
|
|
|
298
301
|
}
|
|
299
302
|
}, [handleIsOpenChange, showTooltip])
|
|
300
303
|
|
|
301
|
-
// Handle closing the tooltip when the mouse leaves the referenceElement
|
|
302
|
-
useCloseOnMouseLeave({handleIsOpenChange, referenceElement, showTooltip})
|
|
303
|
-
|
|
304
304
|
// // Set the max width of the tooltip based on boundaries and portals
|
|
305
305
|
useLayoutEffect(() => {
|
|
306
306
|
// Get the maximum tooltip width (sans tooltip padding)
|
|
@@ -446,7 +446,7 @@ function useCloseOnMouseLeave({
|
|
|
446
446
|
// Since we don't want the `mouseevent` events to be attached and removed if the `referenceElement` is changed
|
|
447
447
|
// we use a "effect event" (https://19.react.dev/learn/separating-events-from-effects#reading-latest-props-and-state-with-effect-events)
|
|
448
448
|
// in order to always see the latest `referenceElement` value inside the event handler itself.
|
|
449
|
-
const onMouseMove = useEffectEvent((target: EventTarget | null) => {
|
|
449
|
+
const onMouseMove = useEffectEvent((target: EventTarget | null, teardown: () => void) => {
|
|
450
450
|
if (!referenceElement) return
|
|
451
451
|
|
|
452
452
|
const isHoveringReference =
|
|
@@ -454,6 +454,8 @@ function useCloseOnMouseLeave({
|
|
|
454
454
|
|
|
455
455
|
if (!isHoveringReference) {
|
|
456
456
|
handleIsOpenChange(false)
|
|
457
|
+
// Allow removing the event listener eagerly, to avoid race conditions
|
|
458
|
+
teardown()
|
|
457
459
|
}
|
|
458
460
|
})
|
|
459
461
|
|
|
@@ -464,7 +466,7 @@ function useCloseOnMouseLeave({
|
|
|
464
466
|
if (!showTooltip) return
|
|
465
467
|
|
|
466
468
|
const handleMouseMove = (event: MouseEvent) => {
|
|
467
|
-
onMouseMove(event.target)
|
|
469
|
+
onMouseMove(event.target, () => window.removeEventListener('mousemove', handleMouseMove))
|
|
468
470
|
}
|
|
469
471
|
|
|
470
472
|
window.addEventListener('mousemove', handleMouseMove)
|
|
@@ -36,6 +36,7 @@ export function TooltipDelayGroupProvider(
|
|
|
36
36
|
const openDelay = typeof delay === 'number' ? delay : delay?.open || 0
|
|
37
37
|
const closeDelay = typeof delay === 'number' ? delay : delay?.close || 0
|
|
38
38
|
|
|
39
|
+
// @TODO split out into separate contexts
|
|
39
40
|
const value: TooltipDelayGroupContextValue = useMemo(
|
|
40
41
|
() => ({
|
|
41
42
|
isGroupActive: isGroupActive,
|
|
@@ -91,6 +91,7 @@ export function LayerProvider(props: LayerProviderProps): React.ReactElement {
|
|
|
91
91
|
// Register this layer on mount
|
|
92
92
|
useEffect(() => parentRegisterChild?.(level), [level, parentRegisterChild])
|
|
93
93
|
|
|
94
|
+
// @TODO split out into separate contexts
|
|
94
95
|
const value: LayerContextValue = useMemo(
|
|
95
96
|
() => ({
|
|
96
97
|
version: 0.0,
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import {Card, Container, Portal, PortalProvider, Stack, Text} from '@sanity/ui'
|
|
2
|
-
import {useState} from 'react'
|
|
2
|
+
import {useMemo, useState} from 'react'
|
|
3
3
|
|
|
4
4
|
export default function NamedStory() {
|
|
5
5
|
const [portal1Element, setPortal1Element] = useState<HTMLDivElement | null>(null)
|
|
6
6
|
const [portal2Element, setPortal2Element] = useState<HTMLDivElement | null>(null)
|
|
7
7
|
const [portal3Element, setPortal3Element] = useState<HTMLDivElement | null>(null)
|
|
8
|
+
const __unstable_elements = useMemo(
|
|
9
|
+
() => ({
|
|
10
|
+
portal1: portal1Element,
|
|
11
|
+
portal2: portal2Element,
|
|
12
|
+
portal3: portal3Element,
|
|
13
|
+
}),
|
|
14
|
+
[portal1Element, portal2Element, portal3Element],
|
|
15
|
+
)
|
|
8
16
|
|
|
9
17
|
return (
|
|
10
|
-
<PortalProvider
|
|
11
|
-
__unstable_elements={{
|
|
12
|
-
portal1: portal1Element,
|
|
13
|
-
portal2: portal2Element,
|
|
14
|
-
portal3: portal3Element,
|
|
15
|
-
}}
|
|
16
|
-
>
|
|
18
|
+
<PortalProvider __unstable_elements={__unstable_elements}>
|
|
17
19
|
<Container width={1}>
|
|
18
20
|
<Card height="fill" padding={4}>
|
|
19
21
|
<Stack space={2}>
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {useMemo} from 'react'
|
|
2
|
-
import {useUnique} from '../../hooks/_internal'
|
|
3
2
|
import {useMounted} from '../../hooks/useMounted'
|
|
4
3
|
import {PortalContext} from './portalContext'
|
|
5
4
|
import {PortalContextValue} from './types'
|
|
@@ -24,10 +23,10 @@ export interface PortalProviderProps {
|
|
|
24
23
|
* @public
|
|
25
24
|
*/
|
|
26
25
|
export function PortalProvider(props: PortalProviderProps): React.ReactElement {
|
|
27
|
-
const {boundaryElement, children, element, __unstable_elements:
|
|
28
|
-
const elements = useUnique(elementsProp)
|
|
26
|
+
const {boundaryElement, children, element, __unstable_elements: elements} = props
|
|
29
27
|
const mounted = useMounted()
|
|
30
28
|
|
|
29
|
+
// @TODO split out into separate contexts
|
|
31
30
|
const value: PortalContextValue = useMemo(() => {
|
|
32
31
|
return {
|
|
33
32
|
version: 0.0,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './useUnique'
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import {useRef} from 'react'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* This is a React hook to make sure that a record identity is the same on every render. Uses strict
|
|
5
|
-
* equality comparison (eg by identity), and only goes one level deep.
|
|
6
|
-
*
|
|
7
|
-
* @internal
|
|
8
|
-
*/
|
|
9
|
-
type Comparable = Record<string | number | symbol, unknown> | undefined | null
|
|
10
|
-
|
|
11
|
-
export function useUnique<ValueType extends Comparable = Comparable>(value: ValueType): ValueType {
|
|
12
|
-
const valueRef = useRef<ValueType>(value)
|
|
13
|
-
|
|
14
|
-
if (!_isEqual(valueRef.current, value)) {
|
|
15
|
-
valueRef.current = value
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
return valueRef.current
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function _isEqual(objA: Comparable, objB: Comparable): boolean {
|
|
22
|
-
if (!objA || !objB) {
|
|
23
|
-
return objA === objB
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const keysA = Object.keys(objA)
|
|
27
|
-
const keysB = Object.keys(objB)
|
|
28
|
-
|
|
29
|
-
if (keysA.length !== keysB.length) {
|
|
30
|
-
return false
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return keysA.every((key) => objA[key] === objB[key])
|
|
34
|
-
}
|