Skip to content

Commit

Permalink
feat: Adding span as an option for Link to render as (#29937)
Browse files Browse the repository at this point in the history
* feat: Adding span as an option for Link to render as.

* Adding tabIndex to Link as span.

* Adding change file.

* Addressing PR feedback.
  • Loading branch information
khmakoto committed Nov 29, 2023
1 parent 6438f49 commit 3d31f25
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 13 deletions.
99 changes: 99 additions & 0 deletions apps/vr-tests-react-components/src/stories/Link.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { tokens } from '@fluentui/react-theme';

const AnchorLink = (props: LinkProps & { as?: 'a' }) => <Link {...props} href="https://www.bing.com" />;
const ButtonLink = (props: LinkProps) => <Link {...props} />;
const SpanLink = (props: LinkProps & { as?: 'span' }) => <Link as="span" {...props} />;

const useInvertedBackgroundStyles = makeStyles({
root: {
Expand Down Expand Up @@ -216,3 +217,101 @@ storiesOf('Link Converged - Rendered as button', module)
used alongside other text content.
</div>
));

storiesOf('Link Converged - Rendered as span', module)
.addDecorator(story => (
<StoryWright
steps={new Steps()
.snapshot('default', { cropTo: '.testWrapper' })
.hover('.fui-Link')
.snapshot('hover', { cropTo: '.testWrapper' })
// This needs to be added so that the focus outline is shown correctly
.executeScript("document.getElementsByClassName('fui-Link')[0].setAttribute('data-fui-focus-visible', '')")
.focus('.fui-Link')
.snapshot('focused', { cropTo: '.testWrapper' })
.executeScript("document.getElementsByClassName('fui-Link')[0].removeAttribute('data-fui-focus-visible')")
.mouseDown('.fui-Link')
.snapshot('pressed', { cropTo: '.testWrapper' })
.mouseUp('.fui-Link')
.end()}
>
{story()}
</StoryWright>
))
.addStory('Stand-alone', () => <SpanLink>Stand-alone link</SpanLink>, { includeRtl: true })
.addStory('Stand-alone Disabled Focusable', () => (
<SpanLink disabled disabledFocusable>
Stand-alone disabled focusable link
</SpanLink>
))
.addStory(
'Inline',
() => (
<div>
This is <SpanLink inline>a link</SpanLink> used alongside other text content.
</div>
),
{ includeRtl: true },
)
.addStory('Inline Disabled Focusable', () => (
<div>
This is{' '}
<SpanLink inline disabled disabledFocusable>
a disabled focusable link
</SpanLink>{' '}
used alongside other text content.
</div>
))
.addStory(
'Inverted',
() => (
<InvertedBackground>
<SpanLink>Link on inverted background</SpanLink>
</InvertedBackground>
),
{ includeDarkMode: true, includeHighContrast: true },
)
.addStory(
'Inverted disabled',
() => (
<InvertedBackground>
<SpanLink disabled disabledFocusable>
Disabled link on inverted background
</SpanLink>
</InvertedBackground>
),
{ includeDarkMode: true, includeHighContrast: true },
)
.addStory('Wraps correctly as an inline element', () => (
<div style={{ width: '100px' }}>
This <SpanLink inline>link wraps correctly between different lines, behaving as an inline element</SpanLink> as
expected.
</div>
));

// We put the disabled stories separately so they do not error on the focused step.
storiesOf('Link Converged - Rendered as button', module)
.addDecorator(story => (
<StoryWright
steps={new Steps()
.snapshot('default', { cropTo: '.testWrapper' })
.hover('.fui-Link')
.snapshot('hover', { cropTo: '.testWrapper' })
.mouseDown('.fui-Link')
.snapshot('pressed', { cropTo: '.testWrapper' })
.mouseUp('.fui-Link')
.end()}
>
{story()}
</StoryWright>
))
.addStory('Stand-alone Disabled', () => <SpanLink disabled>Stand-alone disabled link</SpanLink>)
.addStory('Inline Disabled', () => (
<div>
This is{' '}
<SpanLink inline disabled>
a disabled link
</SpanLink>{' '}
used alongside other text content.
</div>
));
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: Adding span as an option for Link to render as.",
"packageName": "@fluentui/react-link",
"email": "humbertomakotomorimoto@gmail.com",
"dependentChangeType": "patch"
}
4 changes: 2 additions & 2 deletions packages/react-components/react-link/etc/react-link.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export type LinkProps = ComponentProps<LinkSlots> & {

// @public (undocumented)
export type LinkSlots = {
root: Slot<'a', 'button'>;
root: Slot<'a', 'button' | 'span'>;
};

// @public (undocumented)
Expand All @@ -40,7 +40,7 @@ export type LinkState = ComponentState<LinkSlots> & Required<Pick<LinkProps, 'ap
export const renderLink_unstable: (state: LinkState) => JSX.Element;

// @public
export const useLink_unstable: (props: LinkProps, ref: React_2.Ref<HTMLAnchorElement | HTMLButtonElement>) => LinkState;
export const useLink_unstable: (props: LinkProps, ref: React_2.Ref<HTMLAnchorElement | HTMLButtonElement | HTMLSpanElement>) => LinkState;

// @public
export const useLinkState_unstable: (state: LinkState) => LinkState;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export type LinkSlots = {
/**
* Root of the component that renders as either an <a> or a <button> tag.
*/
root: Slot<'a', 'button'>;
root: Slot<'a', 'button' | 'span'>;
};

export type LinkProps = ComponentProps<LinkSlots> & {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,20 @@ import type { LinkProps, LinkState } from './Link.types';
*/
export const useLink_unstable = (
props: LinkProps,
ref: React.Ref<HTMLAnchorElement | HTMLButtonElement>,
ref: React.Ref<HTMLAnchorElement | HTMLButtonElement | HTMLSpanElement>,
): LinkState => {
const backgroundAppearance = useBackgroundAppearance();
const { appearance = 'default', disabled = false, disabledFocusable = false, inline = false } = props;

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

// Casting is required here as `as` prop would break the union between `a` and `button` types
const propsWithAssignedAs = { ...props, as: elementType } as LinkProps;
// Casting is required here as `as` prop would break the union between `a`, `button` and `span` types
const propsWithAssignedAs = {
...props,
as: elementType,
role: elementType === 'span' ? 'button' : undefined,
type: elementType === 'button' ? 'button' : undefined,
} as LinkProps;

const state: LinkState = {
// Props passed at the top-level
Expand All @@ -36,7 +41,6 @@ export const useLink_unstable = (
root: slot.always(
getIntrinsicElementProps<LinkProps>(elementType, {
ref,
type: elementType === 'button' ? 'button' : undefined,
...propsWithAssignedAs,
} as const),
{ elementType },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,21 @@ import type { LinkState } from './Link.types';
*/
export const useLinkState_unstable = (state: LinkState): LinkState => {
const { disabled, disabledFocusable } = state;
const { onClick, onKeyDown, role, tabIndex, type } = state.root;
const { onClick, onKeyDown, role, tabIndex } = state.root;

// Add href and tabIndex=0 for anchor elements.
// Add href for anchor elements.
if (state.root.as === 'a') {
state.root.href = disabled ? undefined : state.root.href;
state.root.tabIndex = tabIndex ?? (disabled && !disabledFocusable ? undefined : 0);

// Add role="link" for disabled and disabledFocusable links.
if (disabled || disabledFocusable) {
state.root.role = role || 'link';
}
}
// Add type="button" for button elements.
else {
state.root.type = type || 'button';

// Add tabIndex=0 for anchor and span elements.
if (state.root.as === 'a' || state.root.as === 'span') {
state.root.tabIndex = tabIndex ?? (disabled && !disabledFocusable ? undefined : 0);
}

// Disallow click event when component is disabled and eat events when disabledFocusable is set to true.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as React from 'react';
import { Link, makeResetStyles } from '@fluentui/react-components';

const useDivWithWidthClassName = makeResetStyles({
width: '200px',
});

export const AsSpan = () => (
<div className={useDivWithWidthClassName()}>
The following link renders as a span.{' '}
<Link as="span">Links that render as a span wrap correctly between lines when their content is very long</Link>.{' '}
This is because they behave as regular inline elements.
</div>
);

AsSpan.parameters = {
docs: {
description: {
story: [
'A Link can be rendered as an html `<span>`, in which case it will have `role="button"` set.',
'Links that render as a span wrap correctly between lines, behaving as inline elements as opposed to links rendered as buttons, which always behave as inline-block elements that do not wrap correctly.',
].join('\n'),
},
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export { Inline } from './LinkInline.stories';
export { Disabled } from './LinkDisabled.stories';
export { DisabledFocusable } from './LinkDisabledFocusable.stories';
export { AsButton } from './LinkAsButton.stories';
export { AsSpan } from './LinkAsSpan.stories';

export default {
title: 'Components/Link',
Expand Down

0 comments on commit 3d31f25

Please sign in to comment.