@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.
- package/README.md +4 -0
- package/components.css +63 -380
- package/dist/cjs/components/_internal/base-button.d.ts.map +1 -1
- package/dist/cjs/components/_internal/base-button.js +1 -1
- package/dist/cjs/components/_internal/base-button.js.map +3 -3
- package/dist/cjs/components/button.d.ts.map +1 -1
- package/dist/cjs/components/button.js +1 -1
- package/dist/cjs/components/button.js.map +3 -3
- package/dist/cjs/components/chatbar.d.ts.map +1 -1
- package/dist/cjs/components/chatbar.js.map +2 -2
- package/dist/cjs/components/icon-button.d.ts.map +1 -1
- package/dist/cjs/components/icon-button.js +2 -2
- package/dist/cjs/components/icon-button.js.map +3 -3
- package/dist/cjs/components/shell.d.ts.map +1 -1
- package/dist/cjs/components/shell.js +1 -1
- package/dist/cjs/components/shell.js.map +3 -3
- package/dist/cjs/components/toggle-button.d.ts.map +1 -1
- package/dist/cjs/components/toggle-button.js +1 -1
- package/dist/cjs/components/toggle-button.js.map +3 -3
- package/dist/cjs/components/toggle-icon-button.d.ts.map +1 -1
- package/dist/cjs/components/toggle-icon-button.js +1 -1
- package/dist/cjs/components/toggle-icon-button.js.map +3 -3
- package/dist/cjs/hooks/index.d.ts +2 -0
- package/dist/cjs/hooks/index.d.ts.map +1 -1
- package/dist/cjs/hooks/index.js +1 -1
- package/dist/cjs/hooks/index.js.map +3 -3
- package/dist/cjs/hooks/use-live-announcer.d.ts.map +1 -1
- package/dist/cjs/hooks/use-live-announcer.js +2 -2
- package/dist/cjs/hooks/use-live-announcer.js.map +3 -3
- package/dist/cjs/hooks/use-toggle-state.d.ts +37 -0
- package/dist/cjs/hooks/use-toggle-state.d.ts.map +1 -0
- package/dist/cjs/hooks/use-toggle-state.js +2 -0
- package/dist/cjs/hooks/use-toggle-state.js.map +7 -0
- package/dist/cjs/hooks/use-tooltip-wrapper.d.ts +29 -0
- package/dist/cjs/hooks/use-tooltip-wrapper.d.ts.map +1 -0
- package/dist/cjs/hooks/use-tooltip-wrapper.js +2 -0
- package/dist/cjs/hooks/use-tooltip-wrapper.js.map +7 -0
- package/dist/esm/components/_internal/base-button.d.ts.map +1 -1
- package/dist/esm/components/_internal/base-button.js +1 -1
- package/dist/esm/components/_internal/base-button.js.map +3 -3
- package/dist/esm/components/button.d.ts.map +1 -1
- package/dist/esm/components/button.js +1 -1
- package/dist/esm/components/button.js.map +3 -3
- package/dist/esm/components/chatbar.d.ts.map +1 -1
- package/dist/esm/components/chatbar.js.map +2 -2
- package/dist/esm/components/icon-button.d.ts.map +1 -1
- package/dist/esm/components/icon-button.js +2 -2
- package/dist/esm/components/icon-button.js.map +3 -3
- package/dist/esm/components/shell.d.ts.map +1 -1
- package/dist/esm/components/shell.js +1 -1
- package/dist/esm/components/shell.js.map +3 -3
- package/dist/esm/components/toggle-button.d.ts.map +1 -1
- package/dist/esm/components/toggle-button.js +1 -1
- package/dist/esm/components/toggle-button.js.map +3 -3
- package/dist/esm/components/toggle-icon-button.d.ts.map +1 -1
- package/dist/esm/components/toggle-icon-button.js +1 -1
- package/dist/esm/components/toggle-icon-button.js.map +3 -3
- package/dist/esm/hooks/index.d.ts +2 -0
- package/dist/esm/hooks/index.d.ts.map +1 -1
- package/dist/esm/hooks/index.js +1 -1
- package/dist/esm/hooks/index.js.map +3 -3
- package/dist/esm/hooks/use-live-announcer.d.ts.map +1 -1
- package/dist/esm/hooks/use-live-announcer.js +2 -2
- package/dist/esm/hooks/use-live-announcer.js.map +3 -3
- package/dist/esm/hooks/use-toggle-state.d.ts +37 -0
- package/dist/esm/hooks/use-toggle-state.d.ts.map +1 -0
- package/dist/esm/hooks/use-toggle-state.js +2 -0
- package/dist/esm/hooks/use-toggle-state.js.map +7 -0
- package/dist/esm/hooks/use-tooltip-wrapper.d.ts +29 -0
- package/dist/esm/hooks/use-tooltip-wrapper.d.ts.map +1 -0
- package/dist/esm/hooks/use-tooltip-wrapper.js +2 -0
- package/dist/esm/hooks/use-tooltip-wrapper.js.map +7 -0
- package/package.json +4 -4
- package/schemas/base-button.json +1 -1
- package/schemas/button.json +1 -1
- package/schemas/icon-button.json +1 -1
- package/schemas/index.json +6 -6
- package/schemas/toggle-button.json +1 -1
- package/schemas/toggle-icon-button.json +1 -1
- package/src/components/_internal/base-button.css +136 -614
- package/src/components/_internal/base-button.tsx +15 -13
- package/src/components/button.tsx +13 -42
- package/src/components/chatbar.tsx +1 -13
- package/src/components/icon-button.tsx +20 -44
- package/src/components/image.css +10 -8
- package/src/components/shell.tsx +13 -9
- package/src/components/toggle-button.tsx +30 -59
- package/src/components/toggle-icon-button.tsx +29 -51
- package/src/hooks/index.ts +2 -0
- package/src/hooks/use-live-announcer.ts +34 -7
- package/src/hooks/use-toggle-state.ts +72 -0
- package/src/hooks/use-tooltip-wrapper.ts +28 -0
- package/src/styles/tokens/color.css +11 -1
- package/styles.css +70 -381
- package/tokens/base.css +7 -1
- 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
|
-
|
|
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
|
-
//
|
|
129
|
-
const tooltipId =
|
|
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
|
|
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
|
|
153
|
+
return vars as React.CSSProperties;
|
|
171
154
|
}, [overrideStyles]);
|
|
172
155
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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 (!
|
|
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
|
-
//
|
|
118
|
-
const tooltipId =
|
|
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
|
-
//
|
|
117
|
+
// Validate accessible name - throw in development, log error in production
|
|
126
118
|
if (!hasAriaLabel && !hasAriaLabelledBy && !hasChildren) {
|
|
127
|
-
|
|
119
|
+
const errorMessage =
|
|
128
120
|
'IconButton: Icon buttons must have an accessible name. Please provide either:' +
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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 (!
|
|
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
|
);
|
package/src/components/image.css
CHANGED
|
@@ -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:
|
|
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.
|
|
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:
|
|
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.
|
|
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
|
package/src/components/shell.tsx
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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 {
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
//
|
|
112
|
-
const
|
|
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
|
-
|
|
132
|
-
|
|
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
|
-
}, [
|
|
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
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
);
|
package/src/hooks/index.ts
CHANGED