-
Notifications
You must be signed in to change notification settings - Fork 13
fix(Breadcrumb)!: BREAKING CHANGE use composition and remove cloneElement #695
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3c406e7
1fc3160
db4db91
4481c5e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,9 @@ | ||
| 'use client'; | ||
|
|
||
| import { mergeProps, useRender } from '@base-ui/react'; | ||
| import { ChevronDownIcon } from '@radix-ui/react-icons'; | ||
| import { cx } from 'class-variance-authority'; | ||
| import React, { | ||
| ComponentProps, | ||
| cloneElement, | ||
| ReactElement, | ||
| ReactNode | ||
| } from 'react'; | ||
| import React, { ReactNode } from 'react'; | ||
| import { Menu } from '../menu'; | ||
| import styles from './breadcrumb.module.css'; | ||
|
|
||
|
|
@@ -17,16 +13,15 @@ export interface BreadcrumbDropdownItem { | |
| onClick?: React.MouseEventHandler<HTMLElement>; | ||
| } | ||
|
|
||
| export interface BreadcrumbItemProps extends ComponentProps<'a'> { | ||
| export interface BreadcrumbItemProps extends useRender.ComponentProps<'a'> { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat -n packages/raystack/components/breadcrumb/breadcrumb-item.tsxRepository: raystack/apsara Length of output: 3836 Fix the ref type contract across all render paths in BreadcrumbItem.
Either split the component variants to have distinct ref types for each case, or remove ref forwarding from the non-anchor branches. 🤖 Prompt for AI Agents |
||
| leadingIcon?: ReactNode; | ||
| current?: boolean; | ||
| dropdownItems?: BreadcrumbDropdownItem[]; | ||
| as?: ReactElement; | ||
| } | ||
|
|
||
| export const BreadcrumbItem = ({ | ||
| ref, | ||
| as, | ||
| render, | ||
| children, | ||
| className, | ||
| leadingIcon, | ||
|
|
@@ -35,24 +30,49 @@ export const BreadcrumbItem = ({ | |
| dropdownItems, | ||
| ...props | ||
| }: BreadcrumbItemProps) => { | ||
| const renderedElement = as ?? <a ref={ref} />; | ||
| const label = ( | ||
| const label = leadingIcon ? ( | ||
| <> | ||
| {leadingIcon && ( | ||
| <span className={styles['breadcrumb-icon']}>{leadingIcon}</span> | ||
| )} | ||
| {children && <span>{children}</span>} | ||
| <span className={styles['breadcrumb-icon']}>{leadingIcon}</span> | ||
| {children != null && <span>{children}</span>} | ||
| </> | ||
| ) : ( | ||
| children | ||
| ); | ||
|
|
||
| const { | ||
| id, | ||
| title, | ||
| 'aria-label': ariaLabel, | ||
| 'aria-labelledby': ariaLabelledby, | ||
| 'aria-describedby': ariaDescribedby | ||
| } = props; | ||
|
Comment on lines
+42
to
+48
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dropdown trigger props are being dropped. This whitelist only forwards Also applies to: 68-75 🤖 Prompt for AI Agents |
||
|
|
||
| const linkElement = useRender({ | ||
| defaultTagName: 'a', | ||
| ref, | ||
| render, | ||
| props: mergeProps<'a'>( | ||
| { | ||
| className: cx(styles['breadcrumb-link']), | ||
| href, | ||
| children: label | ||
| }, | ||
| props | ||
| ) | ||
| }); | ||
|
|
||
| if (dropdownItems) { | ||
| return ( | ||
| <li className={cx(styles['breadcrumb-item'], className)}> | ||
| <Menu> | ||
| <Menu.Trigger | ||
| ref={ref as React.Ref<HTMLButtonElement>} | ||
| className={styles['breadcrumb-dropdown-trigger']} | ||
| {...(props as React.ButtonHTMLAttributes<HTMLButtonElement>)} | ||
| id={id} | ||
| title={title} | ||
| aria-label={ariaLabel} | ||
| aria-labelledby={ariaLabelledby} | ||
| aria-describedby={ariaDescribedby} | ||
| > | ||
| {label} | ||
| <ChevronDownIcon className={styles['breadcrumb-dropdown-icon']} /> | ||
|
|
@@ -72,23 +92,25 @@ export const BreadcrumbItem = ({ | |
| </li> | ||
| ); | ||
| } | ||
| return ( | ||
| <li className={cx(styles['breadcrumb-item'], className)}> | ||
| {cloneElement( | ||
| renderedElement, | ||
| { | ||
| className: cx( | ||
| if (current) { | ||
| return ( | ||
| <li className={cx(styles['breadcrumb-item'], className)}> | ||
| <span | ||
| ref={ref as React.RefObject<HTMLSpanElement>} | ||
| className={cx( | ||
| styles['breadcrumb-link'], | ||
| current && styles['breadcrumb-link-active'] | ||
| ), | ||
| href, | ||
| ...props, | ||
| ...renderedElement.props, | ||
| ref | ||
| }, | ||
| label | ||
| )} | ||
| </li> | ||
| styles['breadcrumb-link-active'] | ||
| )} | ||
| aria-current='page' | ||
| {...props} | ||
|
Comment on lines
+98
to
+105
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Because 🤖 Prompt for AI Agents |
||
| > | ||
| {label} | ||
| </span> | ||
| </li> | ||
| ); | ||
| } | ||
| return ( | ||
| <li className={cx(styles['breadcrumb-item'], className)}>{linkElement}</li> | ||
| ); | ||
| }; | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
Base UI render prop prop merging behavior💡 Result:
Base UI's render prop uses intelligent prop merging via the mergeProps utility to combine component props with user-provided render props. Props are merged left-to-right (rightmost wins for most keys), with special handling for className (concatenated right-to-left), style (deep merged, rightmost overwrites), event handlers (merged and executed right-to-left), and ref (rightmost ref kept). This ensures custom components receive Base UI's behavioral props (like event handlers, data attributes) while preserving user styles and handlers. In render prop usage: - Element form: <Component render={} /> - mergeProps automatically combines props. - Function form: render={(props) => <Custom {...props} />} or render={(props, state) => ...} - use mergeProps manually for merging, e.g., <Custom {...mergeProps(internalProps, userProps)} />. Call event.preventBaseUIHandler to prevent Base UI's internal logic. The useRender hook implements render prop logic, merging props before spreading on the rendered element.
Citations:
🌐 Web query:
Next.js Link component href required prop💡 Result:
Yes, the href prop is required for the Next.js Link component in both App Router and Pages Router.
Citations:
🏁 Script executed:
Repository: raystack/apsara
Length of output: 116
🏁 Script executed:
Repository: raystack/apsara
Length of output: 981
🏁 Script executed:
Repository: raystack/apsara
Length of output: 288
🏁 Script executed:
# Check the breadcrumb component implementation cat apps/www/src/components/playground/breadcrumb-examples.tsxRepository: raystack/apsara
Length of output: 1127
🏁 Script executed:
# Check the demo file to see if there are live examples cat apps/www/src/content/docs/components/breadcrumb/demo.tsRepository: raystack/apsara
Length of output: 4341
🏁 Script executed:
Repository: raystack/apsara
Length of output: 230
🏁 Script executed:
Repository: raystack/apsara
Length of output: 585
🏁 Script executed:
# Check the Breadcrumb.Item component implementation cat packages/raystack/components/breadcrumb/breadcrumb-item.tsxRepository: raystack/apsara
Length of output: 3017
🏁 Script executed:
# Also check the main breadcrumb file cat packages/raystack/components/breadcrumb/breadcrumb.tsxRepository: raystack/apsara
Length of output: 391
🏁 Script executed:
Repository: raystack/apsara
Length of output: 1712
🏁 Script executed:
Repository: raystack/apsara
Length of output: 2677
🏁 Script executed:
Repository: raystack/apsara
Length of output: 233
🏁 Script executed:
Repository: raystack/apsara
Length of output: 86
🏁 Script executed:
Repository: raystack/apsara
Length of output: 2773
🏁 Script executed:
# Check the tsconfig to see if there's any type relaxation cat apps/www/tsconfig.jsonRepository: raystack/apsara
Length of output: 720
🏁 Script executed:
# Check the props.ts file to see the exact type definition for render cat apps/www/src/content/docs/components/breadcrumb/props.tsRepository: raystack/apsara
Length of output: 1689
🏁 Script executed:
Repository: raystack/apsara
Length of output: 41
🏁 Script executed:
Repository: raystack/apsara
Length of output: 126
🏁 Script executed:
Repository: raystack/apsara
Length of output: 2916
Use the function form of the
renderprop for components with required props.The example
render={<NextLink />}will fail TypeScript type-checking because Next.js Link requires thehrefprop. Base UI's prop merging happens at runtime, after JSX evaluation. Use the callback form instead:render={(props) => <NextLink {...props} />}. If you want to keep the element form, explicitly providehref:render={<NextLink href="/" />}.🤖 Prompt for AI Agents