Skip to content

Commit

Permalink
[Joy] Add Link component (#31175)
Browse files Browse the repository at this point in the history
  • Loading branch information
hbjORbj committed Mar 3, 2022
1 parent 1db2ae2 commit 51ebd13
Show file tree
Hide file tree
Showing 9 changed files with 736 additions and 6 deletions.
103 changes: 103 additions & 0 deletions docs/pages/experiments/joy/link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import Moon from '@mui/icons-material/DarkMode';
import Sun from '@mui/icons-material/LightMode';
import Box from '@mui/joy/Box';
import Button from '@mui/joy/Button';
import Link from '@mui/joy/Link';
import { CssVarsProvider, useColorScheme } from '@mui/joy/styles';
import Typography from '@mui/joy/Typography';
import * as React from 'react';

const ColorSchemePicker = () => {
const { mode, setMode } = useColorScheme();
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return null;
}

return (
<Button
variant="outlined"
onClick={() => {
if (mode === 'light') {
setMode('dark');
} else {
setMode('light');
}
}}
sx={{ '--Button-gutter': '0.25rem', minWidth: 'var(--Button-minHeight)' }}
>
{mode === 'light' ? <Moon /> : <Sun />}
</Button>
);
};

export default function JoyButton() {
const buttonProps = {
variant: ['text', 'outlined', 'light', 'contained'],
color: ['primary', 'neutral', 'danger', 'info', 'success', 'warning'],
level: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'body1', 'body2', 'body3'],
underline: ['hover', 'always', 'none'],
} as const;
return (
<CssVarsProvider
theme={{
components: {
MuiSvgIcon: {
defaultProps: {
fontSize: 'xl',
},
styleOverrides: {
root: ({ ownerState, theme }) => ({
...(ownerState.fontSize &&
ownerState.fontSize !== 'inherit' && {
fontSize: theme.vars.fontSize[ownerState.fontSize],
}),
...(ownerState.color &&
ownerState.color !== 'inherit' && {
color: theme.vars.palette[ownerState.color].textColor,
}),
}),
},
},
},
}}
>
<Box sx={{ py: 5, maxWidth: { md: 1152, xl: 1536 }, mx: 'auto' }}>
<Box sx={{ px: 3, pb: 4 }}>
<ColorSchemePicker />
</Box>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 5 }}>
{Object.entries(buttonProps).map(([propName, propValue]) => (
<Box
key={propName}
sx={{ display: 'flex', flexDirection: 'column', gap: 5, p: 2, alignItems: 'center' }}
>
<Typography level="body2" sx={{ fontWeight: 'bold' }}>
{propName}
</Typography>
{propValue.map((value) => (
<Box
key={value}
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Link {...{ [propName]: value }}>Link</Link>
<Typography level="body3" sx={{ textAlign: 'center', mt: '4px' }}>
{value || 'default'}
</Typography>
</Box>
))}
</Box>
))}
</Box>
</Box>
</CssVarsProvider>
);
}
50 changes: 50 additions & 0 deletions packages/mui-joy/src/Link/Link.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import Link from '@mui/joy/Link';
import * as React from 'react';

<Link />;
<Link component="div" />;

// `variant`
<Link variant="text" />;
<Link variant="light" />;
<Link variant="outlined" />;
<Link variant="contained" />;

// `color`
<Link color="primary" />;
<Link color="danger" />;
<Link color="info" />;
<Link color="success" />;
<Link color="warning" />;
<Link color="neutral" />;

// `level`
<Link level="h2" />;
<Link level="h3" />;
<Link level="h4" />;
<Link level="h5" />;
<Link level="h6" />;
<Link level="body1" />;
<Link level="body2" />;
<Link level="body3" />;
<Link level="inherit" />;

// `underline`
<Link underline="always" />;
<Link underline="none" />;
<Link underline="hover" />;

// @ts-expect-error there is no variant `filled`
<Link variant="filled" />;

// @ts-expect-error there is no color `secondary`
<Link color="secondary" />;

// @ts-expect-error there is no level `h7`
<Link level="h7" />;

// @ts-expect-error there is no level `body4`
<Link level="body4" />;

// @ts-expect-error there is no underline `never`
<Link underline="never" />;
186 changes: 186 additions & 0 deletions packages/mui-joy/src/Link/Link.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import * as React from 'react';
import { expect } from 'chai';
import { spy } from 'sinon';
import { act, createRenderer, fireEvent, describeConformance } from 'test/utils';
import Typography from '@mui/joy/Typography';
import Link, { linkClasses as classes } from '@mui/joy/Link';
import { ThemeProvider } from '@mui/joy/styles';
import { unstable_capitalize as capitalize } from '@mui/utils';

