@edgeandnode/eds-react 1.1.0 → 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.
Files changed (118) hide show
  1. package/README.md +27 -3
  2. package/dist/components/Address.d.ts +3 -3
  3. package/dist/components/Address.d.ts.map +1 -1
  4. package/dist/components/Address.js +1 -1
  5. package/dist/components/Address.js.map +1 -1
  6. package/dist/components/Avatar.d.ts +3 -3
  7. package/dist/components/Avatar.d.ts.map +1 -1
  8. package/dist/components/Avatar.js +13 -1
  9. package/dist/components/Avatar.js.map +1 -1
  10. package/dist/components/Breadcrumbs.parts.d.ts +3 -3
  11. package/dist/components/Breadcrumbs.parts.d.ts.map +1 -1
  12. package/dist/components/Breadcrumbs.parts.js +1 -1
  13. package/dist/components/Breadcrumbs.parts.js.map +1 -1
  14. package/dist/components/Button.js +1 -1
  15. package/dist/components/Button.js.map +1 -1
  16. package/dist/components/Card.js +1 -1
  17. package/dist/components/Card.js.map +1 -1
  18. package/dist/components/Chip.parts.d.ts.map +1 -1
  19. package/dist/components/Chip.parts.js +1 -1
  20. package/dist/components/Chip.parts.js.map +1 -1
  21. package/dist/components/Cluster.d.ts +3 -3
  22. package/dist/components/Cluster.d.ts.map +1 -1
  23. package/dist/components/Cluster.js.map +1 -1
  24. package/dist/components/Icon.d.ts.map +1 -1
  25. package/dist/components/Icon.js +0 -2
  26. package/dist/components/Icon.js.map +1 -1
  27. package/dist/components/Link.d.ts.map +1 -1
  28. package/dist/components/Link.js +2 -0
  29. package/dist/components/Link.js.map +1 -1
  30. package/dist/components/Menu.parts.js +1 -1
  31. package/dist/components/Menu.parts.js.map +1 -1
  32. package/dist/components/Search.d.ts.map +1 -1
  33. package/dist/components/Search.js +1 -1
  34. package/dist/components/Search.js.map +1 -1
  35. package/dist/components/SegmentedControl.parts.d.ts.map +1 -1
  36. package/dist/components/SegmentedControl.parts.js +1 -1
  37. package/dist/components/SegmentedControl.parts.js.map +1 -1
  38. package/dist/components/TabSet.parts.js +1 -1
  39. package/dist/components/TabSet.parts.js.map +1 -1
  40. package/dist/components/Tooltip.parts.d.ts.map +1 -1
  41. package/dist/components/Tooltip.parts.js.map +1 -1
  42. package/dist/components/base/Checkable.parts.js +1 -1
  43. package/dist/components/base/Checkable.parts.js.map +1 -1
  44. package/dist/components/base/Field.parts.js +1 -1
  45. package/dist/components/base/Field.parts.js.map +1 -1
  46. package/dist/components/base/Portal.js +3 -3
  47. package/dist/components/base/Portal.js.map +1 -1
  48. package/dist/components/base/Transition.js +1 -1
  49. package/dist/components/base/Transition.js.map +1 -1
  50. package/dist/hooks/useCSSPropsPolyfill.d.ts.map +1 -1
  51. package/dist/hooks/useCSSPropsPolyfill.js +7 -19
  52. package/dist/hooks/useCSSPropsPolyfill.js.map +1 -1
  53. package/dist/hooks/useCSSState.d.ts +4 -0
  54. package/dist/hooks/useCSSState.d.ts.map +1 -1
  55. package/dist/hooks/useCSSState.js +24 -6
  56. package/dist/hooks/useCSSState.js.map +1 -1
  57. package/dist/hooks/useStyleObserver.d.ts.map +1 -1
  58. package/dist/hooks/useStyleObserver.js +9 -5
  59. package/dist/hooks/useStyleObserver.js.map +1 -1
  60. package/dist/postcss-plugin.d.ts +2 -0
  61. package/dist/postcss-plugin.d.ts.map +1 -0
  62. package/dist/postcss-plugin.js +2 -0
  63. package/dist/postcss-plugin.js.map +1 -0
  64. package/dist/utils/internal/BatchedElementStyleObserver.d.ts +13 -0
  65. package/dist/utils/internal/BatchedElementStyleObserver.d.ts.map +1 -0
  66. package/dist/utils/internal/BatchedElementStyleObserver.js +28 -0
  67. package/dist/utils/internal/BatchedElementStyleObserver.js.map +1 -0
  68. package/dist/utils/internal/getStyleQueriesSupported.d.ts +2 -0
  69. package/dist/utils/internal/getStyleQueriesSupported.d.ts.map +1 -0
  70. package/dist/utils/internal/getStyleQueriesSupported.js +12 -0
  71. package/dist/utils/internal/getStyleQueriesSupported.js.map +1 -0
  72. package/dist/utils/internal/index.d.ts +2 -0
  73. package/dist/utils/internal/index.d.ts.map +1 -1
  74. package/dist/utils/internal/index.js +2 -0
  75. package/dist/utils/internal/index.js.map +1 -1
  76. package/dist/utils/internal/styleScheduler.d.ts +8 -3
  77. package/dist/utils/internal/styleScheduler.d.ts.map +1 -1
  78. package/dist/utils/internal/styleScheduler.js +46 -16
  79. package/dist/utils/internal/styleScheduler.js.map +1 -1
  80. package/dist/utils/styleObserver.d.ts +2 -0
  81. package/dist/utils/styleObserver.d.ts.map +1 -1
  82. package/dist/utils/styleObserver.js +40 -33
  83. package/dist/utils/styleObserver.js.map +1 -1
  84. package/dist/vite-plugin.d.ts +2 -0
  85. package/dist/vite-plugin.d.ts.map +1 -0
  86. package/dist/vite-plugin.js +2 -0
  87. package/dist/vite-plugin.js.map +1 -0
  88. package/package.json +13 -11
  89. package/scripts/update-skill.cjs +42 -0
  90. package/skill/SKILL.md +18 -14
  91. package/src/components/Address.tsx +12 -10
  92. package/src/components/Avatar.tsx +61 -49
  93. package/src/components/Breadcrumbs.parts.tsx +11 -9
  94. package/src/components/Button.tsx +1 -1
  95. package/src/components/Card.tsx +1 -1
  96. package/src/components/Chip.parts.tsx +9 -6
  97. package/src/components/Cluster.tsx +3 -5
  98. package/src/components/Icon.tsx +0 -2
  99. package/src/components/Link.tsx +2 -0
  100. package/src/components/Menu.parts.tsx +1 -1
  101. package/src/components/Search.tsx +9 -6
  102. package/src/components/SegmentedControl.parts.tsx +8 -5
  103. package/src/components/TabSet.parts.tsx +1 -1
  104. package/src/components/Tooltip.parts.tsx +4 -1
  105. package/src/components/base/Checkable.parts.tsx +1 -1
  106. package/src/components/base/Field.parts.tsx +1 -1
  107. package/src/components/base/Portal.tsx +3 -3
  108. package/src/components/base/Transition.tsx +1 -1
  109. package/src/hooks/useCSSPropsPolyfill.ts +7 -20
  110. package/src/hooks/useCSSState.ts +29 -8
  111. package/src/hooks/useStyleObserver.ts +18 -14
  112. package/src/postcss-plugin.ts +1 -0
  113. package/src/utils/internal/BatchedElementStyleObserver.ts +31 -0
  114. package/src/utils/internal/getStyleQueriesSupported.ts +11 -0
  115. package/src/utils/internal/index.ts +2 -0
  116. package/src/utils/internal/styleScheduler.ts +43 -15
  117. package/src/utils/styleObserver.ts +38 -40
  118. package/src/vite-plugin.ts +1 -0
