@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.
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.tsx"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proyecto-viviana/solidaria-components",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Pre-wired headless components for SolidJS - port of react-aria-components",
5
5
  "keywords": [
6
6
  "accessibility",
@@ -38,8 +38,8 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@internationalized/date": "^3.8.0",
41
- "@proyecto-viviana/solidaria": "0.3.0",
42
- "@proyecto-viviana/solid-stately": "0.3.0"
41
+ "@proyecto-viviana/solid-stately": "0.3.0",
42
+ "@proyecto-viviana/solidaria": "0.3.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "solid-js": "^1.9.11"
package/src/Button.tsx CHANGED
@@ -97,6 +97,44 @@ function createLiveCustomRootProps(
97
97
  return props;
98
98
  }
99
99
 
100
+ const buttonAriaOverrideProps = [
101
+ "aria-label",
102
+ "aria-labelledby",
103
+ "aria-describedby",
104
+ "aria-details",
105
+ "aria-haspopup",
106
+ "aria-expanded",
107
+ "aria-controls",
108
+ "aria-pressed",
109
+ "aria-current",
110
+ "aria-disabled",
111
+ ] as const;
112
+
113
+ function createForwardedAriaButtonProps(
114
+ source: AriaButtonProps,
115
+ overrides: AriaButtonProps,
116
+ ): AriaButtonProps {
117
+ const result = {} as AriaButtonProps;
118
+
119
+ for (const key in source) {
120
+ Object.defineProperty(result, key, {
121
+ enumerable: true,
122
+ configurable: true,
123
+ get() {
124
+ return (source as Record<string, unknown>)[key];
125
+ },
126
+ });
127
+ }
128
+
129
+ for (const key in overrides) {
130
+ const descriptor = Object.getOwnPropertyDescriptor(overrides, key);
131
+ if (!descriptor) continue;
132
+ Object.defineProperty(result, key, { ...descriptor, enumerable: true, configurable: true });
133
+ }
134
+
135
+ return result;
136
+ }
137
+
100
138
  export interface ButtonRenderProps {
101
139
  /** Whether the button is currently hovered with a mouse. */
102
140
  isHovered: boolean;
@@ -238,28 +276,29 @@ export function Button(props: ButtonProps): JSX.Element {
238
276
  }
239
277
  };
240
278
 
241
- const buttonAria = createButton({
242
- ...ariaProps,
243
- onPress: handlePress,
244
- get onPressStart() {
245
- return resolvePending() ? undefined : ariaProps.onPressStart;
246
- },
247
- get onPressEnd() {
248
- return resolvePending() ? undefined : ariaProps.onPressEnd;
249
- },
250
- get onPressUp() {
251
- return resolvePending() ? undefined : ariaProps.onPressUp;
252
- },
253
- get onPressChange() {
254
- return resolvePending() ? undefined : ariaProps.onPressChange;
255
- },
256
- get onClick() {
257
- return resolvePending() ? undefined : ariaProps.onClick;
258
- },
259
- get isDisabled() {
260
- return resolveDisabled() || resolvePending();
261
- },
262
- });
279
+ const buttonAria = createButton(
280
+ createForwardedAriaButtonProps(ariaProps, {
281
+ onPress: handlePress,
282
+ get onPressStart() {
283
+ return resolvePending() ? undefined : ariaProps.onPressStart;
284
+ },
285
+ get onPressEnd() {
286
+ return resolvePending() ? undefined : ariaProps.onPressEnd;
287
+ },
288
+ get onPressUp() {
289
+ return resolvePending() ? undefined : ariaProps.onPressUp;
290
+ },
291
+ get onPressChange() {
292
+ return resolvePending() ? undefined : ariaProps.onPressChange;
293
+ },
294
+ get onClick() {
295
+ return resolvePending() ? undefined : ariaProps.onClick;
296
+ },
297
+ get isDisabled() {
298
+ return resolveDisabled() || resolvePending();
299
+ },
300
+ }),
301
+ );
263
302
 
264
303
  const { isFocused, isFocusVisible, focusProps } = createFocusRing();
265
304
 
@@ -394,6 +433,13 @@ export function Button(props: ButtonProps): JSX.Element {
394
433
  }
395
434
  return next;
396
435
  };