function focusVisible(element) {
act(() => {
element.blur();
document.dispatchEvent(new window.Event('keydown'));
element.focus();
});
}

describe('<Link />', () => {
const { render } = createRenderer();

describeConformance(<Link href="/">Home</Link>, () => ({
classes,
inheritComponent: Typography,
render,
ThemeProvider,
muiName: 'MuiLink',
refInstanceof: window.HTMLAnchorElement,
testVariantProps: { color: 'primary', variant: 'text' },
testStateOverrides: { prop: 'underline', value: 'always', styleKey: 'underlineAlways' },
skip: [
'classesRoot',
'componentsProp',
'themeDefaultProps',
'propsSpread',
'themeStyleOverrides',
],
}));

it('should render children', () => {
const { queryByText } = render(<Link href="/">Home</Link>);

expect(queryByText('Home')).not.to.equal(null);
});

describe('event callbacks', () => {
it('should fire event callbacks', () => {
const events = ['onBlur', 'onFocus'];

const handlers = events.reduce((result, n) => {
result[n] = spy();
return result;
}, {});

const { container } = render(
<Link href="/" {...handlers}>
Home
</Link>,
);
const anchor = container.querySelector('a');

events.forEach((n) => {
const event = n.charAt(2).toLowerCase() + n.slice(3);
fireEvent[event](anchor);
expect(handlers[n].callCount).to.equal(1);
});
});
});

describe('keyboard focus', () => {
it('should add the focusVisible class when focused', () => {
const { container } = render(<Link href="/">Home</Link>);
const anchor = container.querySelector('a');

expect(anchor).not.to.have.class(classes.focusVisible);

focusVisible(anchor);

expect(anchor).to.have.class(classes.focusVisible);

act(() => {
anchor.blur();
});

expect(anchor).not.to.have.class(classes.focusVisible);
});
});

describe('prop: variant', () => {
it('undefined by default', () => {
const { getByTestId } = render(
<Link href="/" data-testid="root">
Hello World
</Link>,
);

expect(getByTestId('root')).not.to.have.class(classes.variantText);
expect(getByTestId('root')).not.to.have.class(classes.variantOutlined);
expect(getByTestId('root')).not.to.have.class(classes.variantLight);
expect(getByTestId('root')).not.to.have.class(classes.variantContained);
});

['text', 'outlined', 'light', 'contained'].forEach((variant) => {
it(`should render ${variant}`, () => {
const { getByTestId } = render(
<Link href="/" data-testid="root" variant={variant}>
Hello World
</Link>,
);

expect(getByTestId('root')).to.have.class(classes[`variant${capitalize(variant)}`]);
});
});
});

describe('prop: color', () => {
it('adds a primary class by default', () => {
const { getByTestId } = render(
<Link href="/" data-testid="root">
Hello World
</Link>,
);

expect(getByTestId('root')).to.have.class(classes.colorPrimary);
});

['primary', 'success', 'info', 'danger', 'neutral', 'warning'].forEach((color) => {
it(`should render ${color}`, () => {
const { getByTestId } = render(
<Link href="/" data-testid="root" color={color}>
Hello World
</Link>,
);

expect(getByTestId('root')).to.have.class(classes[`color${capitalize(color)}`]);
});
});
});

describe('prop: level', () => {
it('body1 by default', () => {
const { getByTestId } = render(
<Link href="/" data-testid="root">
Hello World
</Link>,
);

expect(getByTestId('root')).have.class(classes.body1);
});

['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'body1', 'body2', 'body3'].forEach((level) => {
it(`should render ${level}`, () => {
const { getByTestId } = render(
<Link href="/" data-testid="root" level={level}>
Hello World
</Link>,
);

expect(getByTestId('root')).to.have.class(classes[level]);
});
});
});

describe('prop: underline', () => {
it('hover by default', () => {
const { getByTestId } = render(
<Link href="/" data-testid="root">
Hello World
</Link>,
);

expect(getByTestId('root')).have.class(classes.underlineHover);
});

['none', 'always', 'hover'].forEach((underline) => {
it(`should render ${underline}`, () => {
const { getByTestId } = render(
<Link href="/" data-testid="root" underline={underline}>
Hello World
</Link>,
);

expect(getByTestId('root')).to.have.class(classes[`underline${capitalize(underline)}`]);
});
});
});
});

0 comments on commit 51ebd13

Please sign in to comment.