@@ -26,15 +26,14 @@ export declare namespace AddressProps {
26
26
  interface ButtonProps
27
27
  extends
28
28
  CommonProps,
29
- Omit<MaybeButtonOrLinkProps.ButtonProps, 'as'>,
29
+ MaybeButtonOrLinkProps.ButtonProps,
30
30
  MaybeButtonOrLinkProps.DisableableProps {}
31
31
  interface LinkProps
32
32
  extends
33
33
  CommonProps,
34
- Omit<MaybeButtonOrLinkProps.LinkProps, 'as'>,
34
+ MaybeButtonOrLinkProps.LinkProps,
35
35
  MaybeButtonOrLinkProps.DisableableProps {}
36
- interface OtherElementProps
37
- extends CommonProps, Omit<MaybeButtonOrLinkProps.OtherElementProps, 'as'> {}
36
+ interface OtherElementProps extends CommonProps, MaybeButtonOrLinkProps.OtherElementProps {}
38
37
  }
39
38
 
40
39
  export type AddressProps =
@@ -55,12 +54,15 @@ export function Address({
55
54
 
56
55
  const { rootProps, nestedProps } = splitProps(props)
57
56
 
58
- const [stateRef, state] = useCSSState({
59
- pointer: undefined,
60
- focus: undefined,
61
- disabled: Boolean(props.disabled),
62
- 'show-copy': false,
63
- })
57
+ const [stateRef, state] = useCSSState(
58
+ {
59
+ pointer: undefined,
60
+ focus: undefined,
61
+ disabled: Boolean(props.disabled),
62
+ 'show-copy': false,
63
+ },
64
+ { returnState: false },
65
+ )
64
66
  const [cssPropsPolyfillStateRef, cssPropsPolyfillAttributes] = useCSSPropsPolyfill(
65
67
  AddressMeta,
66
68
  { variant, size, copy },
@@ -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)
@@ -37,13 +38,11 @@ export declare namespace AvatarProps {
37
38
  }
38
39
  interface CommonProps extends OwnProps, EDSComponentProps<typeof AvatarMeta> {}
39
40
  interface ButtonProps
40
- extends CommonProps, Omit<MaybeButtonOrLinkProps.ButtonProps, 'as' | 'children' | 'render'> {}
41
+ extends CommonProps, Omit<MaybeButtonOrLinkProps.ButtonProps, 'children' | 'render'> {}
41
42
  interface LinkProps
42
- extends CommonProps, Omit<MaybeButtonOrLinkProps.LinkProps, 'as' | 'children' | 'render'> {}
43
+ extends CommonProps, Omit<MaybeButtonOrLinkProps.LinkProps, 'children' | 'render'> {}
43
44
  interface OtherElementProps
44
- extends
45
- CommonProps,
46
- Omit<MaybeButtonOrLinkProps.OtherElementProps, 'as' | 'children' | 'render'> {}
45
+ extends CommonProps, Omit<MaybeButtonOrLinkProps.OtherElementProps, 'children' | 'render'> {}
47
46
  }
48
47
 
49
48
  export type AvatarProps =
@@ -96,55 +95,68 @@ export function Avatar({
96
95
  props,
97
96
  )}
98
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
+ }
99
153
  return (
100
154
  <Tooltip
101
- content={category !== 'other' ? alt : undefined}
155
+ content={clickable ? alt : undefined}
102
156
  {...getTooltipProps(tooltip)}
103
157
  triggerProps={renderProps}
104
158
  >
105
- <Element {...elementProps}>
106
- <span className="nested/avatar-paint pointer-events-none shrink-0">
107
- <span
108
- className="
109
- block overflow-clip bg-default
110
- group-clickable-focus-visible/avatar:outline
111
- @prop-size-xsmall/avatar:size-4
112
- @prop-size-small/avatar:size-5
113
- @prop-size-medium/avatar:size-6
114
- @prop-size-large/avatar:size-8
115
- @prop-size-xlarge/avatar:size-12
116
- @prop-shape-circle/avatar:rounded-full
117
- @prop-shape-rounded/avatar:rounded-4
118
- @prop-shape-rounded/avatar:@prop-size-xsmall/avatar:rounded-2
119
- @prop-shape-rounded/avatar:@prop-size-xlarge/avatar:rounded-8
120
- uu:*:absolute
121
- uu:*:inset-0
122
- uu:*:m-auto
123
- uu:*:size-full
124
- uu:*:object-cover
125
- i:@prop-size-xsmall/avatar:**:icon:default-size-2.5
126
- i:@prop-size-small/avatar:**:icon:default-size-3
127
- i:@prop-size-medium/avatar:**:icon:default-size-4
128
- i:@prop-size-large/avatar:**:icon:default-size-5
129
- i:@prop-size-xlarge/avatar:**:icon:default-size-8
130
- "
131
- >
132
- {src ? (
133
- typeof src === 'string' ? (
134
- <img
135
- src={src}
136
- alt=""
137
- onError={() => setCurrentSourceIndex(currentSourceIndex + 1)}
138
- className="[[src^='data:image/bmp']]:[image-rendering:pixelated]"
139
- />
140
- ) : (
141
- <Tooltip.Collector>{src}</Tooltip.Collector>
142
- )
143
- ) : null}
144
- {alt ? <span className="sr-only">{alt}</span> : null}
145
- </span>
146
- </span>
147
- </Element>
159
+ {avatar}
148
160
  </Tooltip>
149
161
  )
