@edgeandnode/eds-react 1.1.1 → 1.2.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/README.md +27 -3
- package/dist/components/Address.d.ts.map +1 -1
- package/dist/components/Address.js +1 -1
- package/dist/components/Address.js.map +1 -1
- package/dist/components/Avatar.d.ts.map +1 -1
- package/dist/components/Avatar.js +13 -1
- package/dist/components/Avatar.js.map +1 -1
- package/dist/components/Breadcrumbs.parts.d.ts.map +1 -1
- package/dist/components/Breadcrumbs.parts.js +1 -1
- package/dist/components/Breadcrumbs.parts.js.map +1 -1
- package/dist/components/Button.js +1 -1
- package/dist/components/Button.js.map +1 -1
- package/dist/components/Card.js +1 -1
- package/dist/components/Card.js.map +1 -1
- package/dist/components/Chip.parts.d.ts.map +1 -1
- package/dist/components/Chip.parts.js +1 -1
- package/dist/components/Chip.parts.js.map +1 -1
- package/dist/components/Icon.d.ts.map +1 -1
- package/dist/components/Icon.js +0 -2
- package/dist/components/Icon.js.map +1 -1
- package/dist/components/Link.d.ts.map +1 -1
- package/dist/components/Link.js +2 -0
- package/dist/components/Link.js.map +1 -1
- package/dist/components/Menu.parts.js +1 -1
- package/dist/components/Menu.parts.js.map +1 -1
- package/dist/components/Search.d.ts.map +1 -1
- package/dist/components/Search.js +1 -1
- package/dist/components/Search.js.map +1 -1
- package/dist/components/SegmentedControl.parts.d.ts.map +1 -1
- package/dist/components/SegmentedControl.parts.js +1 -1
- package/dist/components/SegmentedControl.parts.js.map +1 -1
- package/dist/components/TabSet.parts.js +1 -1
- package/dist/components/TabSet.parts.js.map +1 -1
- package/dist/components/Tooltip.parts.d.ts.map +1 -1
- package/dist/components/Tooltip.parts.js.map +1 -1
- package/dist/components/base/Checkable.parts.js +1 -1
- package/dist/components/base/Checkable.parts.js.map +1 -1
- package/dist/components/base/Field.parts.js +1 -1
- package/dist/components/base/Field.parts.js.map +1 -1
- package/dist/components/base/Portal.js +3 -3
- package/dist/components/base/Portal.js.map +1 -1
- package/dist/components/base/Transition.js +1 -1
- package/dist/components/base/Transition.js.map +1 -1
- package/dist/hooks/useCSSPropsPolyfill.d.ts.map +1 -1
- package/dist/hooks/useCSSPropsPolyfill.js +7 -19
- package/dist/hooks/useCSSPropsPolyfill.js.map +1 -1
- package/dist/hooks/useCSSState.d.ts +4 -0
- package/dist/hooks/useCSSState.d.ts.map +1 -1
- package/dist/hooks/useCSSState.js +24 -6
- package/dist/hooks/useCSSState.js.map +1 -1
- package/dist/hooks/useStyleObserver.d.ts.map +1 -1
- package/dist/hooks/useStyleObserver.js +9 -5
- package/dist/hooks/useStyleObserver.js.map +1 -1
- package/dist/postcss-plugin.d.ts +2 -0
- package/dist/postcss-plugin.d.ts.map +1 -0
- package/dist/postcss-plugin.js +2 -0
- package/dist/postcss-plugin.js.map +1 -0
- package/dist/utils/internal/BatchedElementStyleObserver.d.ts +13 -0
- package/dist/utils/internal/BatchedElementStyleObserver.d.ts.map +1 -0
- package/dist/utils/internal/BatchedElementStyleObserver.js +28 -0
- package/dist/utils/internal/BatchedElementStyleObserver.js.map +1 -0
- package/dist/utils/internal/getStyleQueriesSupported.d.ts +2 -0
- package/dist/utils/internal/getStyleQueriesSupported.d.ts.map +1 -0
- package/dist/utils/internal/getStyleQueriesSupported.js +12 -0
- package/dist/utils/internal/getStyleQueriesSupported.js.map +1 -0
- package/dist/utils/internal/index.d.ts +2 -0
- package/dist/utils/internal/index.d.ts.map +1 -1
- package/dist/utils/internal/index.js +2 -0
- package/dist/utils/internal/index.js.map +1 -1
- package/dist/utils/internal/styleScheduler.d.ts +8 -3
- package/dist/utils/internal/styleScheduler.d.ts.map +1 -1
- package/dist/utils/internal/styleScheduler.js +46 -16
- package/dist/utils/internal/styleScheduler.js.map +1 -1
- package/dist/utils/styleObserver.d.ts +2 -0
- package/dist/utils/styleObserver.d.ts.map +1 -1
- package/dist/utils/styleObserver.js +40 -33
- package/dist/utils/styleObserver.js.map +1 -1
- package/dist/vite-plugin.d.ts +2 -0
- package/dist/vite-plugin.d.ts.map +1 -0
- package/dist/vite-plugin.js +2 -0
- package/dist/vite-plugin.js.map +1 -0
- package/package.json +13 -11
- package/scripts/update-skill.cjs +42 -0
- package/skill/SKILL.md +16 -12
- package/src/components/Address.tsx +9 -6
- package/src/components/Avatar.tsx +58 -44
- package/src/components/Breadcrumbs.parts.tsx +8 -5
- package/src/components/Button.tsx +1 -1
- package/src/components/Card.tsx +1 -1
- package/src/components/Chip.parts.tsx +9 -6
- package/src/components/Icon.tsx +0 -2
- package/src/components/Link.tsx +2 -0
- package/src/components/Menu.parts.tsx +1 -1
- package/src/components/Search.tsx +9 -6
- package/src/components/SegmentedControl.parts.tsx +8 -5
- package/src/components/TabSet.parts.tsx +1 -1
- package/src/components/Tooltip.parts.tsx +4 -1
- package/src/components/base/Checkable.parts.tsx +1 -1
- package/src/components/base/Field.parts.tsx +1 -1
- package/src/components/base/Portal.tsx +3 -3
- package/src/components/base/Transition.tsx +1 -1
- package/src/hooks/useCSSPropsPolyfill.ts +7 -20
- package/src/hooks/useCSSState.ts +29 -8
- package/src/hooks/useStyleObserver.ts +18 -14
- package/src/postcss-plugin.ts +1 -0
- package/src/utils/internal/BatchedElementStyleObserver.ts +31 -0
- package/src/utils/internal/getStyleQueriesSupported.ts +11 -0
- package/src/utils/internal/index.ts +2 -0
- package/src/utils/internal/styleScheduler.ts +43 -15
- package/src/utils/styleObserver.ts +38 -40
- package/src/vite-plugin.ts +1 -0
|
@@ -13,6 +13,7 @@ import { cn, getAssetUrl, getCSSPropsAttributes } from '../utils/index.ts'
|
|
|
13
13
|
import { getTooltipProps } from '../utils/internal/index.ts'
|
|
14
14
|
import { AvatarMeta } from './Avatar.meta.ts'
|
|
15
15
|
import { MaybeButtonOrLink, type MaybeButtonOrLinkProps } from './base/MaybeButtonOrLink.tsx'
|
|
16
|
+
import { Render } from './base/Render.tsx'
|
|
16
17
|
import { Tooltip, type TooltipProps } from './Tooltip.tsx'
|
|
17
18
|
|
|
18
19
|
const PLACEHOLDER_SRC_CIRCLE = createIdenticon(PLACEHOLDER_ADDRESS)
|
|
@@ -94,55 +95,68 @@ export function Avatar({
|
|
|
94
95
|
props,
|
|
95
96
|
)}
|
|
96
97
|
render={(renderProps, { Element, elementProps, category }) => {
|
|
98
|
+
const avatar = (
|
|
99
|
+
<Element {...elementProps}>
|
|
100
|
+
<span className="nested/avatar-paint pointer-events-none shrink-0">
|
|
101
|
+
<span
|
|
102
|
+
className="
|
|
103
|
+
block overflow-clip bg-default
|
|
104
|
+
group-clickable-focus-visible/avatar:outline
|
|
105
|
+
@prop-size-xsmall/avatar:size-4
|
|
106
|
+
@prop-size-small/avatar:size-5
|
|
107
|
+
@prop-size-medium/avatar:size-6
|
|
108
|
+
@prop-size-large/avatar:size-8
|
|
109
|
+
@prop-size-xlarge/avatar:size-12
|
|
110
|
+
@prop-shape-circle/avatar:rounded-full
|
|
111
|
+
@prop-shape-rounded/avatar:rounded-4
|
|
112
|
+
@prop-shape-rounded/avatar:@prop-size-xsmall/avatar:rounded-2
|
|
113
|
+
@prop-shape-rounded/avatar:@prop-size-xlarge/avatar:rounded-8
|
|
114
|
+
uu:*:absolute
|
|
115
|
+
uu:*:inset-0
|
|
116
|
+
uu:*:m-auto
|
|
117
|
+
uu:*:size-full
|
|
118
|
+
uu:*:object-cover
|
|
119
|
+
i:@prop-size-xsmall/avatar:**:icon:default-size-2.5
|
|
120
|
+
i:@prop-size-small/avatar:**:icon:default-size-3
|
|
121
|
+
i:@prop-size-medium/avatar:**:icon:default-size-4
|
|
122
|
+
i:@prop-size-large/avatar:**:icon:default-size-5
|
|
123
|
+
i:@prop-size-xlarge/avatar:**:icon:default-size-8
|
|
124
|
+
"
|
|
125
|
+
>
|
|
126
|
+
{src ? (
|
|
127
|
+
typeof src === 'string' ? (
|
|
128
|
+
<img
|
|
129
|
+
src={src}
|
|
130
|
+
alt=""
|
|
131
|
+
onError={() => setCurrentSourceIndex(currentSourceIndex + 1)}
|
|
132
|
+
className="[[src^='data:image/bmp']]:[image-rendering:pixelated]"
|
|
133
|
+
/>
|
|
134
|
+
) : (
|
|
135
|
+
<Tooltip.Collector>{src}</Tooltip.Collector>
|
|
136
|
+
)
|
|
137
|
+
) : null}
|
|
138
|
+
{alt ? <span className="sr-only">{alt}</span> : null}
|
|
139
|
+
</span>
|
|
140
|
+
</span>
|
|
141
|
+
</Element>
|
|
142
|
+
)
|
|
143
|
+
const clickable = category !== 'other'
|
|
144
|
+
const focusable = clickable || props.as !== undefined || props.tabIndex !== undefined
|
|
145
|
+
/**
|
|
146
|
+
* Without any content to show (or collect from `src`), a `Tooltip` would only cost its
|
|
147
|
+
* trigger and observers. Avatars that can take focus keep it anyway, since adding or
|
|
148
|
+
* removing it remounts the element, which would lose focus.
|
|
149
|
+
*/
|
|
150
|
+
if (!focusable && tooltip === undefined && typeof src === 'string') {
|
|
151
|
+
return <Render render={avatar} {...renderProps} />
|
|
152
|
+
}
|
|
97
153
|
return (
|
|
98
154
|
<Tooltip
|
|
99
|
-
content={
|
|
155
|
+
content={clickable ? alt : undefined}
|
|
100
156
|
{...getTooltipProps(tooltip)}
|
|
101
157
|
triggerProps={renderProps}
|
|
102
158
|
>
|
|
103
|
-
|
|
104
|
-
<span className="nested/avatar-paint pointer-events-none shrink-0">
|
|
105
|
-
<span
|
|
106
|
-
className="
|
|
107
|
-
block overflow-clip bg-default
|
|
108
|
-
group-clickable-focus-visible/avatar:outline
|
|
109
|
-
@prop-size-xsmall/avatar:size-4
|
|
110
|
-
@prop-size-small/avatar:size-5
|
|
111
|
-
@prop-size-medium/avatar:size-6
|
|
112
|
-
@prop-size-large/avatar:size-8
|
|
113
|
-
@prop-size-xlarge/avatar:size-12
|
|
114
|
-
@prop-shape-circle/avatar:rounded-full
|
|
115
|
-
@prop-shape-rounded/avatar:rounded-4
|
|
116
|
-
@prop-shape-rounded/avatar:@prop-size-xsmall/avatar:rounded-2
|
|
117
|
-
@prop-shape-rounded/avatar:@prop-size-xlarge/avatar:rounded-8
|
|
118
|
-
uu:*:absolute
|
|
119
|
-
uu:*:inset-0
|
|
120
|
-
uu:*:m-auto
|
|
121
|
-
uu:*:size-full
|
|
122
|
-
uu:*:object-cover
|
|
123
|
-
i:@prop-size-xsmall/avatar:**:icon:default-size-2.5
|
|
124
|
-
i:@prop-size-small/avatar:**:icon:default-size-3
|
|
125
|
-
i:@prop-size-medium/avatar:**:icon:default-size-4
|
|
126
|
-
i:@prop-size-large/avatar:**:icon:default-size-5
|
|
127
|
-
i:@prop-size-xlarge/avatar:**:icon:default-size-8
|
|
128
|
-
"
|
|
129
|
-
>
|
|
130
|
-
{src ? (
|
|
131
|
-
typeof src === 'string' ? (
|
|
132
|
-
<img
|
|
133
|
-
src={src}
|
|
134
|
-
alt=""
|
|
135
|
-
onError={() => setCurrentSourceIndex(currentSourceIndex + 1)}
|
|
136
|
-
className="[[src^='data:image/bmp']]:[image-rendering:pixelated]"
|
|
137
|
-
/>
|
|
138
|
-
) : (
|
|
139
|
-
<Tooltip.Collector>{src}</Tooltip.Collector>
|
|
140
|
-
)
|
|
141
|
-
) : null}
|
|
142
|
-
{alt ? <span className="sr-only">{alt}</span> : null}
|
|
143
|
-
</span>
|
|
144
|
-
</span>
|
|
145
|
-
</Element>
|
|
159
|
+
{avatar}
|
|
146
160
|
</Tooltip>
|
|
147
161
|
)
|
|
148
162
|
}}
|
|
@@ -290,11 +290,14 @@ export function BreadcrumbsItem({
|
|
|
290
290
|
const current = passedCurrent ?? isLastItem
|
|
291
291
|
const { rootProps, nestedProps } = splitProps(props)
|
|
292
292
|
|
|
293
|
-
const [stateRef, state] = useCSSState(
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
293
|
+
const [stateRef, state] = useCSSState(
|
|
294
|
+
{
|
|
295
|
+
pointer: undefined,
|
|
296
|
+
focus: undefined,
|
|
297
|
+
current,
|
|
298
|
+
},
|
|
299
|
+
{ returnState: false },
|
|
300
|
+
)
|
|
298
301
|
const [cssPropsPolyfillStatePassedRef, cssPropsPolyfillAttributes] = useCSSPropsPolyfill(
|
|
299
302
|
BreadcrumbsItemMeta,
|
|
300
303
|
{ hideLabel },
|
|
@@ -76,7 +76,7 @@ export function Button({
|
|
|
76
76
|
disabled: Boolean(disabled),
|
|
77
77
|
'addon-only': !children ? 'auto' : '0',
|
|
78
78
|
},
|
|
79
|
-
{ ref: passedRef },
|
|
79
|
+
{ ref: passedRef, returnState: false, exposeState: false },
|
|
80
80
|
)
|
|
81
81
|
const [cssPropsPolyfillStatePassedRef, cssPropsPolyfillAttributes, cssProps] =
|
|
82
82
|
useCSSPropsPolyfill(
|
package/src/components/Card.tsx
CHANGED
|
@@ -72,7 +72,7 @@ export function Card({ ref: passedRef, as, variant, disabled, children, ...props
|
|
|
72
72
|
checked: props.checked,
|
|
73
73
|
disabled: Boolean(disabled),
|
|
74
74
|
},
|
|
75
|
-
{ ref: passedRef },
|
|
75
|
+
{ ref: passedRef, returnState: false },
|
|
76
76
|
)
|
|
77
77
|
const [cssPropsPolyfillStatePassedRef, cssPropsPolyfillAttributes] = useCSSPropsPolyfill(
|
|
78
78
|
CardMeta,
|
|
@@ -65,12 +65,15 @@ export function ChipRoot({
|
|
|
65
65
|
|
|
66
66
|
const groupContext = useContext(ChipGroupContext)
|
|
67
67
|
|
|
68
|
-
const [stateRef, state] = useCSSState(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
const [stateRef, state] = useCSSState(
|
|
69
|
+
{
|
|
70
|
+
pointer: undefined,
|
|
71
|
+
focus: undefined,
|
|
72
|
+
checked: undefined,
|
|
73
|
+
disabled: Boolean(disabled),
|
|
74
|
+
},
|
|
75
|
+
{ returnState: false },
|
|
76
|
+
)
|
|
74
77
|
const [cssPropsPolyfillStateRef, cssPropsPolyfillAttributes] = useCSSPropsPolyfill(
|
|
75
78
|
ChipMeta,
|
|
76
79
|
{ size },
|
package/src/components/Icon.tsx
CHANGED
|
@@ -54,8 +54,6 @@ export function Icon({ src, alt, size, color, ...props }: IconPropsWithSrc) {
|
|
|
54
54
|
{
|
|
55
55
|
className: cn(`
|
|
56
56
|
eds-icon root-flex items-center justify-center
|
|
57
|
-
${/* Fix icons wobbling in Safari when rendered on top of opacity/background transitions */ ''}
|
|
58
|
-
u:will-change-transform
|
|
59
57
|
${/* Flip some icons horizontally in RTL so that `ArrowLeft` points right, etc. */ ''}
|
|
60
58
|
rtl:data-[name*=Left]:*:-scale-x-100
|
|
61
59
|
rtl:data-[name*=Right]:*:-scale-x-100
|
package/src/components/Link.tsx
CHANGED
|
@@ -1015,7 +1015,7 @@ export function MenuItem<T extends OptionValue>({
|
|
|
1015
1015
|
checked,
|
|
1016
1016
|
disabled,
|
|
1017
1017
|
},
|
|
1018
|
-
{ ref: elementPassedRef },
|
|
1018
|
+
{ ref: elementPassedRef, returnState: false, exposeState: false },
|
|
1019
1019
|
)
|
|
1020
1020
|
|
|
1021
1021
|
const groupContext = useContext(MenuGroupContext)
|
|
@@ -54,12 +54,15 @@ export function Search({
|
|
|
54
54
|
const previousActiveElementRef = useRef<HTMLElement | null>(null)
|
|
55
55
|
const [value, setValue] = useControlled(controlledValue, defaultValue ?? '', onValueChange)
|
|
56
56
|
|
|
57
|
-
const [stateRef, state] = useCSSState(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
const [stateRef, state] = useCSSState(
|
|
58
|
+
{
|
|
59
|
+
pointer: undefined,
|
|
60
|
+
focus: undefined,
|
|
61
|
+
blank: value === '',
|
|
62
|
+
open: value !== '',
|
|
63
|
+
},
|
|
64
|
+
{ returnState: false },
|
|
65
|
+
)
|
|
63
66
|
const [cssPropsPolyfillStateRef, cssPropsPolyfillAttributes, cssProps] = useCSSPropsPolyfill(
|
|
64
67
|
SearchMeta,
|
|
65
68
|
{ size, layout },
|
|
@@ -152,11 +152,14 @@ export function SegmentedControlOption<T extends OptionValue>({
|
|
|
152
152
|
const autoValue = useAutoValue(children)
|
|
153
153
|
const value = passedValue !== undefined ? passedValue : autoValue
|
|
154
154
|
|
|
155
|
-
const [stateRef, state] = useCSSState(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
155
|
+
const [stateRef, state] = useCSSState(
|
|
156
|
+
{
|
|
157
|
+
pointer: undefined,
|
|
158
|
+
checked: undefined,
|
|
159
|
+
'is-toggle': undefined,
|
|
160
|
+
},
|
|
161
|
+
{ returnState: false },
|
|
162
|
+
)
|
|
160
163
|
const [cssPropsPolyfillStatePassedRef, cssPropsPolyfillAttributes] = useCSSPropsPolyfill(
|
|
161
164
|
SegmentedControlOptionMeta,
|
|
162
165
|
{ hideLabel },
|
|
@@ -203,7 +203,7 @@ export function TabSetTab({
|
|
|
203
203
|
checked: selected,
|
|
204
204
|
disabled: Boolean(disabled),
|
|
205
205
|
},
|
|
206
|
-
{ ref: passedRef },
|
|
206
|
+
{ ref: passedRef, returnState: false, exposeState: false },
|
|
207
207
|
)
|
|
208
208
|
const [cssPropsPolyfillStatePassedRef, cssPropsPolyfillAttributes] = useCSSPropsPolyfill(
|
|
209
209
|
TabSetTabMeta,
|
|
@@ -176,7 +176,10 @@ export function TooltipRoot({
|
|
|
176
176
|
<Render render={children} {...renderProps} disabled={triggerDisabled} />
|
|
177
177
|
)}
|
|
178
178
|
/>
|
|
179
|
-
{
|
|
179
|
+
{/**
|
|
180
|
+
* Observe CSS props even without content: changing `disabled` can make a descendant's collected
|
|
181
|
+
* content available to another tooltip.
|
|
182
|
+
*/}
|
|
180
183
|
<Portal>
|
|
181
184
|
<span
|
|
182
185
|
ref={cssPropsPolyfillRef}
|
|
@@ -107,7 +107,7 @@ export const FieldRoot = ({
|
|
|
107
107
|
blank: typeof value === 'object' ? value.optionValue === null : !value,
|
|
108
108
|
disabled,
|
|
109
109
|
},
|
|
110
|
-
{ ref: passedRef },
|
|
110
|
+
{ ref: passedRef, returnState: false },
|
|
111
111
|
)
|
|
112
112
|
const [cssPropsPolyfillStatePassedRef, cssPropsPolyfillAttributes] = useCSSPropsPolyfill(
|
|
113
113
|
FieldMeta,
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
import type { ReactNode } from 'react'
|
|
4
4
|
import { createPortal } from 'react-dom'
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { useIsHydrated } from '../../hooks/index.ts'
|
|
7
7
|
|
|
8
8
|
export function Portal({ children }: { children: ReactNode }) {
|
|
9
|
-
const
|
|
9
|
+
const hydrated = useIsHydrated()
|
|
10
10
|
|
|
11
11
|
// Don't render until after hydration
|
|
12
|
-
if (
|
|
12
|
+
if (!hydrated) {
|
|
13
13
|
return null
|
|
14
14
|
}
|
|
15
15
|
|
|
@@ -94,7 +94,7 @@ export function Transition({
|
|
|
94
94
|
{
|
|
95
95
|
transition: 'idle',
|
|
96
96
|
},
|
|
97
|
-
{ ref: passedRef },
|
|
97
|
+
{ ref: passedRef, returnState: false, exposeState: false },
|
|
98
98
|
)
|
|
99
99
|
|
|
100
100
|
// Collapse to the early-exit branch under reduced-motion — `global.css`'s `@media` block can't skip the cross-fade structure (both `<m.span>`s mount during a swap), only `duration <= 0` can.
|
|
@@ -9,8 +9,9 @@ import {
|
|
|
9
9
|
type EDSComponent,
|
|
10
10
|
} from '@edgeandnode/eds-css'
|
|
11
11
|
|
|
12
|
+
import { getStyleQueriesSupported } from '../utils/internal/index.ts'
|
|
12
13
|
import { useCSSProps, type PassedValues, type PropValues } from './useCSSProps.ts'
|
|
13
|
-
import {
|
|
14
|
+
import { useIsHydrated } from './useIsHydrated.ts'
|
|
14
15
|
|
|
15
16
|
type ExtractPolyfillValues<P extends CSSPropsConfig> = {
|
|
16
17
|
[K in keyof P as P[K]['type'] extends 'values' ? K : never]: CSSPropType<P[K]> | undefined
|
|
@@ -114,16 +115,14 @@ export function useCSSPropsPolyfill<
|
|
|
114
115
|
})
|
|
115
116
|
|
|
116
117
|
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* to immediately rerender in browsers that support style queries); unfortunately, that would
|
|
121
|
-
* cause a flash of missing styles in browsers that need the polyfill.
|
|
118
|
+
* Include the polyfill attributes until hydration completes (even if the polyfill is disabled) to
|
|
119
|
+
* prevent hydration errors. Components mounted after hydration (e.g. on client-side navigation)
|
|
120
|
+
* skip them, so they don't have to rerender (and restyle their subtree) just to remove them.
|
|
122
121
|
*/
|
|
123
|
-
const
|
|
122
|
+
const hydrated = useIsHydrated()
|
|
124
123
|
const polyfillAttributes = cssPropsOverride
|
|
125
124
|
? cssPropsOverride.polyfillAttributes
|
|
126
|
-
: polyfillEnabled ||
|
|
125
|
+
: polyfillEnabled || !hydrated
|
|
127
126
|
? Object.fromEntries(
|
|
128
127
|
cssProps
|
|
129
128
|
.filter((prop) => prop.name in passedValues)
|
|
@@ -148,15 +147,3 @@ export function useCSSPropsPolyfill<
|
|
|
148
147
|
|
|
149
148
|
return [callbackRef, polyfillAttributes, returnedPropValues] as const
|
|
150
149
|
}
|
|
151
|
-
|
|
152
|
-
let privateStyleQueriesSupported: boolean | null = null
|
|
153
|
-
|
|
154
|
-
function getStyleQueriesSupported(): boolean {
|
|
155
|
-
if (typeof window === 'undefined' || typeof document === 'undefined') return false
|
|
156
|
-
if (privateStyleQueriesSupported === null) {
|
|
157
|
-
privateStyleQueriesSupported =
|
|
158
|
-
window.getComputedStyle(document.body).getPropertyValue('--eds-style-queries-test') ===
|
|
159
|
-
'success'
|
|
160
|
-
}
|
|
161
|
-
return privateStyleQueriesSupported
|
|
162
|
-
}
|
package/src/hooks/useCSSState.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type { Ref } from 'react'
|
|
|
2
2
|
|
|
3
3
|
import { cssStateVariables } from '@edgeandnode/eds-css'
|
|
4
4
|
|
|
5
|
+
import { getStyleQueriesSupported } from '../utils/internal/index.ts'
|
|
6
|
+
import { useIsHydrated } from './useIsHydrated.ts'
|
|
5
7
|
import { useMapWithInit } from './useMapWithInit.ts'
|
|
6
8
|
import { useRefWithInit } from './useRefWithInit.ts'
|
|
7
9
|
import { useStyleObserver } from './useStyleObserver.ts'
|
|
@@ -50,9 +52,16 @@ export function useCSSState<
|
|
|
50
52
|
options: {
|
|
51
53
|
ref?: Ref<HTMLElement> | undefined
|
|
52
54
|
onChange?: (state: CSSStateType<T>) => void
|
|
55
|
+
/** Set to `false` when only the attributes are consumed; the polyfill still observes state. */
|
|
56
|
+
returnState?: boolean | undefined
|
|
57
|
+
/** Set to `false` when the component does not spread `exposedAttributes` or `allAttributes`. */
|
|
58
|
+
exposeState?: boolean | undefined
|
|
53
59
|
} = {},
|
|
54
60
|
) {
|
|
55
|
-
const { ref, onChange } = options
|
|
61
|
+
const { ref, onChange, returnState = true, exposeState = true } = options
|
|
62
|
+
const polyfillEnabled = !getStyleQueriesSupported()
|
|
63
|
+
const hydrated = useIsHydrated()
|
|
64
|
+
const observeState = returnState || Boolean(onChange) || polyfillEnabled
|
|
56
65
|
|
|
57
66
|
const variables = useRefWithInit(() =>
|
|
58
67
|
Object.keys(variableInitialValues).map((variableName) => {
|
|
@@ -60,10 +69,21 @@ export function useCSSState<
|
|
|
60
69
|
const defaultValue = !isArbitrary
|
|
61
70
|
? cssStateVariables[variableName as CSSStateVariableName].defaultValue
|
|
62
71
|
: undefined
|
|
72
|
+
const passedValue = variableInitialValues[variableName as keyof T]
|
|
63
73
|
return {
|
|
64
74
|
name: variableName,
|
|
65
75
|
defaultValue,
|
|
66
|
-
initialValue:
|
|
76
|
+
initialValue: passedValue ?? defaultValue,
|
|
77
|
+
/**
|
|
78
|
+
* Nothing is exposed until CSS says otherwise, and a missing `data-eds-exposed-*` attribute
|
|
79
|
+
* falls back to the native state. Starting from the default value instead would make the
|
|
80
|
+
* first style observation remove the attribute, rerendering the component and restyling its
|
|
81
|
+
* whole subtree.
|
|
82
|
+
*/
|
|
83
|
+
initialExposedValue:
|
|
84
|
+
passedValue === undefined || String(passedValue) === String(defaultValue)
|
|
85
|
+
? undefined
|
|
86
|
+
: passedValue,
|
|
67
87
|
observedProperty: isArbitrary
|
|
68
88
|
? `--eds-state-${variableName}`
|
|
69
89
|
: `--eds-observed-${variableName}`,
|
|
@@ -74,14 +94,15 @@ export function useCSSState<
|
|
|
74
94
|
}),
|
|
75
95
|
).current
|
|
76
96
|
|
|
77
|
-
const properties =
|
|
78
|
-
|
|
79
|
-
|
|
97
|
+
const properties = variables.flatMap((variable) => [
|
|
98
|
+
...(observeState ? [variable.observedProperty] : []),
|
|
99
|
+
...(exposeState ? [variable.exposedProperty] : []),
|
|
100
|
+
])
|
|
80
101
|
|
|
81
102
|
const propertyValues = useMapWithInit(() =>
|
|
82
103
|
variables.flatMap((variable) => [
|
|
83
104
|
[variable.observedProperty, String(variable.initialValue ?? '')],
|
|
84
|
-
[variable.exposedProperty, String(variable.
|
|
105
|
+
[variable.exposedProperty, String(variable.initialExposedValue ?? '')],
|
|
85
106
|
]),
|
|
86
107
|
)
|
|
87
108
|
|
|
@@ -131,12 +152,12 @@ export function useCSSState<
|
|
|
131
152
|
}>(
|
|
132
153
|
(result, variable) => {
|
|
133
154
|
const exposedValue = getStateValue(variable, 'exposed')
|
|
134
|
-
if (exposedValue !== undefined) {
|
|
155
|
+
if (exposeState && exposedValue !== undefined) {
|
|
135
156
|
result.exposedAttributes[variable.exposedAttribute] = String(exposedValue)
|
|
136
157
|
result.allAttributes[variable.exposedAttribute] = String(exposedValue)
|
|
137
158
|
}
|
|
138
159
|
const observedValue = getStateValue(variable, 'observed')
|
|
139
|
-
if (observedValue !== undefined) {
|
|
160
|
+
if ((polyfillEnabled || !hydrated) && observedValue !== undefined) {
|
|
140
161
|
result.polyfillAttributes[variable.polyfillAttribute] = String(observedValue)
|
|
141
162
|
result.allAttributes[variable.polyfillAttribute] = String(observedValue)
|
|
142
163
|
}
|
|
@@ -32,25 +32,29 @@ export function useStyleObserver<T extends string>(
|
|
|
32
32
|
if (!element || properties.length === 0 || disabled) return
|
|
33
33
|
|
|
34
34
|
// TODO: Use `style-observer`'s `immediate` option when it exists: https://github.com/LeaVerou/style-observer/issues/45
|
|
35
|
-
|
|
36
|
-
styleScheduler.schedule(() => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
35
|
+
const cancelImmediate = immediate
|
|
36
|
+
? styleScheduler.schedule(() => {
|
|
37
|
+
const computedStyle = window.getComputedStyle(element)
|
|
38
|
+
memoizedOnChange(
|
|
39
|
+
properties.map((property) => ({
|
|
40
|
+
target: element,
|
|
41
|
+
property,
|
|
42
|
+
value: computedStyle.getPropertyValue(property),
|
|
43
|
+
oldValue: '',
|
|
44
|
+
})),
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
: undefined
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
const unsubscribe = styleObserver.subscribe(
|
|
50
50
|
element,
|
|
51
51
|
properties,
|
|
52
52
|
memoizedOnChange as (records: StyleObserverRecord[]) => void,
|
|
53
53
|
)
|
|
54
|
+
return () => {
|
|
55
|
+
cancelImmediate?.()
|
|
56
|
+
unsubscribe()
|
|
57
|
+
}
|
|
54
58
|
},
|
|
55
59
|
[properties, memoizedOnChange, immediate, disabled],
|
|
56
60
|
[watchedRef],
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from '@edgeandnode/eds-css/postcss-plugin'
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ElementStyleObserver } from 'style-observer'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `style-observer` reads computed styles between its transition writes on each new target. Buffer
|
|
5
|
+
* those first writes so every target can read its original styles before any transitions change.
|
|
6
|
+
* Subsequent updates retain the library's transition and browser compatibility handling.
|
|
7
|
+
*/
|
|
8
|
+
export class BatchedElementStyleObserver extends ElementStyleObserver {
|
|
9
|
+
private pendingStyles: Map<string, { value: string; priority: string | undefined }> | undefined =
|
|
10
|
+
new Map()
|
|
11
|
+
|
|
12
|
+
override setProperty(property: string, value: string, priority?: string) {
|
|
13
|
+
if (this.pendingStyles) {
|
|
14
|
+
this.pendingStyles.set(property, { value, priority })
|
|
15
|
+
} else {
|
|
16
|
+
super.setProperty(property, value, priority)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
override getProperty(property: string) {
|
|
21
|
+
return this.pendingStyles?.get(property)?.value ?? super.getProperty(property)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
flush() {
|
|
25
|
+
const pendingStyles = this.pendingStyles
|
|
26
|
+
this.pendingStyles = undefined
|
|
27
|
+
for (const [property, { value, priority }] of pendingStyles ?? []) {
|
|
28
|
+
super.setProperty(property, value, priority)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
let styleQueriesSupported: boolean | null = null
|
|
2
|
+
|
|
3
|
+
export function getStyleQueriesSupported(): boolean {
|
|
4
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') return false
|
|
5
|
+
if (styleQueriesSupported === null) {
|
|
6
|
+
styleQueriesSupported =
|
|
7
|
+
window.getComputedStyle(document.body).getPropertyValue('--eds-style-queries-test') ===
|
|
8
|
+
'success'
|
|
9
|
+
}
|
|
10
|
+
return styleQueriesSupported
|
|
11
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { getTooltipProps } from './getTooltipProps.ts'
|
|
2
2
|
export { isLazy, resolveLazy } from './resolveLazy.ts'
|
|
3
3
|
export { styleScheduler } from './styleScheduler.ts'
|
|
4
|
+
export { BatchedElementStyleObserver } from './BatchedElementStyleObserver.ts'
|
|
5
|
+
export { getStyleQueriesSupported } from './getStyleQueriesSupported.ts'
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Batched style scheduler that collects style operations within a single frame and executes them
|
|
3
|
-
* together using `requestAnimationFrame`.
|
|
4
|
-
* into a single reflow.
|
|
3
|
+
* together using `requestAnimationFrame`. Reads run before writes to avoid layout thrashing.
|
|
5
4
|
*
|
|
6
5
|
* @example
|
|
7
6
|
* ````ts
|
|
@@ -23,8 +22,9 @@
|
|
|
23
22
|
*/
|
|
24
23
|
|
|
25
24
|
class StyleScheduler {
|
|
26
|
-
private
|
|
27
|
-
private pending
|
|
25
|
+
private animationFrameId: number | null = null
|
|
26
|
+
private pending = new Set<() => void>()
|
|
27
|
+
private pendingWrites = new Set<() => void>()
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Schedule an operation to be executed in the next animation frame. All operations scheduled
|
|
@@ -32,24 +32,52 @@ class StyleScheduler {
|
|
|
32
32
|
* Returns a function that cancels the scheduled callback if it hasn't run yet.
|
|
33
33
|
*/
|
|
34
34
|
schedule(callback: () => void): () => void {
|
|
35
|
-
this.
|
|
35
|
+
return this.enqueue(callback, this.pending)
|
|
36
|
+
}
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
this.rafId = null
|
|
38
|
+
/** Schedule a write after all reads in the frame, including writes queued during those reads. */
|
|
39
|
+
scheduleWrite(callback: () => void): () => void {
|
|
40
|
+
return this.enqueue(callback, this.pendingWrites)
|
|
41
|
+
}
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
private enqueue(callback: () => void, queue: Set<() => void>): () => void {
|
|
44
|
+
const operation = () => callback()
|
|
45
|
+
queue.add(operation)
|
|
46
|
+
this.requestFrame()
|
|
47
|
+
return () => {
|
|
48
|
+
queue.delete(operation)
|
|
45
49
|
}
|
|
50
|
+
}
|
|
46
51
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
private runBatch(queue: Set<() => void>) {
|
|
53
|
+
try {
|
|
54
|
+
for (const callback of queue) {
|
|
55
|
+
queue.delete(callback)
|
|
56
|
+
callback()
|
|
57
|
+
}
|
|
58
|
+
} finally {
|
|
59
|
+
// Cancellation closures can outlive the batch, including when a callback throws.
|
|
60
|
+
queue.clear()
|
|
50
61
|
}
|
|
51
62
|
}
|
|
52
63
|
|
|
64
|
+
private requestFrame() {
|
|
65
|
+
if (this.animationFrameId !== null) return
|
|
66
|
+
this.animationFrameId = requestAnimationFrame(() => {
|
|
67
|
+
const callbacks = this.pending
|
|
68
|
+
this.pending = new Set()
|
|
69
|
+
try {
|
|
70
|
+
this.runBatch(callbacks)
|
|
71
|
+
const writes = this.pendingWrites
|
|
72
|
+
this.pendingWrites = new Set()
|
|
73
|
+
this.runBatch(writes)
|
|
74
|
+
} finally {
|
|
75
|
+
this.animationFrameId = null
|
|
76
|
+
if (this.pending.size > 0 || this.pendingWrites.size > 0) this.requestFrame()
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
53
81
|
/**
|
|
54
82
|
* Get computed style for an element in a batched manner. Returns a promise that resolves with the
|
|
55
83
|
* computed style once all batched operations are executed.
|