@kushagradhawan/kookie-ui 0.1.70 → 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/_internal/shell-bottom.d.ts +2 -21
- package/dist/cjs/components/_internal/shell-bottom.d.ts.map +1 -1
- package/dist/cjs/components/_internal/shell-bottom.js +1 -1
- package/dist/cjs/components/_internal/shell-bottom.js.map +3 -3
- package/dist/cjs/components/_internal/shell-inspector.d.ts +10 -21
- package/dist/cjs/components/_internal/shell-inspector.d.ts.map +1 -1
- package/dist/cjs/components/_internal/shell-inspector.js +1 -1
- package/dist/cjs/components/_internal/shell-inspector.js.map +3 -3
- package/dist/cjs/components/_internal/shell-prop-helpers.d.ts +7 -0
- package/dist/cjs/components/_internal/shell-prop-helpers.d.ts.map +1 -0
- package/dist/cjs/components/_internal/shell-prop-helpers.js +2 -0
- package/dist/cjs/components/_internal/shell-prop-helpers.js.map +7 -0
- package/dist/cjs/components/_internal/shell-sidebar.d.ts +4 -21
- package/dist/cjs/components/_internal/shell-sidebar.d.ts.map +1 -1
- package/dist/cjs/components/_internal/shell-sidebar.js +1 -1
- package/dist/cjs/components/_internal/shell-sidebar.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 +11 -2
- package/dist/cjs/components/chatbar.d.ts.map +1 -1
- package/dist/cjs/components/chatbar.js +1 -1
- package/dist/cjs/components/chatbar.js.map +3 -3
- 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/schemas/shell.schema.d.ts +70 -70
- package/dist/cjs/components/shell.context.d.ts +1 -0
- package/dist/cjs/components/shell.context.d.ts.map +1 -1
- package/dist/cjs/components/shell.context.js.map +2 -2
- package/dist/cjs/components/shell.d.ts +6 -26
- package/dist/cjs/components/shell.d.ts.map +1 -1
- package/dist/cjs/components/shell.hooks.d.ts +19 -2
- package/dist/cjs/components/shell.hooks.d.ts.map +1 -1
- package/dist/cjs/components/shell.hooks.js +1 -1
- package/dist/cjs/components/shell.hooks.js.map +3 -3
- package/dist/cjs/components/shell.js +1 -1
- package/dist/cjs/components/shell.js.map +3 -3
- package/dist/cjs/components/shell.types.d.ts +21 -0
- package/dist/cjs/components/shell.types.d.ts.map +1 -1
- package/dist/cjs/components/shell.types.js +1 -1
- package/dist/cjs/components/shell.types.js.map +2 -2
- 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/_internal/shell-bottom.d.ts +2 -21
- package/dist/esm/components/_internal/shell-bottom.d.ts.map +1 -1
- package/dist/esm/components/_internal/shell-bottom.js +1 -1
- package/dist/esm/components/_internal/shell-bottom.js.map +3 -3
- package/dist/esm/components/_internal/shell-inspector.d.ts +10 -21
- package/dist/esm/components/_internal/shell-inspector.d.ts.map +1 -1
- package/dist/esm/components/_internal/shell-inspector.js +1 -1
- package/dist/esm/components/_internal/shell-inspector.js.map +3 -3
- package/dist/esm/components/_internal/shell-prop-helpers.d.ts +7 -0
- package/dist/esm/components/_internal/shell-prop-helpers.d.ts.map +1 -0
- package/dist/esm/components/_internal/shell-prop-helpers.js +2 -0
- package/dist/esm/components/_internal/shell-prop-helpers.js.map +7 -0
- package/dist/esm/components/_internal/shell-sidebar.d.ts +4 -21
- package/dist/esm/components/_internal/shell-sidebar.d.ts.map +1 -1
- package/dist/esm/components/_internal/shell-sidebar.js +1 -1
- package/dist/esm/components/_internal/shell-sidebar.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 +11 -2
- package/dist/esm/components/chatbar.d.ts.map +1 -1
- package/dist/esm/components/chatbar.js +1 -1
- package/dist/esm/components/chatbar.js.map +3 -3
- 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/schemas/shell.schema.d.ts +70 -70
- package/dist/esm/components/shell.context.d.ts +1 -0
- package/dist/esm/components/shell.context.d.ts.map +1 -1
- package/dist/esm/components/shell.context.js.map +2 -2
- package/dist/esm/components/shell.d.ts +6 -26
- package/dist/esm/components/shell.d.ts.map +1 -1
- package/dist/esm/components/shell.hooks.d.ts +19 -2
- package/dist/esm/components/shell.hooks.d.ts.map +1 -1
- package/dist/esm/components/shell.hooks.js +1 -1
- package/dist/esm/components/shell.hooks.js.map +3 -3
- package/dist/esm/components/shell.js +1 -1
- package/dist/esm/components/shell.js.map +3 -3
- package/dist/esm/components/shell.types.d.ts +21 -0
- package/dist/esm/components/shell.types.d.ts.map +1 -1
- package/dist/esm/components/shell.types.js.map +2 -2
- 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/_internal/shell-bottom.tsx +305 -321
- package/src/components/_internal/shell-inspector.tsx +310 -320
- package/src/components/_internal/shell-prop-helpers.ts +53 -0
- package/src/components/_internal/shell-sidebar.tsx +370 -384
- package/src/components/button.tsx +13 -42
- package/src/components/chatbar.tsx +7 -3
- package/src/components/icon-button.tsx +20 -44
- package/src/components/image.css +10 -8
- package/src/components/shell.context.tsx +1 -0
- package/src/components/shell.hooks.ts +67 -2
- package/src/components/shell.tsx +199 -209
- package/src/components/shell.types.ts +23 -0
- 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
|
@@ -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
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Reference counter to track how many components are using the live region.
|
|
5
|
+
* Only remove the live region when no components are using it.
|
|
6
|
+
*/
|
|
7
|
+
let liveRegionRefCount = 0;
|
|
8
|
+
|
|
3
9
|
/**
|
|
4
10
|
* Hook for making live announcements to screen readers
|
|
5
11
|
* Creates a hidden aria-live region for announcing dynamic content changes
|
|
6
12
|
*/
|
|
7
13
|
export function useLiveAnnouncer() {
|
|
14
|
+
// Track the timeout so we can clean it up on unmount
|
|
15
|
+
const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
8
16
|
|
|
9
17
|
// Create aria-live region if it doesn't exist
|
|
10
18
|
React.useEffect(() => {
|
|
11
19
|
let liveRegion = document.getElementById('rt-live-announcer');
|
|
12
|
-
|
|
20
|
+
|
|
13
21
|
if (!liveRegion) {
|
|
14
22
|
liveRegion = document.createElement('div');
|
|
15
23
|
liveRegion.id = 'rt-live-announcer';
|
|
@@ -27,10 +35,24 @@ export function useLiveAnnouncer() {
|
|
|
27
35
|
document.body.appendChild(liveRegion);
|
|
28
36
|
}
|
|
29
37
|
|
|
38
|
+
// Increment ref count when component mounts
|
|
39
|
+
liveRegionRefCount++;
|
|
40
|
+
|
|
30
41
|
return () => {
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
// Decrement ref count when component unmounts
|
|
43
|
+
liveRegionRefCount--;
|
|
44
|
+
|
|
45
|
+
// Clean up timeout on unmount
|
|
46
|
+
if (timeoutRef.current) {
|
|
47
|
+
clearTimeout(timeoutRef.current);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Only remove the live region if no components are using it
|
|
51
|
+
if (liveRegionRefCount === 0) {
|
|
52
|
+
const region = document.getElementById('rt-live-announcer');
|
|
53
|
+
if (region) {
|
|
54
|
+
region.remove();
|
|
55
|
+
}
|
|
34
56
|
}
|
|
35
57
|
};
|
|
36
58
|
}, []);
|
|
@@ -38,15 +60,20 @@ export function useLiveAnnouncer() {
|
|
|
38
60
|
const announce = React.useCallback((message: string) => {
|
|
39
61
|
const liveRegion = document.getElementById('rt-live-announcer');
|
|
40
62
|
if (liveRegion) {
|
|
63
|
+
// Clear any pending timeout
|
|
64
|
+
if (timeoutRef.current) {
|
|
65
|
+
clearTimeout(timeoutRef.current);
|
|
66
|
+
}
|
|
67
|
+
|
|
41
68
|
// Clear previous announcements
|
|
42
69
|
liveRegion.textContent = '';
|
|
43
|
-
|
|
70
|
+
|
|
44
71
|
// Add new announcement with a small delay to ensure it's announced
|
|
45
|
-
setTimeout(() => {
|
|
72
|
+
timeoutRef.current = setTimeout(() => {
|
|
46
73
|
liveRegion.textContent = message;
|
|
47
74
|
}, 100);
|
|
48
75
|
}
|
|
49
76
|
}, []);
|
|
50
77
|
|
|
51
78
|
return announce;
|
|
52
|
-
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useLiveAnnouncer } from './use-live-announcer.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Options for the useToggleState hook
|
|
6
|
+
*/
|
|
7
|
+
interface UseToggleStateOptions {
|
|
8
|
+
/** Controlled pressed state */
|
|
9
|
+
pressed?: boolean;
|
|
10
|
+
/** Callback when pressed state changes */
|
|
11
|
+
onPressedChange?: (pressed: boolean) => void;
|
|
12
|
+
/** Function to get the accessible name for announcements */
|
|
13
|
+
getAccessibleName: () => string;
|
|
14
|
+
/** Component name for warning messages */
|
|
15
|
+
componentName: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Hook for shared toggle button state management.
|
|
20
|
+
* Provides accessibility announcements and controlled/uncontrolled warnings.
|
|
21
|
+
*
|
|
22
|
+
* @param options - Configuration options for the toggle state
|
|
23
|
+
* @returns Object containing the handlePressedChange callback
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```tsx
|
|
27
|
+
* const getAccessibleName = React.useCallback(() => 'Toggle button', []);
|
|
28
|
+
*
|
|
29
|
+
* const { handlePressedChange } = useToggleState({
|
|
30
|
+
* pressed,
|
|
31
|
+
* onPressedChange,
|
|
32
|
+
* getAccessibleName,
|
|
33
|
+
* componentName: 'ToggleButton',
|
|
34
|
+
* });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function useToggleState({ pressed, onPressedChange, getAccessibleName, componentName }: UseToggleStateOptions) {
|
|
38
|
+
const announce = useLiveAnnouncer();
|
|
39
|
+
|
|
40
|
+
// Track if we've already warned about controlled without handler
|
|
41
|
+
const warnedRef = React.useRef(false);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Memoized handler for state changes with accessibility announcements.
|
|
45
|
+
* Announces the new state immediately for screen readers.
|
|
46
|
+
*/
|
|
47
|
+
const handlePressedChange = React.useCallback(
|
|
48
|
+
(newPressed: boolean) => {
|
|
49
|
+
const accessibleName = getAccessibleName();
|
|
50
|
+
// Announce the state change for screen readers
|
|
51
|
+
announce(`${accessibleName} ${newPressed ? 'pressed' : 'unpressed'}`);
|
|
52
|
+
// Call the user's change handler
|
|
53
|
+
onPressedChange?.(newPressed);
|
|
54
|
+
},
|
|
55
|
+
[announce, onPressedChange, getAccessibleName],
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Development-only warning for controlled/uncontrolled pattern
|
|
59
|
+
// Only warns once to avoid console spam
|
|
60
|
+
React.useEffect(() => {
|
|
61
|
+
if (process.env.NODE_ENV === 'development' && pressed !== undefined && onPressedChange === undefined && !warnedRef.current) {
|
|
62
|
+
warnedRef.current = true;
|
|
63
|
+
console.warn(
|
|
64
|
+
`${componentName}: You provided a \`pressed\` prop without an \`onPressedChange\` handler. ` +
|
|
65
|
+
'This will result in a read-only toggle button. If you want the button to be interactive, ' +
|
|
66
|
+
'you should provide an `onPressedChange` handler.',
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}, [pressed, onPressedChange, componentName]);
|
|
70
|
+
|
|
71
|
+
return { handlePressedChange };
|
|
72
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hook for managing tooltip accessibility props and conditional rendering.
|
|
5
|
+
* Encapsulates tooltip ID generation and aria-describedby binding.
|
|
6
|
+
*
|
|
7
|
+
* @param tooltip - The tooltip content (if any)
|
|
8
|
+
* @returns Object containing tooltipId, hasTooltip flag, and accessibility props
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* const { tooltipId, hasTooltip, accessibilityProps } = useTooltipWrapper(tooltip);
|
|
13
|
+
*
|
|
14
|
+
* const button = <button {...accessibilityProps}>Click me</button>;
|
|
15
|
+
*
|
|
16
|
+
* if (!hasTooltip) return button;
|
|
17
|
+
*
|
|
18
|
+
* return <Tooltip id={tooltipId} content={tooltip}>{button}</Tooltip>;
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function useTooltipWrapper(tooltip: React.ReactNode) {
|
|
22
|
+
const tooltipId = React.useId();
|
|
23
|
+
const hasTooltip = Boolean(tooltip);
|
|
24
|
+
|
|
25
|
+
const accessibilityProps = React.useMemo(() => (hasTooltip ? { 'aria-describedby': tooltipId } : {}), [hasTooltip, tooltipId]);
|
|
26
|
+
|
|
27
|
+
return { tooltipId, hasTooltip, accessibilityProps };
|
|
28
|
+
}
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
--color-surface-translucent: rgba(255, 255, 255, var(--surface-opacity));
|
|
17
17
|
--color-dialog-solid: white;
|
|
18
18
|
--color-dialog-translucent: rgba(255, 255, 255, var(--dialog-opacity));
|
|
19
|
+
|
|
20
|
+
/* Material system defaults (solid) - controls color-mix blending for component backgrounds */
|
|
21
|
+
--material-blend: 100%;
|
|
22
|
+
--material-alpha: 0;
|
|
19
23
|
}
|
|
20
24
|
:is(.dark, .dark-theme),
|
|
21
25
|
:is(.dark, .dark-theme) :where(.radix-themes:not(.light, .light-theme)) {
|
|
@@ -130,13 +134,16 @@
|
|
|
130
134
|
/* * * * * * * * * * * * * * * * * * * */
|
|
131
135
|
|
|
132
136
|
.radix-themes {
|
|
133
|
-
&:where([data-panel-background='solid']) {
|
|
137
|
+
&:where([data-panel-background='solid'], [data-material='solid']) {
|
|
134
138
|
--color-panel: var(--color-panel-solid);
|
|
135
139
|
--color-surface: var(--color-surface-solid);
|
|
136
140
|
--color-dialog: var(--color-dialog-solid);
|
|
137
141
|
--backdrop-filter-panel: none;
|
|
138
142
|
--backdrop-filter-components: none;
|
|
139
143
|
--backdrop-filter-dialog: none;
|
|
144
|
+
/* Material system for component backgrounds */
|
|
145
|
+
--material-blend: 100%;
|
|
146
|
+
--material-alpha: 0;
|
|
140
147
|
}
|
|
141
148
|
&:where([data-panel-background='translucent'], [data-material='translucent']) {
|
|
142
149
|
--color-panel: var(--color-panel-translucent);
|
|
@@ -145,6 +152,9 @@
|
|
|
145
152
|
--backdrop-filter-panel: blur(var(--backdrop-blur-panel));
|
|
146
153
|
--backdrop-filter-components: blur(var(--backdrop-blur-components));
|
|
147
154
|
--backdrop-filter-dialog: blur(var(--backdrop-blur-dialog));
|
|
155
|
+
/* Material system for component backgrounds */
|
|
156
|
+
--material-blend: 60%;
|
|
157
|
+
--material-alpha: 1;
|
|
148
158
|
}
|
|
149
159
|
}
|
|
150
160
|
|