@proyecto-viviana/solidaria-components 0.3.0 → 0.3.2

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/src/TextField.tsx CHANGED
@@ -79,6 +79,7 @@ export interface TextFieldContextValue {
79
79
  isInvalid?: boolean;
80
80
  slots?: Record<string, TextFieldProps>;
81
81
  inputId?: string;
82
+ setInputId?: (id: string | undefined) => void;
82
83
  }
83
84
 
84
85
  export const TextFieldContext = createContext<TextFieldContextValue | null>(null);
@@ -204,6 +205,14 @@ export function Input(props: InputProps): JSX.Element {
204
205
  const context = useContext(TextFieldContext);
205
206
  let inputElement: HTMLInputElement | undefined;
206
207
 
208
+ createEffect(() => {
209
+ context?.setInputId?.(props.id);
210
+ });
211
+
212
+ onCleanup(() => {
213
+ context?.setInputId?.(undefined);
214
+ });
215
+
207
216
  // Merge context inputProps with local props (local props take precedence)
208
217
  const mergedProps = () => {
209
218
  if (context) {
@@ -291,6 +300,14 @@ export function TextArea(props: TextAreaProps): JSX.Element {
291
300
  const context = useContext(TextFieldContext);
292
301
  let textAreaElement: HTMLTextAreaElement | undefined;
293
302
 
303
+ createEffect(() => {
304
+ context?.setInputId?.(props.id);
305
+ });
306
+
307
+ onCleanup(() => {
308
+ context?.setInputId?.(undefined);
309
+ });
310
+
294
311
  // Merge context inputProps with local props (local props take precedence)
295
312
  // Note: TextArea uses inputProps from context since it's an input variant
296
313
  const mergedProps = () => {
@@ -396,6 +413,7 @@ export function TextField(props: TextFieldProps): JSX.Element {
396
413
  errorMessageProps: _errorMessageProps,
397
414
  isInvalid: _isInvalid,
398
415
  slots: _slots,
416
+ setInputId: _setInputId,
399
417
  ...rest
400
418
  } = contextProps;
401
419
  return rest as TextFieldProps;
@@ -449,6 +467,7 @@ export function TextField(props: TextFieldProps): JSX.Element {
449
467
  return ariaProps.isDisabled;
450
468
  },
451
469
  });
470
+ const [inputIdOverride, setInputIdOverride] = createSignal<string | undefined>();
452
471
 
453
472
  const renderValues = createMemo<TextFieldRenderProps>(() => ({
454
473
  isDisabled: ariaProps.isDisabled || false,
@@ -551,7 +570,10 @@ export function TextField(props: TextFieldProps): JSX.Element {
551
570
  // onMount effect would flip undefined->id post-mount and re-execute the Label
552
571
  // template — crashing hydration if the re-run lands mid-hydration (dev).
553
572
  get inputId() {
554
- return (textFieldAria.inputProps as { id?: string }).id;
573
+ return inputIdOverride() ?? (textFieldAria.inputProps as { id?: string }).id;
574
+ },
575
+ setInputId(id: string | undefined) {
576
+ setInputIdOverride(id);
555
577
  },
556
578
  };
557
579
  // Resolve the render-prop children ONCE (untracked). Re-invoking it on a
@@ -559,10 +581,11 @@ export function TextField(props: TextFieldProps): JSX.Element {
559
581
  // throws a Hydration Mismatch (worst in dev, where slow unbundled modules widen
560
582
  // the hydration window). The children carry their own fine-grained reactivity
561
583
  // (render-value getters + <Show>s), so they update without being re-created.
562
- const fieldChildren = untrack(() => {
563
- const children = local.children;
564
- return typeof children === "function" ? children(childRenderValues) : children;
565
- });
584
+ const FieldChildren = () =>
585
+ untrack(() => {
586
+ const children = local.children;
587
+ return typeof children === "function" ? children(childRenderValues) : children;
588
+ });
566
589
  const rootProps = () =>
567
590
  ({
568
591
  ...domProps(),
@@ -581,7 +604,7 @@ export function TextField(props: TextFieldProps): JSX.Element {
581
604
  const customRootProps = () =>
582
605
  ({
583
606
  ...rootProps(),
584
- children: fieldChildren,
607
+ children: <FieldChildren />,
585
608
  }) as JSX.HTMLAttributes<HTMLDivElement>;
586
609
 
587
610
  return (
@@ -590,7 +613,9 @@ export function TextField(props: TextFieldProps): JSX.Element {
590
613
  {local.render ? (
591
614
  local.render(customRootProps(), renderValues())
592
615
  ) : (
593
- <div {...rootProps()}>{fieldChildren}</div>
616
+ <div {...rootProps()}>
617
+ <FieldChildren />
618
+ </div>
594
619
  )}
595
620
  </TextFieldContext.Provider>
596
621
  </FieldErrorContext.Provider>
package/src/Toast.tsx CHANGED
@@ -252,7 +252,13 @@ export function ToastRegion(props: ToastRegionProps): JSX.Element {
252
252
 
253
253
  const renderProps = useRenderProps(
254
254
  {
255
- children: props.children,
255
+ // Lazy: the region content is gated behind `<Show when={isHydrated() && …}>`
256
+ // (Portal) below, so reading children must not instantiate templates during
257
+ // the component body (would walk getNextElement before the gate the server
258
+ // kept closed → hydration mismatch). See Popover for the full rationale.
259
+ get children() {
260
+ return props.children;
261
+ },
256
262
  class: local.class,
257
263
  style: local.style,
258
264
  defaultClassName: "solidaria-ToastRegion",
package/src/Tooltip.tsx CHANGED
@@ -226,6 +226,40 @@ const TriggerWrapper: ParentComponent<{
226
226
  }> = (props) => {
227
227
  const child = () => props.children as JSX.Element;
228
228
  const [triggerElement, setTriggerElement] = createSignal<HTMLElement | null>(null);
229
+ const [wrapperElement, setWrapperElement] = createSignal<HTMLSpanElement | null>(null);
230
+ const getWrapperEventProps = () => {
231
+ const triggerProps = props.triggerProps as Record<string, unknown>;
232
+ const wrapperProps: Record<string, unknown> = {};
233
+ const eventPropNames = [
234
+ "onFocus",
235
+ "onBlur",
236
+ "onPointerEnter",
237
+ "onPointerLeave",
238
+ "onPointerOver",
239
+ "onPointerOut",
240
+ "onMouseEnter",
241
+ "onMouseLeave",
242
+ "onTouchStart",
243
+ "onPointerDown",
244
+ "onKeyDown",
245
+ ];
246
+
247
+ for (const propName of eventPropNames) {
248
+ const handler = triggerProps[propName];
249
+ if (typeof handler === "function") {
250
+ wrapperProps[propName] = handler;
251
+ }
252
+ }
253
+
254
+ if (!wrapperProps.onPointerEnter && typeof triggerProps.onMouseEnter === "function") {
255
+ wrapperProps.onPointerEnter = triggerProps.onMouseEnter;
256
+ }
257
+ if (!wrapperProps.onPointerLeave && typeof triggerProps.onMouseLeave === "function") {
258
+ wrapperProps.onPointerLeave = triggerProps.onMouseLeave;
259
+ }
260
+
261
+ return wrapperProps as JSX.HTMLAttributes<HTMLSpanElement>;
262
+ };
229
263
 
230
264
  createEffect(() => {
231
265
  const element = triggerElement();
@@ -241,7 +275,9 @@ const TriggerWrapper: ParentComponent<{
241
275
  element.removeAttribute("aria-describedby");
242
276
  }
243
277
 
244
- const listeners: Array<[string, EventListener]> = [];
278
+ const wrapper = wrapperElement();
279
+ const targets = Array.from(new Set([element, wrapper].filter(Boolean))) as HTMLElement[];
280
+ const listeners: Array<[HTMLElement, string, EventListener]> = [];
245
281
  const eventProps = [
246
282
  ["onFocus", "focus"],
247
283
  ["onBlur", "blur"],
@@ -257,17 +293,24 @@ const TriggerWrapper: ParentComponent<{
257
293
  ] as const;
258
294
 
259
295
  for (const [propName, eventName] of eventProps) {
260
- const handler = triggerProps[propName];
296
+ let handler = triggerProps[propName];
297
+ if (!handler && propName === "onPointerEnter") {
298
+ handler = triggerProps.onMouseEnter;
299
+ } else if (!handler && propName === "onPointerLeave") {
300
+ handler = triggerProps.onMouseLeave;
301
+ }
261
302
  if (typeof handler === "function") {
262
303
  const listener = handler as EventListener;
263
- element.addEventListener(eventName, listener);
264
- listeners.push([eventName, listener]);
304
+ for (const target of targets) {
305
+ target.addEventListener(eventName, listener);
306
+ listeners.push([target, eventName, listener]);
307
+ }
265
308
  }
266
309
  }
267
310
 
268
311
  onCleanup(() => {
269
- for (const [eventName, listener] of listeners) {
270
- element.removeEventListener(eventName, listener);
312
+ for (const [target, eventName, listener] of listeners) {
313
+ target.removeEventListener(eventName, listener);
271
314
  }
272
315
  if (describedBy && element.getAttribute("aria-describedby") === describedBy) {
273
316
  element.removeAttribute("aria-describedby");
@@ -279,6 +322,8 @@ const TriggerWrapper: ParentComponent<{
279
322
  // However, display:contents makes getBoundingClientRect return zeros,
280
323
  // so we pass a ref callback that finds the first actual element child.
281
324
  const handleRef = (span: HTMLSpanElement) => {
325
+ setWrapperElement(span);
326
+
282
327
  const findElementChild = (el: Element): HTMLElement | null => {
283
328
  for (const child of el.children) {
284
329
  if (child instanceof HTMLElement) {
@@ -329,7 +374,7 @@ const TriggerWrapper: ParentComponent<{
329
374
  };
330
375
 
331
376
  return (
332
- <span ref={handleRef} style={{ display: "contents" }}>
377
+ <span {...getWrapperEventProps()} ref={handleRef} style={{ display: "contents" }}>
333
378
  {child()}
334
379
  </span>
335
380
  );
package/src/utils.tsx CHANGED
@@ -334,13 +334,17 @@ export function useIsHydrated(): Accessor<boolean> {
334
334
  return () => false;
335
335
  }
336
336
 
337
- // On client, start false and switch to true after animation frame
338
- // This ensures we're past the hydration phase
337
+ // On client, start false (so the first render matches the server, which
338
+ // emitted nothing for hydrated-gated content) and flip to true after mount.
339
339
  const [isHydrated, setIsHydrated] = createSignal(false);
340
340
 
341
- // Use requestAnimationFrame to ensure we're past hydration
342
- // onMount may not fire during hydration for matching DOM
343
- requestAnimationFrame(() => {
341
+ // onMount runs in the effect phase — *after* the synchronous hydration pass
342
+ // has finished walking the server DOM so flipping here renders the gated
343
+ // content as a fresh client-side update (Portal: no getNextElement walk, no
344
+ // mismatch), yet fires synchronously under `render()` (unit tests / pure CSR)
345
+ // where requestAnimationFrame would never run. This mirrors the component
346
+ // gate above and is strictly earlier than a rAF tick.
347
+ onMount(() => {
344
348
  setIsHydrated(true);
345
349
  });
346
350