436
+ const directAriaProps = () => {
437
+ const next: Record<string, unknown> = {};
438
+ for (const name of buttonAriaOverrideProps) {
439
+ next[name] = (ariaProps as Record<string, unknown>)[name];
440
+ }
441
+ return next;
442
+ };
397
443
  const disablePendingInteractions = (props: Record<string, unknown>) => {
398
444
  if (!resolvePending()) {
399
445
  return props;
@@ -433,6 +479,7 @@ export function Button(props: ButtonProps): JSX.Element {
433
479
  ({
434
480
  ...domProps(),
435
481
  ...disablePendingInteractions(cleanButtonProps()),
482
+ ...directAriaProps(),
436
483
  ...triggerAriaProps(),
437
484
  ...cleanFocusProps(),
438
485
  ...cleanHoverProps(),
package/src/ComboBox.tsx CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  type JSX,
10
10
  type Accessor,
11
11
  createContext,
12
+ createEffect,
12
13
  createMemo,
13
14
  onCleanup,
14
15
  splitProps,
@@ -290,6 +291,7 @@ export interface ComboBoxOptionProps<T>
290
291
 
291
292
  interface ComboBoxContextValue<T> {
292
293
  state: ComboBoxState<T>;
294
+ listState: ListState<T>;
293
295
  inputProps: () => JSX.InputHTMLAttributes<HTMLInputElement>;
294
296
  buttonProps: () => JSX.HTMLAttributes<HTMLElement>;
295
297
  listBoxProps: () => JSX.HTMLAttributes<HTMLElement>;
@@ -308,13 +310,31 @@ interface ComboBoxContextValue<T> {
308
310
  setTriggerRef: (el: HTMLElement | null) => void;
309
311
  listBoxRef: () => HTMLElement | null;
310
312
  setListBoxRef: (el: HTMLElement | null) => void;
313
+ registerOptionAction: (key: Key, action: (() => void) | undefined) => void;
314
+ runOptionAction: (key: Key) => void;
311
315
  slots?: Record<string, Partial<ComboBoxProps<T>>>;
312
316
  }
313
317
 
318
+ type InputKeyboardEvent = KeyboardEvent & {
319
+ currentTarget: HTMLInputElement;
320
+ target: Element;
321
+ };
322
+
314
323
  export const ComboBoxContext = createContext<ComboBoxContextValue<unknown> | null>(null);
315
324
  export const ComboBoxStateContext = createContext<ComboBoxState<unknown> | null>(null);
316
325
  export const ComboBoxValueContext = ComboBoxContext;
317
326
 
327
+ function callInputKeyDown(
328
+ handler: JSX.EventHandlerUnion<HTMLInputElement, KeyboardEvent> | undefined,
329
+ event: InputKeyboardEvent,
330
+ ) {
331
+ if (typeof handler === "function") {
332
+ handler(event);
333
+ } else if (handler) {
334
+ handler[0](handler[1], event);
335
+ }
336
+ }
337
+
318
338
  /**
319
339
  * A combobox combines a text input with a listbox, allowing users to filter a list of options.
320
340
  */
@@ -360,6 +380,10 @@ export function ComboBox<T>(props: ComboBoxProps<T>): JSX.Element {
360
380
  let buttonRef: HTMLElement | null = null;
361
381
  let triggerRef: HTMLElement | null = null;
362
382
  let listBoxRef: HTMLElement | null = null;
383
+ const optionActions = new Map<Key, () => void>();
384
+ const runOptionAction = (key: Key) => {
385
+ optionActions.get(key)?.();
386
+ };
363
387
 
364
388
  const state = createComboBoxState<T>({
365
389
  get items() {
@@ -441,6 +465,7 @@ export function ComboBox<T>(props: ComboBoxProps<T>): JSX.Element {
441
465
  return ariaProps.isRequired;
442
466
  },
443
467
  });
468
+ const listState = createComboBoxListStateAdapter(state);
444
469
 
445
470
  const effectiveFormValue = createMemo<"key" | "text">(() => {
446
471
  if (stateProps.allowsCustomValue) {
@@ -472,6 +497,28 @@ export function ComboBox<T>(props: ComboBoxProps<T>): JSX.Element {
472
497
  () => listBoxRef,
473
498
  );
474
499
 
500
+ const getInputProps = () => {
501
+ const inputProps = comboBoxAria.inputProps;
502
+ const originalOnKeyDown = inputProps.onKeyDown;
503
+
504
+ return {
505
+ ...inputProps,
506
+ onKeyDown: (event: InputKeyboardEvent) => {
507
+ const focusedKey = state.focusedKey();
508
+ const shouldRunAction =
509
+ event.key === "Enter" &&
510
+ state.isOpen() &&
511
+ focusedKey != null &&
512
+ !state.isKeyDisabled(focusedKey);
513
+ const optionAction = shouldRunAction ? optionActions.get(focusedKey) : undefined;
514
+
515
+ callInputKeyDown(originalOnKeyDown, event);
516
+
517
+ optionAction?.();
518
+ },
519
+ } as JSX.InputHTMLAttributes<HTMLInputElement>;
520
+ };
521
+
475
522
  const { isHovered, hoverProps } = createHover({
476
523
  get isDisabled() {
477
524
  return ariaProps.isDisabled;
@@ -519,9 +566,13 @@ export function ComboBox<T>(props: ComboBoxProps<T>): JSX.Element {
519
566
  value={
520
567
  {
521
568
  state,
522
- inputProps: () => comboBoxAria.inputProps,
569
+ listState,
570
+ inputProps: getInputProps,
523
571
  buttonProps: () => comboBoxAria.buttonProps,
524
- listBoxProps: () => comboBoxAria.listBoxProps,
572
+ listBoxProps: () => ({
573
+ ...comboBoxAria.listBoxProps,
574
+ onAction: runOptionAction,
575
+ }),
525
576
  labelProps: () => comboBoxAria.labelProps,
526
577
  descriptionProps: () => comboBoxAria.descriptionProps,
527
578
  errorMessageProps: () => comboBoxAria.errorMessageProps,
@@ -545,6 +596,14 @@ export function ComboBox<T>(props: ComboBoxProps<T>): JSX.Element {
545
596
  setListBoxRef: (el) => {
546
597
  listBoxRef = el;
547
598
  },
599
+ registerOptionAction: (key, action) => {
600
+ if (action) {
601
+ optionActions.set(key, action);
602
+ } else {
603
+ optionActions.delete(key);
604
+ }
605
+ },
606
+ runOptionAction,
548
607
  slots: local.slots,
549
608
  } as ComboBoxContextValue<unknown>
550
609
  }
@@ -850,7 +909,7 @@ export function ComboBoxListBox<T>(props: ComboBoxListBoxProps<T>): JSX.Element
850
909
  throw new Error("ComboBoxListBox must be used within a ComboBox");
851
910
  }
852
911
  const context = rawContext as ComboBoxContextValue<T>;
853
- const { state: comboBoxState, isOpen, inputRef, buttonRef, setListBoxRef } = context;
912
+ const { state: comboBoxState, listState, isOpen, inputRef, buttonRef, setListBoxRef } = context;
854
913
  const state = comboBoxState;
855
914
 
856
915
  let listBoxRef: HTMLUListElement | undefined;
@@ -880,7 +939,7 @@ export function ComboBoxListBox<T>(props: ComboBoxListBoxProps<T>): JSX.Element
880
939
  // Create listbox aria props using ComboBoxState's ListState-compatible interface
881
940
  const { listBoxProps } = createListBox(
882
941
  context.listBoxProps as unknown as AriaListBoxProps,
883
- createComboBoxListStateAdapter(state),
942
+ listState,
884
943
  );
885
944
 
886
945
  const renderValues = createMemo<ComboBoxListBoxRenderProps>(() => ({
@@ -976,15 +1035,24 @@ export function ComboBoxOption<T>(props: ComboBoxOptionProps<T>): JSX.Element {
976
1035
 
977
1036
  const stateContext = useContext(ComboBoxStateContext);
978
1037
  const comboBoxContext = useContext(ComboBoxContext);
979
- if (!stateContext) {
1038
+ if (!stateContext || !comboBoxContext) {
980
1039
  throw new Error("ComboBoxOption must be used within a ComboBox");
981
1040
  }
982
1041
  const state = stateContext as ComboBoxState<T>;
1042
+ const listState = (comboBoxContext as ComboBoxContextValue<T>).listState;
983
1043
  const optionId = () => {
984
1044
  const listBoxId = getComboBoxData(state as ComboBoxState<unknown>)?.listBoxId;
985
1045
  return listBoxId ? `${listBoxId}-option-${local.id}` : String(local.id);
986
1046
  };
987
1047
 
1048
+ createEffect(() => {
1049
+ const key = local.id;
1050
+ comboBoxContext?.registerOptionAction(key, local.onAction);
1051
+ onCleanup(() => {
1052
+ comboBoxContext?.registerOptionAction(key, undefined);
1053
+ });
1054
+ });
1055
+
988
1056
  // Create option aria props using ComboBoxState's ListState-compatible interface
989
1057
  const optionAria = createOption<T>(
990
1058
  {
@@ -998,9 +1066,6 @@ export function ComboBoxOption<T>(props: ComboBoxOptionProps<T>): JSX.Element {
998
1066
  get "aria-label"() {
999
1067
  return ariaProps["aria-label"];
1000
1068
  },
1001
- get onAction() {
1002
- return local.onAction;
1003
- },
1004
1069
  shouldSelectOnPressUp: true,
1005
1070
  shouldFocusOnHover: true,
1006
1071
  shouldUseVirtualFocus: true,
@@ -1014,8 +1079,11 @@ export function ComboBoxOption<T>(props: ComboBoxOptionProps<T>): JSX.Element {
1014
1079
  get onHoverChange() {
1015
1080
  return ariaProps.onHoverChange;
1016
1081
  },
1082
+ get onAction() {
1083
+ return local.onAction;
1084
+ },
1017
1085
  },
1018
- createComboBoxListStateAdapter(state),
1086
+ listState,
1019
1087
  );
1020
1088
 
1021
1089
  const isOptionFocusVisible = () =>
@@ -39,6 +39,7 @@ import {
39
39
  type CalendarState,
40
40
  type RangeCalendarState,
41
41
  type DateFieldStateProps,
42
+ type DatePickerStateOptions,
42
43
  type CalendarDate,
43
44
  type DateValue,
44
45
  type RangeCalendarStateProps,
@@ -349,11 +350,65 @@ function DatePickerInner<T extends DateValue = CalendarDate>(
349
350
  const [triggerRef, setTriggerRef] = createSignal<HTMLElement | null>(null);
350
351
  const [fieldRef, setFieldRef] = createSignal<HTMLDivElement | null>(null);
351
352
 
352
- // Unified state using createDatePickerState as single source of truth
353
- const datePickerState = createDatePickerState<T>({
354
- ...(stateProps as unknown as import("@proyecto-viviana/solid-stately").DatePickerStateOptions<T>),
355
- shouldCloseOnSelect: local.shouldCloseOnSelect,
356
- });
353
+ // Unified state using createDatePickerState as single source of truth.
354
+ // Use getters here so controlled props keep tracking after splitProps.
355
+ const datePickerStateProps = {
356
+ get value() {
357
+ return stateProps.value;
358
+ },
359
+ get defaultValue() {
360
+ return stateProps.defaultValue;
361
+ },
362
+ get onChange() {
363
+ return stateProps.onChange;
364
+ },
365
+ get minValue() {
366
+ return stateProps.minValue;
367
+ },
368
+ get maxValue() {
369
+ return stateProps.maxValue;
370
+ },
371
+ get isDisabled() {
372
+ return stateProps.isDisabled;
373
+ },
374
+ get isReadOnly() {
375
+ return stateProps.isReadOnly;
376
+ },
377
+ get isRequired() {
378
+ return stateProps.isRequired;
379
+ },
380
+ get granularity() {
381
+ return stateProps.granularity;
382
+ },
383
+ get hourCycle() {
384
+ return stateProps.hourCycle;
385
+ },
386
+ get hideTimeZone() {
387
+ return stateProps.hideTimeZone;
388
+ },
389
+ get placeholderValue() {
390
+ return stateProps.placeholderValue;
391
+ },
392
+ get shouldCloseOnSelect() {
393
+ return local.shouldCloseOnSelect;
394
+ },
395
+ get defaultOpen() {
396
+ return stateProps.defaultOpen;
397
+ },
398
+ get isOpen() {
399
+ return stateProps.isOpen;
400
+ },
401
+ get onOpenChange() {
402
+ return stateProps.onOpenChange;
403
+ },
404
+ get isDateUnavailable() {
405
+ return stateProps.isDateUnavailable;
406
+ },
407
+ get validationState() {
408
+ return stateProps.validationState;
409
+ },
410
+ } satisfies DatePickerStateOptions<T>;
411
+ const datePickerState = createDatePickerState<T>(datePickerStateProps);
357
412
 
358
413
  const overlayState = {
359
414
  get isOpen() {
package/src/Menu.tsx CHANGED
@@ -373,6 +373,7 @@ export function SubmenuTrigger(props: SubmenuTriggerProps): JSX.Element {
373
373
  const triggerId = createUniqueId();
374
374
  const menuId = createUniqueId();
375
375
  let hoverTimeout: number | undefined;
376
+ let hasPointerHover = false;
376
377
  const delay = () => props.delay ?? 200;
377
378
 
378
379
  const clearHoverTimeout = () => {
@@ -387,6 +388,16 @@ export function SubmenuTrigger(props: SubmenuTriggerProps): JSX.Element {
387
388
  state.open();
388
389
  };
389
390
 
391
+ const queueOpenSubmenu = () => {
392
+ clearHoverTimeout();
393
+ const open = () => state.open();
394
+ if (typeof queueMicrotask === "function") {
395
+ queueMicrotask(open);
396
+ } else {
397
+ Promise.resolve().then(open);
398
+ }
399
+ };
400
+
390
401
  const scheduleOpen = () => {
391
402
  clearHoverTimeout();
392
403
  hoverTimeout = window.setTimeout(() => {
@@ -395,6 +406,28 @@ export function SubmenuTrigger(props: SubmenuTriggerProps): JSX.Element {
395
406
  }, delay());
396
407
  };
397
408
 
409
+ const schedulePointerOpen = (event: PointerEvent) => {
410
+ hasPointerHover = true;
411
+ if (event.isTrusted === false) {
412
+ queueOpenSubmenu();
413
+ return;
414
+ }
415
+
416
+ scheduleOpen();
417
+ };
418
+
419
+ const openFromMouseHover = () => {
420
+ if (state.isOpen()) {
421
+ return;
422
+ }
423
+
424
+ if (hasPointerHover) {
425
+ scheduleOpen();
426
+ } else {
427
+ queueOpenSubmenu();
428
+ }
429
+ };
430
+
398
431
  onCleanup(clearHoverTimeout);
399
432
 
400
433
  const menuTriggerContext = createMemo<MenuTriggerContextValue>(() => ({
@@ -430,17 +463,22 @@ export function SubmenuTrigger(props: SubmenuTriggerProps): JSX.Element {
430
463
  props: () => ({
431
464
  id: triggerId,
432
465
  "aria-haspopup": "menu",
433
- "aria-expanded": state.isOpen() || undefined,
434
- "aria-controls": state.isOpen() ? menuId : undefined,
466
+ get "aria-expanded"() {
467
+ return state.isOpen() || undefined;
468
+ },
469
+ get "aria-controls"() {
470
+ return state.isOpen() ? menuId : undefined;
471
+ },
435
472
  onPointerEnter: (event: PointerEvent) => {
436
473
  if (event.pointerType === "touch") return;
437
- scheduleOpen();
474
+ schedulePointerOpen(event);
438
475
  },
439
476
  onPointerOver: (event: PointerEvent) => {
440
477
  if (event.pointerType === "touch") return;
441
- scheduleOpen();
478
+ schedulePointerOpen(event);
442
479
  },
443
- onMouseEnter: () => scheduleOpen(),
480
+ onMouseEnter: openFromMouseHover,
481
+ onMouseOver: openFromMouseHover,
444
482
  onKeyDown: (event: KeyboardEvent) => {
445
483
  if (event.key === "ArrowRight" || event.key === "Enter" || event.key === " ") {
446
484
  event.preventDefault();
@@ -1028,6 +1066,11 @@ export function Menu<T>(props: MenuProps<T>): JSX.Element {
1028
1066
  dndDropIndicator(index, position) ??
1029
1067
  parentCollectionRenderer?.renderDropIndicator?.(index, position),
1030
1068
  }));
1069
+ const menuItemContextValue = createMemo<MenuItemContextValue>(() =>
1070
+ local.shouldCloseOnSelect !== undefined
1071
+ ? { closeOnSelect: local.shouldCloseOnSelect }
1072
+ : {},
1073
+ );
1031
1074
  const menuListChildren = () => (
1032
1075
  <SharedElementTransition>
1033
1076
  {state.collection().size === 0 && !usesStaticChildren() && local.renderEmptyState ? (
@@ -1156,7 +1199,7 @@ export function Menu<T>(props: MenuProps<T>): JSX.Element {
1156
1199
  <StaticMenuCollectionContext.Provider
1157
1200
  value={usesStaticChildren() ? staticCollectionContext : null}
1158
1201
  >
1159
- <MenuItemContext.Provider value={{ closeOnSelect: local.shouldCloseOnSelect }}>
1202
+ <MenuItemContext.Provider value={menuItemContextValue()}>
1160
1203
  <CollectionRendererContext.Provider value={collectionRenderer()}>
1161
1204
  <>
1162
1205
  <Show when={ariaProps.label}>
package/src/Modal.tsx CHANGED
@@ -479,7 +479,14 @@ function ModalContent(
479
479
 
480
480
  const renderProps = useRenderProps(
481
481
  {
482
- children: props.children,
482
+ // Lazy: the modal content is gated behind `<Show when={isHydrated() && …}>`
483
+ // (Portal) below, so it must not be instantiated during the component body.
484
+ // An eager `children: props.children` would build the content template (and
485
+ // walk getNextElement) before the gate — content the server never emitted →
486
+ // hydration mismatch. See Popover for the full rationale.
487
+ get children() {
488
+ return props.children;
489
+ },
483
490
  class: local.class,
484
491
  style: local.style,
485
492
  defaultClassName: "solidaria-Modal",
@@ -525,9 +525,19 @@ export function NumberFieldIncrementButton(props: NumberFieldIncrementButtonProp
525
525
  }
526
526
 
527
527
  const isDisabled = () => context.isDisabled || !context.state.canIncrement();
528
+ const pressButtonProps = () => {
529
+ const {
530
+ onClick: _onClick,
531
+ disabled: _disabled,
532
+ type: _type,
533
+ tabIndex: _tabIndex,
534
+ ...rest
535
+ } = context.incrementButtonProps as Record<string, unknown>;
536
+ return rest;
537
+ };
528
538
 
529
539
  const buttonAria = createButton({
530
- ...(context.incrementButtonProps as Record<string, unknown>),
540
+ ...pressButtonProps(),
531
541
  elementType: "div",
532
542
  get isDisabled() {
533
543
  return isDisabled();
@@ -594,9 +604,19 @@ export function NumberFieldDecrementButton(props: NumberFieldDecrementButtonProp
594
604
  }
595
605
 
596
606
  const isDisabled = () => context.isDisabled || !context.state.canDecrement();
607
+ const pressButtonProps = () => {
608
+ const {
609
+ onClick: _onClick,
610
+ disabled: _disabled,
611
+ type: _type,
612
+ tabIndex: _tabIndex,
613
+ ...rest
614
+ } = context.decrementButtonProps as Record<string, unknown>;
615
+ return rest;
616
+ };
597
617
 
598
618
  const buttonAria = createButton({
599
- ...(context.decrementButtonProps as Record<string, unknown>),
619
+ ...pressButtonProps(),
600
620
  elementType: "div",
601
621
  get isDisabled() {
602
622
  return isDisabled();
package/src/Popover.tsx CHANGED
@@ -407,7 +407,16 @@ export function Popover(props: PopoverProps): JSX.Element {
407
407
 
408
408
  const renderProps = useRenderProps(
409
409
  {
410
- children: props.children,
410
+ // Read children lazily. The popover content is gated behind
411
+ // `<Show when={isHydrated() && …}>` below, so it must NOT be instantiated
412
+ // during the component body. An eager `children: props.children` reads the
413
+ // child getter at object-construction time, building the content template
414
+ // (and walking `getNextElement`) before the gate — which the server, with
415
+ // the gate closed, never emitted → hydration mismatch. The getter defers
416
+ // the read until `renderChildren()` runs inside the gated overlay.
417
+ get children() {
418
+ return props.children;
419
+ },
411
420
  class: local.class,
412
421
  style: local.style,
413
422
  defaultClassName: "solidaria-Popover",
@@ -471,6 +480,14 @@ export function Popover(props: PopoverProps): JSX.Element {
471
480
  };
472
481
 
473
482
  const shouldBeDialog = () => !local.isNonModal || resolvedTrigger() === "SubmenuTrigger";
483
+ const shouldContainFocus = () => {
484
+ if (!shouldBeDialog()) {
485
+ return false;
486
+ }
487
+
488
+ const trigger = resolvedTrigger();
489
+ return trigger !== "MenuTrigger" && trigger !== "SubmenuTrigger";
490
+ };
474
491
  const portalContext = useUNSAFE_PortalContext();
475
492
  const portalContainer = () => {
476
493
  if (isSubPopover()) {
@@ -538,7 +555,7 @@ export function Popover(props: PopoverProps): JSX.Element {
538
555
  <PopoverContext.Provider
539
556
  value={{ placement: popoverAria.placement, arrowProps: () => popoverAria.arrowProps }}
540
557
  >
541
- <FocusScope contain={shouldBeDialog()} restoreFocus>
558
+ <FocusScope contain={shouldContainFocus()} restoreFocus>
542
559
  <div
543
560
  {...domProps()}
544
561
  {...cleanPopoverProps()}
package/src/TagGroup.tsx CHANGED
@@ -365,6 +365,7 @@ export function Tag(props: TagProps): JSX.Element {
365
365
  get key() {
366
366
  return local.id;
367
367
  },
368
+ role: "row",
368
369
  get isDisabled() {
369
370
  return local.isDisabled || groupContext?.isDisabled;
370
371
  },