150
162
  }}
@@ -269,10 +269,9 @@ export declare namespace BreadcrumbsItemProps {
269
269
  current?: boolean | undefined
270
270
  }
271
271
  interface CommonProps extends OwnProps, EDSComponentProps<typeof BreadcrumbsItemMeta> {}
272
- interface ButtonProps extends CommonProps, Omit<MaybeButtonOrLinkProps.ButtonProps, 'as'> {}
273
- interface LinkProps extends CommonProps, Omit<MaybeButtonOrLinkProps.LinkProps, 'as'> {}
274
- interface OtherElementProps
275
- extends CommonProps, Omit<MaybeButtonOrLinkProps.OtherElementProps, 'as'> {}
272
+ interface ButtonProps extends CommonProps, MaybeButtonOrLinkProps.ButtonProps {}
273
+ interface LinkProps extends CommonProps, MaybeButtonOrLinkProps.LinkProps {}
274
+ interface OtherElementProps extends CommonProps, MaybeButtonOrLinkProps.OtherElementProps {}
276
275
  }
277
276
 
278
277
  export type BreadcrumbsItemProps =
@@ -291,11 +290,14 @@ export function BreadcrumbsItem({
291
290
  const current = passedCurrent ?? isLastItem
292
291
  const { rootProps, nestedProps } = splitProps(props)
293
292
 
294
- const [stateRef, state] = useCSSState({
295
- pointer: undefined,
296
- focus: undefined,
297
- current,
298
- })
293
+ const [stateRef, state] = useCSSState(
294
+ {
295
+ pointer: undefined,
296
+ focus: undefined,
297
+ current,
298
+ },
299
+ { returnState: false },
300
+ )
299
301
  const [cssPropsPolyfillStatePassedRef, cssPropsPolyfillAttributes] = useCSSPropsPolyfill(
300
302
  BreadcrumbsItemMeta,
301
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(
@@ -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
- pointer: undefined,
70
- focus: undefined,
71
- checked: undefined,
72
- disabled: Boolean(disabled),
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 },
@@ -11,13 +11,11 @@ import { ClusterMeta } from './Cluster.meta.ts'
11
11
 
12
12
  export declare namespace ClusterProps {
13
13
  interface ButtonProps
14
- extends EDSComponentProps<typeof ClusterMeta>, Omit<MaybeButtonOrLinkProps.ButtonProps, 'as'> {}
14
+ extends EDSComponentProps<typeof ClusterMeta>, MaybeButtonOrLinkProps.ButtonProps {}
15
15
  interface LinkProps
16
- extends EDSComponentProps<typeof ClusterMeta>, Omit<MaybeButtonOrLinkProps.LinkProps, 'as'> {}
16
+ extends EDSComponentProps<typeof ClusterMeta>, MaybeButtonOrLinkProps.LinkProps {}
17
17
  interface OtherElementProps
18
- extends
19
- EDSComponentProps<typeof ClusterMeta>,
20
- Omit<MaybeButtonOrLinkProps.OtherElementProps, 'as'> {}
18
+ extends EDSComponentProps<typeof ClusterMeta>, MaybeButtonOrLinkProps.OtherElementProps {}
21
19
  }
22
20
 
23
21
  export type ClusterProps =
@@ -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
@@ -54,6 +54,8 @@ export function Link({
54
54
  },
55
55
  {
56
56
  ref: passedRef,
57
+ returnState: false,
58
+ exposeState: false,
57
59
  },
58
60
  )
59
61
  const [cssPropsPolyfillStatePassedRef, cssPropsPolyfillAttributes, cssProps] =
@@ -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
- pointer: undefined,
59
- focus: undefined,
60
- blank: value === '',
61
- open: value !== '',
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
- pointer: undefined,
157
- checked: undefined,
158
- 'is-toggle': undefined,
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
- {/* Ensure changes to CSS props are detected even when the tooltip is not mounted */}
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}
@@ -209,7 +209,7 @@ export function CheckableLabel({
209
209
 
210
210
  const [statePassedRef, state] = useCSSState(
211
211
  { pointer: undefined, focus: undefined, checked, disabled },
212
- { ref: passedRef },
212
+ { ref: passedRef, returnState: false },
213
213
  )
214
214
 
215
215
  return (
@@ -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 { useFirstRender } from '../../hooks/index.ts'
6
+ import { useIsHydrated } from '../../hooks/index.ts'
7
7
 
8
8
  export function Portal({ children }: { children: ReactNode }) {
9
- const firstRender = useFirstRender({ immediatelyRerender: true })
9
+ const hydrated = useIsHydrated()
10
10
 
11
11
  // Don't render until after hydration
12
- if (firstRender) {
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 { useFirstRender } from './useFirstRender.ts'
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
- * Always include the polyfill attributes on the first render (even if the polyfill is disabled)
118
- * to prevent hydration errors. It would be nice to optimize for the case where the polyfill is
119
- * not needed instead (by always excluding the attributes on the first render, thereby not having
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 firstRender = useFirstRender({ immediatelyRerender: !polyfillEnabled })
122
+ const hydrated = useIsHydrated()
124
123
  const polyfillAttributes = cssPropsOverride
125
124
  ? cssPropsOverride.polyfillAttributes
126
- : polyfillEnabled || firstRender
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
- }
@@ -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: variableInitialValues[variableName as keyof T] ?? defaultValue,
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 = useRefWithInit(() =>
78
- variables.flatMap((variable) => [variable.observedProperty, variable.exposedProperty]),
79
- ).current
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.initialValue ?? '')],
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
- if (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
- }
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
- return styleObserver.subscribe(
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'