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
2 changes: 1 addition & 1 deletion docs/data/material/components/material-icons/synonyms.js
Original file line number Diff line number Diff line change
Expand Up @@ -2009,7 +2009,7 @@ const synonyms = {
Pool: 'athlete athletic beach body entertainment exercise hobby human ocean people person places sea sports swimming water',
PortableWifiOff:
'connected connection data device disabled enabled internet network offline service signal slash usage wireless',
Portrait: 'account face human people person photo picture profile user',
Portrait: 'account face frame human people person photo picture profile user',
PostAdd:
'+ data document drive file folders item page paper plus sheet slide text writing',
Power: 'charge cord electrical online outlet plug socket',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import InputLabel from '@mui/material/InputLabel';
import Select from '@mui/material/Select';
import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
import InputBase from '@mui/material/InputBase';
import AddIcon from '@mui/icons-material/Add';
import StarIcon from '@mui/icons-material/Star';
import HomeIcon from '@mui/icons-material/Home';
Expand All @@ -66,12 +67,23 @@ const theme = createTheme({

const noop = () => {};

function Row({ label, children }) {
function Row({ label, secondary, children }) {
return (
<React.Fragment>
<Typography variant="body2" sx={{ fontWeight: 600, alignSelf: 'center' }}>
{label}
</Typography>
<Box sx={{ alignSelf: 'center' }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{label}
</Typography>
{secondary ? (
<Typography
variant="caption"
color="text.secondary"
sx={{ display: 'block' }}
>
{secondary}
</Typography>
) : null}
</Box>
<Stack
direction="row"
spacing={1.5}
Expand All @@ -86,6 +98,7 @@ function Row({ label, children }) {
Row.propTypes = {
children: PropTypes.node,
label: PropTypes.string.isRequired,
secondary: PropTypes.string,
};

function Bucket({ title, hint, children }) {
Expand All @@ -104,7 +117,7 @@ function Bucket({ title, hint, children }) {
<Box
sx={{
display: 'grid',
gridTemplateColumns: '150px 1fr',
gridTemplateColumns: '180px 1fr',
alignItems: 'center',
columnGap: 3,
rowGap: 2,
Expand Down Expand Up @@ -205,6 +218,23 @@ export default function FullFocusVisibleDemo() {
<Row label="Switch">
<FormControlLabel control={<Switch defaultChecked />} label="Switch" />
</Row>
<Row label="InputBase">
<InputBase placeholder="Test" />
</Row>
<Row label="TextField" secondary="(filled + disableUnderline)">
<TextField
label="Test"
variant="filled"
slotProps={{ input: { disableUnderline: true } }}
/>
</Row>
<Row label="TextField" secondary="(standard + disableUnderline)">
<TextField
label="Test"
variant="standard"
slotProps={{ input: { disableUnderline: true } }}
/>
</Row>
<Row label="Pagination">
<Pagination count={3} />
</Row>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import InputLabel from '@mui/material/InputLabel';
import Select from '@mui/material/Select';
import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
import InputBase from '@mui/material/InputBase';
import AddIcon from '@mui/icons-material/Add';
import StarIcon from '@mui/icons-material/Star';
import HomeIcon from '@mui/icons-material/Home';
Expand All @@ -65,12 +66,31 @@ const theme = createTheme({

const noop = () => {};

function Row({ label, children }: { label: string; children: React.ReactNode }) {
function Row({
label,
secondary,
children,
}: {
label: string;
secondary?: string;
children: React.ReactNode;
}) {
return (
<React.Fragment>
<Typography variant="body2" sx={{ fontWeight: 600, alignSelf: 'center' }}>
{label}
</Typography>
<Box sx={{ alignSelf: 'center' }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{label}
</Typography>
{secondary ? (
<Typography
variant="caption"
color="text.secondary"
sx={{ display: 'block' }}
>
{secondary}
</Typography>
) : null}
</Box>
<Stack
direction="row"
spacing={1.5}
Expand Down Expand Up @@ -106,7 +126,7 @@ function Bucket({
<Box
sx={{
display: 'grid',
gridTemplateColumns: '150px 1fr',
gridTemplateColumns: '180px 1fr',
alignItems: 'center',
columnGap: 3,
rowGap: 2,
Expand Down Expand Up @@ -204,6 +224,23 @@ export default function FullFocusVisibleDemo() {
<Row label="Switch">
<FormControlLabel control={<Switch defaultChecked />} label="Switch" />
</Row>
<Row label="InputBase">
<InputBase placeholder="Test" />
</Row>
<Row label="TextField" secondary="(filled + disableUnderline)">
<TextField
label="Test"
variant="filled"
slotProps={{ input: { disableUnderline: true } }}
/>
</Row>
<Row label="TextField" secondary="(standard + disableUnderline)">
<TextField
label="Test"
variant="standard"
slotProps={{ input: { disableUnderline: true } }}
/>
</Row>
<Row label="Pagination">
<Pagination count={3} />
</Row>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,11 @@ The `styles` prop in the `GlobalStyles` component supports a callback in case yo

{{"demo": "GlobalCssOverrideTheme.js", "iframe": true, "height": 100}}

:::warning
Values in `GlobalStyles` are written into the stylesheet as they are.
If one comes from user input, validate it first, see [dynamic values in `sx`](/system/getting-started/the-sx-prop/#dynamic-values).
:::

If you are already using the [CssBaseline](/material-ui/react-css-baseline/) component for setting baseline styles, you can also add these global styles as overrides for this component. Here is how you can achieve the same by using this approach.

{{"demo": "OverrideCssBaseline.js", "iframe": true, "height": 100}}
Expand Down
5 changes: 5 additions & 0 deletions docs/data/material/customization/theming/theming.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ The following sections cover the most important theme variables:

You can check out the [default theme section](/material-ui/customization/default-theme/) to view the default theme in full.

:::warning
Theme values are written into the stylesheet as they are.
If you build a theme from user input, validate those values first, see [dynamic values in `sx`](/system/getting-started/the-sx-prop/#dynamic-values).
:::

### Custom variables

When using Material聽UI's theme with [MUI聽System](/system/getting-started/) or [any other styling solution](/material-ui/integrations/interoperability/), it can be convenient to add additional variables to the theme so you can use them everywhere.
Expand Down
6 changes: 6 additions & 0 deletions docs/data/system/getting-started/the-sx-prop/the-sx-prop.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,12 @@ Visit the [Content Security Policy guide](/material-ui/guides/content-security-p

{{"demo": "DynamicValues.js", "bg": true}}

:::warning
Values in `sx` are written into the stylesheet as they are, so a value containing `}` can end the rule and add CSS of its own.
If a value comes from user input, check it against the format you expect before using it, for example `/^#[0-9a-f]{6}$/i` for a color from a color picker, or `Number()` for a size.
The same applies to [theme values](/material-ui/customization/theming/#theme-configuration-variables) and [`GlobalStyles`](/material-ui/customization/how-to-customize/#4-global-css-override).
:::

## TypeScript usage

A frequent source of confusion with the `sx` prop is TypeScript's [type widening](https://mariusschulz.com/blog/literal-type-widening-in-typescript), which causes this example not to work as expected:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ const Root = styled('div')(
},
'& a': {
color: `var(--muidocs-palette-error-800, ${lightTheme.palette.error[800]})`,
'& code': { color: 'inherit' },
textDecorationColor: alpha(lightTheme.palette.error.main, 0.4),
'&:hover': {
textDecorationColor: 'inherit',
Expand Down Expand Up @@ -416,6 +417,7 @@ const Root = styled('div')(
},
'& a': {
color: `var(--muidocs-palette-success-900, ${lightTheme.palette.success[900]})`,
'& code': { color: 'inherit' },
textDecorationColor: alpha(lightTheme.palette.success.main, 0.4),
'&:hover': {
textDecorationColor: 'inherit',
Expand All @@ -434,6 +436,7 @@ const Root = styled('div')(
},
'& a': {
color: `var(--muidocs-palette-warning-800, ${lightTheme.palette.warning[800]})`,
'& code': { color: 'inherit' },
textDecorationColor: alpha(lightTheme.palette.warning.main, 0.4),
'&:hover': {
textDecorationColor: 'inherit',
Expand Down Expand Up @@ -747,6 +750,7 @@ const Root = styled('div')(
},
'& a': {
color: `var(--muidocs-palette-error-200, ${darkTheme.palette.error[200]})`,
'& code': { color: 'inherit' },
},
},
'&.MuiCallout-info': {
Expand All @@ -772,6 +776,7 @@ const Root = styled('div')(
},
'& a': {
color: `var(--muidocs-palette-success-100, ${darkTheme.palette.success[100]})`,
'& code': { color: 'inherit' },
},
},
'&.MuiCallout-warning': {
Expand All @@ -786,6 +791,7 @@ const Root = styled('div')(
},
'& a': {
color: `var(--muidocs-palette-warning-100, ${darkTheme.palette.warning[100]})`,
'& code': { color: 'inherit' },
},
},
},
Expand Down
1 change: 1 addition & 0 deletions packages/mui-material/src/FilledInput/FilledInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ const FilledInput = React.forwardRef(function FilledInput(inProps, ref) {
slotProps={componentsProps}
fullWidth={fullWidth}
inputComponent={inputComponent}
internalDisabledThemeFocusVisible={!disableUnderline}
multiline={multiline}
ref={ref}
type={type}
Expand Down
44 changes: 42 additions & 2 deletions packages/mui-material/src/FilledInput/FilledInput.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest';
import { createRenderer, screen } from '@mui/internal-test-utils';
import { styled } from '@mui/material/styles';
import { act, createRenderer, screen, isJsdom } from '@mui/internal-test-utils';
import { createTheme, styled, ThemeProvider } from '@mui/material/styles';
import FilledInput, { filledInputClasses as classes } from '@mui/material/FilledInput';
import InputBase from '@mui/material/InputBase';
import describeConformance from '../../test/describeConformance';
Expand Down Expand Up @@ -108,4 +108,44 @@ describe('<FilledInput />', () => {
render(<FilledInput notched data-testid="root" />);
expect(screen.getByTestId('root')).not.to.have.attribute('notched');
});

describe('theme.focusVisible', () => {
const theme = createTheme({ focusVisible: true });

it.skipIf(isJsdom())('renders the ring on focus when the underline is disabled', async () => {
render(
<ThemeProvider theme={theme}>
<FilledInput disableUnderline data-testid="root" />
</ThemeProvider>,
);
const root = screen.getByTestId('root');

expect(root).toHaveComputedStyle({ outlineStyle: 'none' });

await act(async () => {
root.querySelector('input').focus();
});

expect(root).toHaveComputedStyle({
outlineStyle: 'solid',
outlineWidth: '2px',
outlineOffset: '2px',
});
});

it.skipIf(isJsdom())('does not render the ring when the underline is shown', async () => {
render(
<ThemeProvider theme={theme}>
<FilledInput data-testid="root" />
</ThemeProvider>,
);
const root = screen.getByTestId('root');

await act(async () => {
root.querySelector('input').focus();
});

expect(root).toHaveComputedStyle({ outlineStyle: 'none' });
});
});
});
10 changes: 10 additions & 0 deletions packages/mui-material/src/Input/Input.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
InputBaseRoot,
InputBaseInput,
} from '../InputBase/InputBase';
import { applyInsetFocusVisible } from '../styles/focusVisible';

const useUtilityClasses = (ownerState) => {
const { classes, disableUnderline } = ownerState;
Expand Down Expand Up @@ -69,6 +70,14 @@ const InputRoot = styled(InputBaseRoot, {
},
},
},
{
props: ({ ownerState }) => ownerState.disableUnderline && theme.focusVisible,
style: {
// prevent indicator too close to the label in TextField
...applyInsetFocusVisible(1),
paddingInline: 4,
},
},
{
props: ({ ownerState }) => !ownerState.disableUnderline,
style: {
Expand Down Expand Up @@ -172,6 +181,7 @@ const Input = React.forwardRef(function Input(inProps, ref) {
slotProps={componentsProps}
fullWidth={fullWidth}
inputComponent={inputComponent}
internalDisabledThemeFocusVisible={!disableUnderline}
multiline={multiline}
ref={ref}
type={type}
Expand Down
44 changes: 43 additions & 1 deletion packages/mui-material/src/Input/Input.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { describe, it, expect } from 'vitest';
import { createRenderer, screen } from '@mui/internal-test-utils';
import { act, createRenderer, screen, isJsdom } from '@mui/internal-test-utils';
import InputBase from '@mui/material/InputBase';
import Input, { inputClasses as classes } from '@mui/material/Input';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import describeConformance from '../../test/describeConformance';

describe('<Input />', () => {
Expand Down Expand Up @@ -49,4 +50,45 @@ describe('<Input />', () => {
render(<Input notched data-testid="root" />);
expect(screen.getByTestId('root')).not.to.have.attribute('notched');
});

describe('theme.focusVisible', () => {
const theme = createTheme({ focusVisible: true });

it.skipIf(isJsdom())('renders the ring on focus when the underline is disabled', async () => {
render(
<ThemeProvider theme={theme}>
<Input disableUnderline data-testid="root" />
</ThemeProvider>,
);
const root = screen.getByTestId('root');

expect(root).toHaveComputedStyle({ outlineStyle: 'none' });

await act(async () => {
root.querySelector('input').focus();
});

expect(root).toHaveComputedStyle({
outlineStyle: 'solid',
outlineWidth: '2px',
// inset, so the ring does not crowd the TextField label
outlineOffset: '-2px',
});
});

it.skipIf(isJsdom())('does not render the ring when the underline is shown', async () => {
render(
<ThemeProvider theme={theme}>
<Input data-testid="root" />
</ThemeProvider>,
);
const root = screen.getByTestId('root');

await act(async () => {
root.querySelector('input').focus();
});

expect(root).toHaveComputedStyle({ outlineStyle: 'none' });
});
});
});
Loading
Loading