@kushagradhawan/kookie-ui 0.1.71 → 0.1.72

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 (96) hide show
  1. package/README.md +4 -0
  2. package/components.css +63 -380
  3. package/dist/cjs/components/_internal/base-button.d.ts.map +1 -1
  4. package/dist/cjs/components/_internal/base-button.js +1 -1
  5. package/dist/cjs/components/_internal/base-button.js.map +3 -3
  6. package/dist/cjs/components/button.d.ts.map +1 -1
  7. package/dist/cjs/components/button.js +1 -1
  8. package/dist/cjs/components/button.js.map +3 -3
  9. package/dist/cjs/components/chatbar.d.ts.map +1 -1
  10. package/dist/cjs/components/chatbar.js.map +2 -2
  11. package/dist/cjs/components/icon-button.d.ts.map +1 -1
  12. package/dist/cjs/components/icon-button.js +2 -2
  13. package/dist/cjs/components/icon-button.js.map +3 -3
  14. package/dist/cjs/components/shell.d.ts.map +1 -1
  15. package/dist/cjs/components/shell.js +1 -1
  16. package/dist/cjs/components/shell.js.map +3 -3
  17. package/dist/cjs/components/toggle-button.d.ts.map +1 -1
  18. package/dist/cjs/components/toggle-button.js +1 -1
  19. package/dist/cjs/components/toggle-button.js.map +3 -3
  20. package/dist/cjs/components/toggle-icon-button.d.ts.map +1 -1
  21. package/dist/cjs/components/toggle-icon-button.js +1 -1
  22. package/dist/cjs/components/toggle-icon-button.js.map +3 -3
  23. package/dist/cjs/hooks/index.d.ts +2 -0
  24. package/dist/cjs/hooks/index.d.ts.map +1 -1
  25. package/dist/cjs/hooks/index.js +1 -1
  26. package/dist/cjs/hooks/index.js.map +3 -3
  27. package/dist/cjs/hooks/use-live-announcer.d.ts.map +1 -1
  28. package/dist/cjs/hooks/use-live-announcer.js +2 -2
  29. package/dist/cjs/hooks/use-live-announcer.js.map +3 -3
  30. package/dist/cjs/hooks/use-toggle-state.d.ts +37 -0
  31. package/dist/cjs/hooks/use-toggle-state.d.ts.map +1 -0
  32. package/dist/cjs/hooks/use-toggle-state.js +2 -0
  33. package/dist/cjs/hooks/use-toggle-state.js.map +7 -0
  34. package/dist/cjs/hooks/use-tooltip-wrapper.d.ts +29 -0
  35. package/dist/cjs/hooks/use-tooltip-wrapper.d.ts.map +1 -0
  36. package/dist/cjs/hooks/use-tooltip-wrapper.js +2 -0
  37. package/dist/cjs/hooks/use-tooltip-wrapper.js.map +7 -0
  38. package/dist/esm/components/_internal/base-button.d.ts.map +1 -1
  39. package/dist/esm/components/_internal/base-button.js +1 -1
  40. package/dist/esm/components/_internal/base-button.js.map +3 -3
  41. package/dist/esm/components/button.d.ts.map +1 -1
  42. package/dist/esm/components/button.js +1 -1
  43. package/dist/esm/components/button.js.map +3 -3
  44. package/dist/esm/components/chatbar.d.ts.map +1 -1
  45. package/dist/esm/components/chatbar.js.map +2 -2
  46. package/dist/esm/components/icon-button.d.ts.map +1 -1
  47. package/dist/esm/components/icon-button.js +2 -2
  48. package/dist/esm/components/icon-button.js.map +3 -3
  49. package/dist/esm/components/shell.d.ts.map +1 -1
  50. package/dist/esm/components/shell.js +1 -1
  51. package/dist/esm/components/shell.js.map +3 -3
  52. package/dist/esm/components/toggle-button.d.ts.map +1 -1
  53. package/dist/esm/components/toggle-button.js +1 -1
  54. package/dist/esm/components/toggle-button.js.map +3 -3
  55. package/dist/esm/components/toggle-icon-button.d.ts.map +1 -1
  56. package/dist/esm/components/toggle-icon-button.js +1 -1
  57. package/dist/esm/components/toggle-icon-button.js.map +3 -3
  58. package/dist/esm/hooks/index.d.ts +2 -0
  59. package/dist/esm/hooks/index.d.ts.map +1 -1
  60. package/dist/esm/hooks/index.js +1 -1
  61. package/dist/esm/hooks/index.js.map +3 -3
  62. package/dist/esm/hooks/use-live-announcer.d.ts.map +1 -1
  63. package/dist/esm/hooks/use-live-announcer.js +2 -2
  64. package/dist/esm/hooks/use-live-announcer.js.map +3 -3
  65. package/dist/esm/hooks/use-toggle-state.d.ts +37 -0
  66. package/dist/esm/hooks/use-toggle-state.d.ts.map +1 -0
  67. package/dist/esm/hooks/use-toggle-state.js +2 -0
  68. package/dist/esm/hooks/use-toggle-state.js.map +7 -0
  69. package/dist/esm/hooks/use-tooltip-wrapper.d.ts +29 -0
  70. package/dist/esm/hooks/use-tooltip-wrapper.d.ts.map +1 -0
  71. package/dist/esm/hooks/use-tooltip-wrapper.js +2 -0
  72. package/dist/esm/hooks/use-tooltip-wrapper.js.map +7 -0
  73. package/package.json +4 -4
  74. package/schemas/base-button.json +1 -1
  75. package/schemas/button.json +1 -1
  76. package/schemas/icon-button.json +1 -1
  77. package/schemas/index.json +6 -6
  78. package/schemas/toggle-button.json +1 -1
  79. package/schemas/toggle-icon-button.json +1 -1
  80. package/src/components/_internal/base-button.css +136 -614
  81. package/src/components/_internal/base-button.tsx +15 -13
  82. package/src/components/button.tsx +13 -42
  83. package/src/components/chatbar.tsx +1 -13
  84. package/src/components/icon-button.tsx +20 -44
  85. package/src/components/image.css +10 -8
  86. package/src/components/shell.tsx +13 -9
  87. package/src/components/toggle-button.tsx +30 -59
  88. package/src/components/toggle-icon-button.tsx +29 -51
  89. package/src/hooks/index.ts +2 -0
  90. package/src/hooks/use-live-announcer.ts +34 -7
  91. package/src/hooks/use-toggle-state.ts +72 -0
  92. package/src/hooks/use-tooltip-wrapper.ts +28 -0
  93. package/src/styles/tokens/color.css +11 -1
  94. package/styles.css +70 -381
  95. package/tokens/base.css +7 -1
  96. package/tokens.css +7 -1
