Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "fix: move inline styling and context resolution out of the base hook",
"packageName": "@fluentui/react-link",
"email": "vgenaev@gmail.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ export const Default = (): React.ReactNode => (

<p className={styles.paragraph}>
By continuing you agree to our{' '}
<Link href="#" inline className={`${styles.link} ${styles.inline}`}>
<Link href="#" className={`${styles.link} ${styles.inline}`}>
Terms of Service
</Link>{' '}
and{' '}
<Link href="#" inline className={`${styles.link} ${styles.inline}`}>
<Link href="#" className={`${styles.link} ${styles.inline}`}>
Privacy Policy
</Link>
.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
A link navigates the user to another page, section, or resource.

Use Link for navigation actions and Button for in-page operations. Links support an `inline` prop that renders them within a text flow, and a `disabled` prop that prevents navigation while preserving accessibility.
Use Link for navigation actions and Button for in-page operations. Links support a `disabled` prop that prevents navigation while preserving accessibility.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const Default = (): React.ReactNode => (
<MessageBarBody className={styles.body}>
<MessageBarTitle className={styles.title}>Descriptive title</MessageBarTitle>
Message providing information to the user with actionable insights.{' '}
<Link className={`${linkStyles.link} ${linkStyles.inline}`} href="#" inline>
<Link className={`${linkStyles.link} ${linkStyles.inline}`} href="#">
Learn more
</Link>
</MessageBarBody>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const Intent = (): React.ReactNode => (
<MessageBarBody className={styles.body}>
<MessageBarTitle className={styles.title}>{item.title}</MessageBarTitle>
Message providing information to the user with actionable insights.{' '}
<Link className={`${linkStyles.link} ${linkStyles.inline}`} href="#" inline>
<Link className={`${linkStyles.link} ${linkStyles.inline}`} href="#">
Learn more
</Link>
</MessageBarBody>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ import type { SlotClassNames } from '@fluentui/react-utilities';
export const Link: ForwardRefComponent<LinkProps>;

// @public
export type LinkBaseProps = DistributiveOmit<LinkProps, 'appearance'>;
Comment thread
mainframev marked this conversation as resolved.
export type LinkBaseProps = DistributiveOmit<LinkProps, 'appearance' | 'inline'>;

// @public
export type LinkBaseState = DistributiveOmit<LinkState, 'appearance' | 'backgroundAppearance'>;
export type LinkBaseState = DistributiveOmit<LinkState, 'appearance' | 'backgroundAppearance' | 'inline'>;

// @public (undocumented)
export const linkClassNames: SlotClassNames<LinkSlots>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,17 @@ export type LinkProps = ComponentProps<LinkSlots> & {
};

/**
* Link props without design-specific props (appearance).
* Link props without design-specific props (appearance, inline).
* Use this when building a base link that is unstyled or uses a custom design system.
*/
export type LinkBaseProps = DistributiveOmit<LinkProps, 'appearance'>;
export type LinkBaseProps = DistributiveOmit<LinkProps, 'appearance' | 'inline'>;

export type LinkState = ComponentState<LinkSlots> &
Required<Pick<LinkProps, 'appearance' | 'disabled' | 'disabledFocusable' | 'inline'>> & {
backgroundAppearance?: BackgroundAppearanceContextValue;
};

/**
* Link state without design-specific state (appearance, backgroundAppearance).
* Link state without design-specific state (appearance, backgroundAppearance, inline).
*/
export type LinkBaseState = DistributiveOmit<LinkState, 'appearance' | 'backgroundAppearance'>;
export type LinkBaseState = DistributiveOmit<LinkState, 'appearance' | 'backgroundAppearance' | 'inline'>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import * as React from 'react';
import { renderHook } from '@testing-library/react-hooks';

import { LinkContextProvider } from '../../contexts/linkContext';
import type { LinkBaseState } from './Link.types';
import { useLink_unstable, useLinkBase_unstable } from './useLink';

const useLinkBaseTypeTests = () => {
// @ts-expect-error - inline is a styled Link prop, not a LinkBase prop
useLinkBase_unstable({ inline: true }, null);

const state: LinkBaseState = useLinkBase_unstable({}, null);
// @ts-expect-error - inline is not exposed by LinkBaseState
state.inline;
};

useLinkBaseTypeTests;

describe('useLink_unstable', () => {
it('defaults inline to false', () => {
const { result } = renderHook(() => useLink_unstable({}, null));

expect(result.current.inline).toBe(false);
});

it('sets inline from props', () => {
const { result } = renderHook(() => useLink_unstable({ inline: true }, null));

expect(result.current.inline).toBe(true);
});

it('sets inline from context when the prop is absent', () => {
const wrapper = ({ children }: { children?: React.ReactNode }) => (
<LinkContextProvider value={{ inline: true }}>{children}</LinkContextProvider>
);
const { result } = renderHook(() => useLink_unstable({}, null), { wrapper });

expect(result.current.inline).toBe(true);
});

it('prefers an explicit inline prop over context', () => {
const wrapper = ({ children }: { children?: React.ReactNode }) => (
<LinkContextProvider value={{ inline: true }}>{children}</LinkContextProvider>
);
const { result } = renderHook(() => useLink_unstable({ inline: false }, null), { wrapper });

expect(result.current.inline).toBe(false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,22 @@ export const useLink_unstable = (
ref: React.Ref<HTMLAnchorElement | HTMLButtonElement | HTMLSpanElement>,
): LinkState => {
const backgroundAppearance = useBackgroundAppearance();
const { appearance = 'default', ...baseProps } = props;
const { inline: inlineContext } = useLinkContext();
const { appearance = 'default', inline: inlineProp, ...baseProps } = props;

const state = useLinkBase_unstable(baseProps, ref);

return {
appearance,
backgroundAppearance,
inline: inlineProp ?? !!inlineContext,
...state,
};
};

/**
* Base hook for Link component, which manages state related to ARIA, keyboard handling,
* disabled behavior, and slot structure. This hook excludes design-specific props (appearance).
* disabled behavior, and slot structure. This hook excludes design-specific props (appearance, inline).
*
* @param props - User provided props to the Link component.
* @param ref - User provided ref to be passed to the Link component.
Expand All @@ -39,8 +41,7 @@ export const useLinkBase_unstable = (
props: LinkBaseProps,
ref: React.Ref<HTMLAnchorElement | HTMLButtonElement | HTMLSpanElement>,
): LinkBaseState => {
const { inline: inlineContext } = useLinkContext();
const { disabled = false, disabledFocusable = false, inline = false } = props;
const { disabled = false, disabledFocusable = false } = props;

const elementType = props.as || (props.href ? 'a' : 'button');

Expand All @@ -56,7 +57,6 @@ export const useLinkBase_unstable = (
// Props passed at the top-level
disabled,
disabledFocusable,
inline: inline ?? !!inlineContext,

// Slots definition
components: {
Expand Down
Loading