@@ -83,9 +83,7 @@ const BaseButton = React.forwardRef<BaseButtonElement, BaseButtonProps>((props,
83
83
  // This helps developers migrate to the new material prop
84
84
  React.useEffect(() => {
85
85
  if (props.panelBackground !== undefined) {
86
- console.warn(
87
- 'Warning: The `panelBackground` prop is deprecated and will be removed in a future version. Use `material` prop instead.',
88
- );
86
+ console.warn('Warning: The `panelBackground` prop is deprecated and will be removed in a future version. Use `material` prop instead.');
89
87
  }
90
88
  }, [props.panelBackground]);
91
89
 
@@ -96,6 +94,10 @@ const BaseButton = React.forwardRef<BaseButtonElement, BaseButtonProps>((props,
96
94
  // This prevents layout thrashing when using translucent materials
97
95
  const buttonRef = React.useRef<HTMLElement>(null);
98
96
 
97
+ // Use a ref to track current material value to avoid stale closures in setTimeout
98
+ const materialRef = React.useRef(effectiveMaterial);
99
+ materialRef.current = effectiveMaterial;
100
+
99
101
  React.useEffect(() => {
100
102
  const button = buttonRef.current;
101
103
  if (!button) return;
@@ -106,14 +108,17 @@ const BaseButton = React.forwardRef<BaseButtonElement, BaseButtonProps>((props,
106
108
  // Add will-change when material is translucent to optimize rendering
107
109
  button.style.setProperty('will-change', 'backdrop-filter');
108
110
 
111
+ // Track timeout for cleanup
112
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
113
+
109
114
  // Clean up will-change after transition completes to prevent memory leaks
110
115
  const cleanup = () => {
111
- const transitionDuration =
112
- getComputedStyle(button).getPropertyValue('--duration-2') || '75ms';
116
+ const transitionDuration = getComputedStyle(button).getPropertyValue('--duration-2') || '75ms';
113
117
  const duration = parseInt(transitionDuration) || 75;
114
118
 
115
- setTimeout(() => {
116
- if (button && effectiveMaterial !== 'translucent') {
119
+ timeoutId = setTimeout(() => {
120
+ // Use ref to get current value, not stale closure value
121
+ if (button && materialRef.current !== 'translucent') {
117
122
  button.style.setProperty('will-change', 'auto');
118
123
  }
119
124
  }, duration);
@@ -124,6 +129,7 @@ const BaseButton = React.forwardRef<BaseButtonElement, BaseButtonProps>((props,
124
129
  observer.observe(button, { attributes: true, attributeFilter: ['data-material'] });
125
130
 
126
131
  return () => {
132
+ if (timeoutId) clearTimeout(timeoutId);
127
133
  observer.disconnect();
128
134
  button.style.setProperty('will-change', 'auto');
129
135
  };
@@ -138,8 +144,7 @@ const BaseButton = React.forwardRef<BaseButtonElement, BaseButtonProps>((props,
138
144
 
139
145
  // Only pass disabled for elements that support it
140
146
  // This prevents invalid HTML attributes on unsupported elements
141
- const shouldPassDisabled =
142
- asChild || !as || ['button', 'input', 'textarea', 'select'].includes(as);
147
+ const shouldPassDisabled = asChild || !as || ['button', 'input', 'textarea', 'select'].includes(as);
143
148
 
144
149
  // Determine if we are rendering a real <button> element so we can set a safe
145
150
  // default type. Native <button> defaults to type="submit" which can cause
@@ -221,10 +226,7 @@ const BaseButton = React.forwardRef<BaseButtonElement, BaseButtonProps>((props,
221
226
  {/* Centered spinner overlay during loading state */}
222
227
  <Flex asChild align="center" justify="center" position="absolute" inset="0">
223
228
  <span>
224
- <Spinner
225
- size={mapResponsiveProp(size, mapButtonSizeToSpinnerSize)}
226
- aria-hidden="true"
227
- />
229
+ <Spinner size={mapResponsiveProp(size, mapButtonSizeToSpinnerSize)} aria-hidden="true" />
228
230
  </span>
229
231
  </Flex>
230
232
  </>
@@ -3,6 +3,7 @@ import classNames from 'classnames';
3
3
 
4
4
  import { BaseButton } from './_internal/base-button.js';
5
5
  import { Tooltip } from './tooltip.js';
6
+ import { useTooltipWrapper } from '../hooks/use-tooltip-wrapper.js';
6
7
  import type { BaseButtonProps } from './_internal/base-button.js';
7
8
 
8
9
  /**
@@ -84,9 +85,7 @@ type ButtonProps<C extends React.ElementType = 'button'> = ButtonOwnProps & {
84
85
  * Button component type that supports polymorphic rendering
85
86
  * @template C - The element type to render as
86
87
  */
87
- type ButtonComponent = <C extends React.ElementType = 'button'>(
88
- props: ButtonProps<C> & { ref?: React.ForwardedRef<ButtonElement> },
89
- ) => React.ReactElement | null;
88
+ type ButtonComponent = <C extends React.ElementType = 'button'>(props: ButtonProps<C> & { ref?: React.ForwardedRef<ButtonElement> }) => React.ReactElement | null;
90
89
 
91
90
  /**
92
91
  * Button component for triggering actions throughout your interface
@@ -113,27 +112,11 @@ type ButtonComponent = <C extends React.ElementType = 'button'>(
113
112
  */
114
113
  const Button = React.forwardRef(
115
114
  (
116
- {
117
- className,
118
- tooltip,
119
- tooltipSide = 'top',
120
- tooltipAlign = 'center',
121
- tooltipDelayDuration,
122
- tooltipDisableHoverableContent,
123
- overrideStyles,
124
- ...props
125
- }: ButtonProps,
115
+ { className, style, tooltip, tooltipSide = 'top', tooltipAlign = 'center', tooltipDelayDuration, tooltipDisableHoverableContent, overrideStyles, ...props }: ButtonProps,
126
116
  forwardedRef: React.ForwardedRef<ButtonElement>,
127
117
  ) => {
128
- // Generate unique ID for tooltip accessibility
129
- const tooltipId = React.useId();
130
- const hasTooltip = Boolean(tooltip);
131
-
132
- // Prepare accessibility props for tooltip integration
133
- const tooltipAccessibilityProps = React.useMemo(
134
- () => (hasTooltip ? { 'aria-describedby': tooltipId } : {}),
135
- [hasTooltip, tooltipId],
136
- );
118
+ // Use shared tooltip wrapper hook for accessibility props
119
+ const { tooltipId, hasTooltip, accessibilityProps: tooltipAccessibilityProps } = useTooltipWrapper(tooltip);
137
120
 
138
121
  // Create the base button element with tooltip accessibility props
139
122
  // Map overrideStyles to CSS variables consumed by the override variant rules
@@ -152,7 +135,7 @@ const Button = React.forwardRef(
152
135
  setVar(`--button-override-${prefix}filter`, s.filter);
153
136
  setVar(`--button-override-${prefix}outline`, s.outline);
154
137
  setVar(`--button-override-${prefix}outline-offset`, s.outlineOffset);
155
- setVar(`--button-override-${prefix}opacity`, s.opacity as any);
138
+ setVar(`--button-override-${prefix}opacity`, s.opacity);
156
139
  };
157
140
 
158
141
  apply('', overrideStyles.normal);
@@ -167,35 +150,23 @@ const Button = React.forwardRef(
167
150
  setVar('--button-override-focus-outline-offset', overrideStyles.focus.outlineOffset);
168
151
  }
169
152
 
170
- return vars as unknown as React.CSSProperties;
153
+ return vars as React.CSSProperties;
171
154
  }, [overrideStyles]);
172
155
 
173
- const button = (
174
- <BaseButton
175
- {...props}
176
- {...tooltipAccessibilityProps}
177
- ref={forwardedRef}
178
- className={classNames('rt-Button', className)}
179
- style={overrideVars ? { ...overrideVars, ...(props as any).style } : (props as any).style}
180
- />
181
- );
156
+ // Combine override styles with user-provided styles
157
+ const combinedStyle = React.useMemo(() => (overrideVars ? { ...overrideVars, ...style } : style), [overrideVars, style]);
158
+
159
+ const button = <BaseButton {...props} {...tooltipAccessibilityProps} ref={forwardedRef} className={classNames('rt-Button', className)} style={combinedStyle} />;
182
160
 
183
161
  // If no tooltip is provided, return the button as-is for better performance
184
- if (!tooltip) {
162
+ if (!hasTooltip) {
185
163
  return button;
186
164
  }
187
165
 
188
166
  // Wrap with Tooltip when tooltip content is provided
189
167
  // This creates a compound component that handles both button and tooltip functionality
190
168
  return (
191
- <Tooltip
192
- content={tooltip}
193
- side={tooltipSide}
194
- align={tooltipAlign}
195
- delayDuration={tooltipDelayDuration}
196
- disableHoverableContent={tooltipDisableHoverableContent}
197
- id={tooltipId}
198
- >
169
+ <Tooltip content={tooltip} side={tooltipSide} align={tooltipAlign} delayDuration={tooltipDelayDuration} disableHoverableContent={tooltipDisableHoverableContent} id={tooltipId}>
199
170
  {button}
200
171
  </Tooltip>
201
172
  );
@@ -1130,19 +1130,7 @@ type SendProps = Omit<IconButtonProps, 'aria-label' | 'aria-labelledby'> & {
1130
1130
  };
1131
1131
 
1132
1132
  const Send = React.forwardRef<HTMLButtonElement, SendProps>((props, forwardedRef) => {
1133
- const {
1134
- asChild,
1135
- clearOnSend = true,
1136
- disabled,
1137
- children,
1138
- className,
1139
- style,
1140
- size: sizeProp,
1141
- variant: variantProp,
1142
- 'aria-label': ariaLabel,
1143
- 'aria-labelledby': ariaLabelledby,
1144
- ...buttonProps
1145
- } = props;
1133
+ const { asChild, clearOnSend = true, disabled, children, className, style, size: sizeProp, variant: variantProp, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, ...buttonProps } = props;
1146
1134
  const ctx = useChatbarContext();
1147
1135
 
1148
1136
  const trimmed = ctx.value.trim();
@@ -3,6 +3,7 @@ import classNames from 'classnames';
3
3
 
4
4
  import { BaseButton } from './_internal/base-button.js';
5
5
  import { Tooltip } from './tooltip.js';
6
+ import { useTooltipWrapper } from '../hooks/use-tooltip-wrapper.js';
6
7
  import type { BaseButtonProps } from './_internal/base-button.js';
7
8
 
8
9
  type IconButtonElement = React.ElementRef<typeof BaseButton>;
@@ -57,9 +58,7 @@ type IconButtonProps<C extends React.ElementType = 'button'> = IconButtonOwnProp
57
58
  * IconButton component type that supports polymorphic rendering
58
59
  * @template C - The element type to render as
59
60
  */
60
- type IconButtonComponent = <C extends React.ElementType = 'button'>(
61
- props: IconButtonProps<C> & { ref?: React.ForwardedRef<IconButtonElement> },
62
- ) => React.ReactElement | null;
61
+ type IconButtonComponent = <C extends React.ElementType = 'button'>(props: IconButtonProps<C> & { ref?: React.ForwardedRef<IconButtonElement> }) => React.ReactElement | null;
63
62
 
64
63
  /**
65
64
  * IconButton component for compact, accessible icon-only interactions
@@ -103,68 +102,45 @@ type IconButtonComponent = <C extends React.ElementType = 'button'>(
103
102
  */
104
103
  const IconButton = React.forwardRef(
105
104
  (
106
- {
107
- className,
108
- tooltip,
109
- tooltipSide = 'top',
110
- tooltipAlign = 'center',
111
- tooltipDelayDuration,
112
- tooltipDisableHoverableContent,
113
- ...props
114
- }: IconButtonProps,
105
+ { className, tooltip, tooltipSide = 'top', tooltipAlign = 'center', tooltipDelayDuration, tooltipDisableHoverableContent, ...props }: IconButtonProps,
115
106
  forwardedRef: React.ForwardedRef<IconButtonElement>,
116
107
  ) => {
117
- // Generate unique ID for tooltip accessibility
118
- const tooltipId = React.useId();
108
+ // Use shared tooltip wrapper hook for accessibility props
109
+ const { tooltipId, hasTooltip, accessibilityProps: tooltipAccessibilityProps } = useTooltipWrapper(tooltip);
110
+
119
111
  // Runtime accessibility validation to ensure WCAG compliance
120
112
  // This helps catch accessibility issues during development
121
113
  const hasAriaLabel = 'aria-label' in props && props['aria-label'];
122
114
  const hasAriaLabelledBy = 'aria-labelledby' in props && props['aria-labelledby'];
123
115
  const hasChildren = 'children' in props && props.children;
124
116
 
125
- // Throw descriptive error if no accessible name is provided
117
+ // Validate accessible name - throw in development, log error in production
126
118
  if (!hasAriaLabel && !hasAriaLabelledBy && !hasChildren) {
127
- throw new Error(
119
+ const errorMessage =
128
120
  'IconButton: Icon buttons must have an accessible name. Please provide either:' +
129
- '\n- aria-label prop with descriptive text' +
130
- '\n- aria-labelledby prop referencing a label element' +
131
- '\n- or visible text children',
132
- );
133
- }
121
+ '\n- aria-label prop with descriptive text' +
122
+ '\n- aria-labelledby prop referencing a label element' +
123
+ '\n- or visible text children';
134
124
 
135
- // Prepare accessibility props for tooltip integration
136
- const hasTooltip = Boolean(tooltip);
137
- const tooltipAccessibilityProps = React.useMemo(
138
- () => (hasTooltip ? { 'aria-describedby': tooltipId } : {}),
139
- [hasTooltip, tooltipId],
140
- );
125
+ if (process.env.NODE_ENV === 'development') {
126
+ throw new Error(errorMessage);
127
+ } else {
128
+ console.error(errorMessage);
129
+ }
130
+ }
141
131
 
142
132
  // Create the base icon button element with accessibility props
143
- const iconButton = (
144
- <BaseButton
145
- {...props}
146
- {...tooltipAccessibilityProps}
147
- ref={forwardedRef}
148
- className={classNames('rt-IconButton', className)}
149
- />
150
- );
133
+ const iconButton = <BaseButton {...props} {...tooltipAccessibilityProps} ref={forwardedRef} className={classNames('rt-IconButton', className)} />;
151
134
 
152
135
  // If no tooltip is provided, return the icon button as-is for better performance
153
- if (!tooltip) {
136
+ if (!hasTooltip) {
154
137
  return iconButton;
155
138
  }
156
139
 
157
140
  // Wrap with Tooltip when tooltip content is provided
158
141
  // This creates a compound component that handles both button and tooltip functionality
159
142
  return (
160
- <Tooltip
161
- content={tooltip}
162
- side={tooltipSide}
163
- align={tooltipAlign}
164
- delayDuration={tooltipDelayDuration}
165
- disableHoverableContent={tooltipDisableHoverableContent}
166
- id={tooltipId}
167
- >
143
+ <Tooltip content={tooltip} side={tooltipSide} align={tooltipAlign} delayDuration={tooltipDelayDuration} disableHoverableContent={tooltipDisableHoverableContent} id={tooltipId}>
168
144
  {iconButton}
169
145
  </Tooltip>
170
146
  );
@@ -63,7 +63,7 @@
63
63
  .rt-Image {
64
64
  border: 1px solid CanvasText;
65
65
  }
66
-
66
+
67
67
  /* Enhanced focus visibility in forced colors mode */
68
68
  .rt-Image:where(:focus-visible) {
69
69
  outline: 2px solid Highlight;
@@ -78,7 +78,9 @@
78
78
  */
79
79
  .rt-Image:where(:any-link, button, label) {
80
80
  cursor: pointer;
81
- transition: var(--transition-card), filter var(--motion-duration-small) var(--motion-ease-standard); /* Smooth transitions for interactive states */
81
+ transition:
82
+ var(--transition-card),
83
+ filter var(--motion-duration-small) var(--motion-ease-standard); /* Smooth transitions for interactive states */
82
84
 
83
85
  /*
84
86
  * Hover effects with progressive enhancement
@@ -87,7 +89,7 @@
87
89
  @media (hover: hover) {
88
90
  &:where(:hover) {
89
91
  box-shadow: var(--shadow-3); /* Subtle elevation on hover */
90
- filter: brightness(1.05) contrast(1.02); /* Slight brightness/contrast boost */
92
+ filter: brightness(1.08) contrast(1.02); /* Slight brightness/contrast boost */
91
93
  }
92
94
  }
93
95
 
@@ -116,13 +118,15 @@
116
118
  */
117
119
  :where(:any-link, button, label) {
118
120
  cursor: pointer;
119
- transition: var(--transition-card), filter var(--motion-duration-small) var(--motion-ease-standard); /* Smooth transitions for interactive states */
121
+ transition:
122
+ var(--transition-card),
123
+ filter var(--motion-duration-small) var(--motion-ease-standard); /* Smooth transitions for interactive states */
120
124
 
121
125
  /* Hover effects for wrapper elements */
122
126
  @media (hover: hover) {
123
127
  &:where(:hover) {
124
128
  /* box-shadow: var(--shadow-3); */
125
- filter: brightness(1.05) contrast(1.02);
129
+ filter: brightness(1.08) contrast(1.02);
126
130
  }
127
131
  }
128
132
 
@@ -141,8 +145,6 @@
141
145
  outline-offset: -2px;
142
146
  }
143
147
 
144
-
145
-
146
148
  /*
147
149
  * Object-fit variants for responsive image scaling
148
150
  * These classes control how images are resized to fit their containers
@@ -217,7 +219,7 @@
217
219
  color: var(--gray-11); /* Subtle but readable color */
218
220
  margin-top: var(--space-2); /* Consistent spacing from image */
219
221
  text-align: center; /* Centered alignment for visual balance */
220
-
222
+
221
223
  /*
222
224
  * Text wrapping and hyphenation for better layout
223
225
  * Prevents caption text from breaking the layout
@@ -450,17 +450,21 @@ const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(({ className, chil
450
450
  const peekCtxValue = React.useMemo(() => ({ peekTarget, setPeekTarget, peekPane, clearPeek }), [peekTarget, setPeekTarget, peekPane, clearPeek]);
451
451
  const actionsCtxValue = React.useMemo(() => ({ togglePane, expandPane, collapsePane, setSidebarToggleComputer }), [togglePane, expandPane, collapsePane, setSidebarToggleComputer]);
452
452
 
453
+ // Memoized full context value for ShellProvider to prevent unnecessary effect re-runs
454
+ const shellContextValue = React.useMemo(
455
+ () => ({
456
+ ...baseContextValue,
457
+ peekTarget,
458
+ setPeekTarget,
459
+ peekPane,
460
+ clearPeek,
461
+ }),
462
+ [baseContextValue, peekTarget, setPeekTarget, peekPane, clearPeek],
463
+ );
464
+
453
465
  return (
454
466
  <div {...props} ref={ref} className={classNames('rt-ShellRoot', className)} style={{ ...heightStyle, ...props.style }}>
455
- <ShellProvider
456
- value={{
457
- ...baseContextValue,
458
- peekTarget,
459
- setPeekTarget,
460
- peekPane,
461
- clearPeek,
462
- }}
463
- >
467
+ <ShellProvider value={shellContextValue}>
464
468
  <PresentationContext.Provider value={presentationCtxValue}>
465
469
  <LeftModeContext.Provider value={leftModeCtxValue}>
466
470
  <PanelModeContext.Provider value={panelModeCtxValue}>
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import { Toggle } from 'radix-ui';
3
3
  import { Button } from './button.js';
4
- import { useLiveAnnouncer } from '../hooks/use-live-announcer.js';
4
+ import { useToggleState } from '../hooks/use-toggle-state.js';
5
5
 
6
6
  /**
7
7
  * ToggleButton props that extend Button with toggle-specific functionality
@@ -63,66 +63,37 @@ type ToggleButtonElement = React.ElementRef<typeof Button>;
63
63
  * </ToggleButton>
64
64
  * ```
65
65
  */
66
- const ToggleButton = React.forwardRef<ToggleButtonElement, ToggleButtonProps>(
67
- ({ pressed, onPressedChange, defaultPressed, children, ...buttonProps }, forwardedRef) => {
68
- // Get the live announcer for accessibility announcements
69
- const announce = useLiveAnnouncer();
66
+ const ToggleButton = React.forwardRef<ToggleButtonElement, ToggleButtonProps>(({ pressed, onPressedChange, defaultPressed, children, ...buttonProps }, forwardedRef) => {
67
+ /**
68
+ * Extract accessible name from button content for announcements.
69
+ * This ensures screen readers announce meaningful state changes.
70
+ */
71
+ const getAccessibleName = React.useCallback(() => {
72
+ if (typeof children === 'string') return children;
73
+ if (React.isValidElement(children) && typeof (children.props as any)?.children === 'string') {
74
+ return (children.props as any).children;
75
+ }
76
+ return 'Toggle button';
77
+ }, [children]);
70
78
 
71
- /**
72
- * Extract accessible name from button content for announcements
73
- * This ensures screen readers announce meaningful state changes
74
- */
75
- const getAccessibleName = React.useCallback(() => {
76
- if (typeof children === 'string') return children;
77
- if (React.isValidElement(children) && typeof (children.props as any)?.children === 'string') {
78
- return (children.props as any).children;
79
- }
80
- return 'Toggle button';
81
- }, [children]);
79
+ // Use shared toggle state hook for accessibility announcements and warnings
80
+ const { handlePressedChange } = useToggleState({
81
+ pressed,
82
+ onPressedChange,
83
+ getAccessibleName,
84
+ componentName: 'ToggleButton',
85
+ });
82
86
 
83
- /**
84
- * Memoized handler for state changes with accessibility announcements
85
- * This ensures screen readers announce the new state immediately
86
- */
87
- const handlePressedChange = React.useCallback(
88
- (newPressed: boolean) => {
89
- const accessibleName = getAccessibleName();
90
- // Announce the state change for screen readers
91
- announce(`${accessibleName} ${newPressed ? 'pressed' : 'unpressed'}`);
92
- // Call the user's change handler
93
- onPressedChange?.(newPressed);
94
- },
95
- [announce, onPressedChange, getAccessibleName],
96
- );
97
-
98
- // Development-only warning for controlled/uncontrolled pattern
99
- // This helps developers avoid common state management mistakes
100
- React.useEffect(() => {
101
- if (process.env.NODE_ENV === 'development' && pressed !== undefined && onPressedChange === undefined) {
102
- console.warn(
103
- 'ToggleButton: You provided a `pressed` prop without an `onPressedChange` handler. ' +
104
- 'This will result in a read-only toggle button. If you want the button to be interactive, ' +
105
- 'you should provide an `onPressedChange` handler.',
106
- );
107
- }
108
- }, [pressed, onPressedChange]);
109
-
110
- // Render the toggle button using Radix UI's Toggle primitive
111
- // This provides proper ARIA attributes and keyboard navigation
112
- return (
113
- <Toggle.Root
114
- pressed={pressed}
115
- onPressedChange={handlePressedChange}
116
- defaultPressed={defaultPressed}
117
- asChild
118
- >
119
- <Button {...buttonProps} ref={forwardedRef}>
120
- {children}
121
- </Button>
122
- </Toggle.Root>
123
- );
124
- },
125
- );
87
+ // Render the toggle button using Radix UI's Toggle primitive
88
+ // This provides proper ARIA attributes and keyboard navigation
89
+ return (
90
+ <Toggle.Root pressed={pressed} onPressedChange={handlePressedChange} defaultPressed={defaultPressed} asChild>
91
+ <Button {...buttonProps} ref={forwardedRef}>
92
+ {children}
93
+ </Button>
94
+ </Toggle.Root>
95
+ );
96
+ });
126
97
  ToggleButton.displayName = 'ToggleButton';
127
98
 
128
99
  export { ToggleButton };
@@ -2,7 +2,7 @@ import * as React from 'react';
2
2
  import { Toggle } from 'radix-ui';
3
3
  import { IconButton } from './icon-button.js';
4
4
  import { BaseButton } from './_internal/base-button.js';
5
- import { useLiveAnnouncer } from '../hooks/use-live-announcer.js';
5
+ import { useToggleState } from '../hooks/use-toggle-state.js';
6
6
  import type { IconButtonProps } from './icon-button.js';
7
7
 
8
8
  type ToggleIconButtonElement = React.ElementRef<typeof BaseButton>;
@@ -104,32 +104,34 @@ type ToggleIconButtonPropsWithAccessibility = ToggleIconButtonProps & Accessibil
104
104
  * <span id="notifications-label">Toggle notifications</span>
105
105
  * ```
106
106
  */
107
- const ToggleIconButton = React.forwardRef<
108
- ToggleIconButtonElement,
109
- ToggleIconButtonPropsWithAccessibility
110
- >(({ pressed, onPressedChange, defaultPressed, ...iconButtonProps }, forwardedRef) => {
111
- // Get the live announcer for accessibility announcements
112
- const announce = useLiveAnnouncer();
107
+ const ToggleIconButton = React.forwardRef<ToggleIconButtonElement, ToggleIconButtonPropsWithAccessibility>(({ pressed, onPressedChange, defaultPressed, ...iconButtonProps }, forwardedRef) => {
108
+ // Extract specific props for stable dependency array
109
+ const { 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, children } = iconButtonProps;
110
+
111
+ // Cache the label lookup from aria-labelledby to avoid repeated DOM queries
112
+ const cachedLabelRef = React.useRef<string | null>(null);
113
+
114
+ // Clear cached label when ariaLabelledBy changes
115
+ React.useEffect(() => {
116
+ cachedLabelRef.current = null;
117
+ }, [ariaLabelledBy]);
113
118
 
114
119
  /**
115
- * Extract accessible name from various sources for announcements
116
- * This ensures screen readers announce meaningful state changes
120
+ * Extract accessible name from various sources for announcements.
121
+ * This ensures screen readers announce meaningful state changes.
117
122
  * Priority: aria-label > aria-labelledby > children > fallback
118
123
  */
119
124
  const getAccessibleName = React.useCallback(() => {
120
- const {
121
- 'aria-label': ariaLabel,
122
- 'aria-labelledby': ariaLabelledBy,
123
- children,
124
- } = iconButtonProps;
125
-
126
125
  // First priority: direct aria-label
127
126
  if (ariaLabel) return ariaLabel;
128
127
 
129
- // Second priority: referenced label element
128
+ // Second priority: referenced label element (cached)
130
129
  if (ariaLabelledBy) {
131
- const labelElement = document.getElementById(ariaLabelledBy);
132
- return labelElement?.textContent || 'Toggle icon button';
130
+ if (cachedLabelRef.current === null) {
131
+ const labelElement = document.getElementById(ariaLabelledBy);
132
+ cachedLabelRef.current = labelElement?.textContent || 'Toggle icon button';
133
+ }
134
+ return cachedLabelRef.current;
133
135
  }
134
136
 
135
137
  // Third priority: visible text children
@@ -140,45 +142,21 @@ const ToggleIconButton = React.forwardRef<
140
142
 
141
143
  // Fallback for edge cases
142
144
  return 'Toggle icon button';
143
- }, [iconButtonProps]);
144
-
145
- /**
146
- * Memoized handler for state changes with accessibility announcements
147
- * This ensures screen readers announce the new state immediately
148
- */
149
- const handlePressedChange = React.useCallback(
150
- (newPressed: boolean) => {
151
- const accessibleName = getAccessibleName();
152
- // Announce the state change for screen readers
153
- announce(`${accessibleName} ${newPressed ? 'pressed' : 'unpressed'}`);
154
- // Call the user's change handler
155
- onPressedChange?.(newPressed);
156
- },
157
- [announce, onPressedChange, getAccessibleName],
158
- );
145
+ }, [ariaLabel, ariaLabelledBy, children]);
159
146
 
160
- // Development-only warning for controlled/uncontrolled pattern
161
- // This helps developers avoid common state management mistakes
162
- React.useEffect(() => {
163
- if (process.env.NODE_ENV === 'development' && pressed !== undefined && onPressedChange === undefined) {
164
- console.warn(
165
- 'ToggleIconButton: You provided a `pressed` prop without an `onPressedChange` handler. ' +
166
- 'This will result in a read-only toggle button. If you want the button to be interactive, ' +
167
- 'you should provide an `onPressedChange` handler.',
168
- );
169
- }
170
- }, [pressed, onPressedChange]);
147
+ // Use shared toggle state hook for accessibility announcements and warnings
148
+ const { handlePressedChange } = useToggleState({
149
+ pressed,
150
+ onPressedChange,
151
+ getAccessibleName,
152
+ componentName: 'ToggleIconButton',
153
+ });
171
154
 
172
155
  // Render the toggle icon button using Radix UI's Toggle primitive
173
156
  // This provides proper ARIA attributes and keyboard navigation
174
157
  // The IconButton component handles accessibility validation internally
175
158
  return (
176
- <Toggle.Root
177
- pressed={pressed}
178
- onPressedChange={handlePressedChange}
179
- defaultPressed={defaultPressed}
180
- asChild
181
- >
159
+ <Toggle.Root pressed={pressed} onPressedChange={handlePressedChange} defaultPressed={defaultPressed} asChild>
182
160
  <IconButton {...iconButtonProps} ref={forwardedRef} />
183
161
  </Toggle.Root>
184
162
  );
@@ -1,2 +1,4 @@
1
1
  export { useLiveAnnouncer } from './use-live-announcer.js';
2
2
  export { useBodyPointerEventsCleanup } from './use-body-pointer-events-cleanup.js';
3
+ export { useTooltipWrapper } from './use-tooltip-wrapper.js';
4
+ export { useToggleState } from './use-toggle-state.js';