Skip to content
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

[pickers] POC: PickersTextField styling - outlined variant #10778

Merged
merged 28 commits into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ec13e1a
wip
noraleonte Oct 13, 2023
0201c2d
Merge branch 'master' of https://github.com/noraleonte/mui-x into fak…
noraleonte Oct 17, 2023
349ed30
outlined input wip
noraleonte Oct 18, 2023
23c017a
WIP 1
noraleonte Oct 23, 2023
a3d6637
fix conflicts
noraleonte Oct 24, 2023
e82202e
adjustments
noraleonte Oct 24, 2023
a8b624e
fix todos
noraleonte Oct 24, 2023
1052aca
scripts
noraleonte Oct 24, 2023
1e938c7
adornments wip
noraleonte Oct 30, 2023
f3a63a2
Iteration 1 outlined FakeTextField
noraleonte Nov 2, 2023
55164af
Merge branch 'master' of https://github.com/noraleonte/mui-x into fak…
noraleonte Nov 2, 2023
e4047b2
scripts
noraleonte Nov 2, 2023
1b130ca
Merge branch 'master' of https://github.com/noraleonte/mui-x into fak…
noraleonte Nov 6, 2023
babb6e7
adjustments
noraleonte Nov 9, 2023
c425072
update fakeInput
noraleonte Nov 17, 2023
595007d
Merge branch 'next' of https://github.com/noraleonte/mui-x into fake-…
noraleonte Nov 20, 2023
25f711c
wip
noraleonte Nov 22, 2023
68fe9f1
Apply suggestions from code review - Flavien
noraleonte Nov 22, 2023
66db521
wip
noraleonte Nov 22, 2023
9763632
Merge branch 'fake-textfield-styling' of https://github.com/noraleont…
noraleonte Nov 22, 2023
5881456
remove demo
noraleonte Nov 22, 2023
2aa1414
make ts happy
noraleonte Nov 22, 2023
9a76cd0
ts
noraleonte Nov 22, 2023
fb1acd7
ts
noraleonte Nov 22, 2023
3969f89
fix
noraleonte Nov 22, 2023
7a21081
Cleanup
noraleonte Nov 24, 2023
15bed58
Apply styles to separators
noraleonte Nov 24, 2023
05b6540
rename fake TextField
noraleonte Nov 24, 2023
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,323 @@
import * as React from 'react';
import clsx from 'clsx';
import Box from '@mui/material/Box';
import { useFormControl } from '@mui/material/FormControl';
import { styled } from '@mui/material/styles';
import useForkRef from '@mui/utils/useForkRef';
import {
unstable_composeClasses as composeClasses,
unstable_capitalize as capitalize,
visuallyHidden,
} from '@mui/utils';
import { fakeInputClasses, getFakeInputUtilityClass } from './fakeTextFieldClasses';
import Outline from './Outline';
import { FakeInputElement, FakeInputProps } from './FakeInput.types';

const SectionsWrapper = styled(Box, {
name: 'MuiFakeInput',
slot: 'Root',
overridesResolver: (props, styles) => styles.root,
})<{ ownerState: OwnerStateType }>(({ theme, ownerState }) => {
const borderColor =
theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.23)' : 'rgba(255, 255, 255, 0.23)';
return {
cursor: 'text',
padding: '16.5px 14px',
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'center',
width: ownerState.fullWidth ? '100%' : '25ch',
position: 'relative',
borderRadius: (theme.vars || theme).shape.borderRadius,
[`&:hover .${fakeInputClasses.notchedOutline}`]: {
borderColor: (theme.vars || theme).palette.text.primary,
},

// Reset on touch devices, it doesn't add specificity
'@media (hover: none)': {
[`&:hover .${fakeInputClasses.notchedOutline}`]: {
borderColor: theme.vars
? `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.23)`
: borderColor,
},
},
[`&.${fakeInputClasses.focused} .${fakeInputClasses.notchedOutline}`]: {
borderStyle: 'solid',
borderColor: (theme.vars || theme).palette[ownerState.color].main,
borderWidth: 2,
},
[`&.${fakeInputClasses.disabled}`]: {
[`& .${fakeInputClasses.notchedOutline}`]: {
borderColor: (theme.vars || theme).palette.action.disabled,
},

'*': {
color: (theme.vars || theme).palette.action.disabled,
},
},

[`&.${fakeInputClasses.error} .${fakeInputClasses.notchedOutline}`]: {
borderColor: (theme.vars || theme).palette.error.main,
},

...(ownerState.size === 'small' && {
padding: '8.5px 14px',
}),
};
});
noraleonte marked this conversation as resolved.
Show resolved Hide resolved
const SectionsContainer = styled('div', {
name: 'MuiFakeInput',
slot: 'Input',
overridesResolver: (props, styles) => styles.input,
})<{ ownerState: OwnerStateType }>(({ theme, ownerState }) => {
noraleonte marked this conversation as resolved.
Show resolved Hide resolved
return {
fontFamily: theme.typography.fontFamily,
fontSize: 'inherit',
lineHeight: '1.4375em', // 23px
flexGrow: 1,
visibility: ownerState.adornedStart || ownerState.focused ? 'visible' : 'hidden',
};
});
const SectionContainer = styled('span', {
noraleonte marked this conversation as resolved.
Show resolved Hide resolved
name: 'MuiFakeInput',
slot: 'Section',
overridesResolver: (props, styles) => styles.section,
})(({ theme }) => {
return {
noraleonte marked this conversation as resolved.
Show resolved Hide resolved
fontFamily: theme.typography.fontFamily,
fontSize: 'inherit',
lineHeight: '1.4375em', // 23px
flexGrow: 1,
};
});
const SectionInput = styled('span', {
noraleonte marked this conversation as resolved.
Show resolved Hide resolved
name: 'MuiFakeInput',
slot: 'Content',
overridesResolver: (props, styles) => styles.content,
})(({ theme }) => {
return {
noraleonte marked this conversation as resolved.
Show resolved Hide resolved
fontFamily: theme.typography.fontFamily,
lineHeight: '1.4375em', // 23px
letterSpacing: 'inherit',
width: 'fit-content',
};
});

const FakeHiddenInput = styled('input', {
name: 'MuiFakeInput',
slot: 'HiddenInput',
overridesResolver: (props, styles) => styles.hiddenInput,
})({
...visuallyHidden,
});

const NotchedOutlineRoot = styled(Outline, {
name: 'MuiFakeInput',
slot: 'NotchedOutline',
overridesResolver: (props, styles) => styles.notchedOutline,
})(({ theme }) => {
const borderColor =
theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.23)' : 'rgba(255, 255, 255, 0.23)';
return {
borderColor: theme.vars
? `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.23)`
: borderColor,
};
});

function InputContent({
elements,
contentEditable,
ownerState,
}: {
elements: FakeInputElement[];
contentEditable?: string | boolean;
ownerState: OwnerStateType;
}) {
if (contentEditable) {
return elements
.map(({ content, before, after }) => `${before.children}${content.children}${after.children}`)
.join('');
}

return (
<React.Fragment>
{elements.map(({ container, content, before, after }, elementIndex) => (
<SectionContainer key={elementIndex} {...container}>
<span {...before} className={clsx(fakeInputClasses.before, before?.className)} />
<SectionInput
{...content}
className={clsx(fakeInputClasses.content, content?.className)}
{...{ ownerState }}
/>
<span {...after} className={clsx(fakeInputClasses.after, after?.className)} />
</SectionContainer>
))}
</React.Fragment>
);
}

const useUtilityClasses = (ownerState: OwnerStateType) => {
const {
focused,
disabled,
error,
classes,
fullWidth,
color,
size,
endAdornment,
startAdornment,
} = ownerState;

const slots = {
root: [
'root',
focused && !disabled && 'focused',
disabled && 'disabled',
error && 'error',
fullWidth && 'fullWidth',
`color${capitalize(color)}`,
size === 'small' && 'inputSizeSmall',
Boolean(startAdornment) && 'adornedStart',
Boolean(endAdornment) && 'adornedEnd',
],
notchedOutline: ['notchedOutline'],
before: ['before'],
after: ['after'],
content: ['content'],
input: ['input'],
};

return composeClasses(slots, getFakeInputUtilityClass, classes);
};

// TODO: move to utils
// Separates the state props from the form control
function formControlState({ props, states, muiFormControl }) {
return states.reduce((acc, state) => {
acc[state] = props[state];

if (muiFormControl) {
if (typeof props[state] === 'undefined') {
acc[state] = muiFormControl[state];
}
}

return acc;
}, {});
}

interface OwnerStateType extends FakeInputProps {
color: 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning';
disabled?: boolean;
error?: boolean;
fullWidth?: boolean;
variant?: 'filled' | 'outlined' | 'standard';
size?: 'small' | 'medium';
adornedStart?: boolean;
}

const FakeInput = React.forwardRef(function FakeInput(
props: FakeInputProps,
ref: React.Ref<HTMLDivElement>,
) {
const {
elements,
defaultValue,
label,
onFocus,
onWrapperClick,
onBlur,
valueStr,
onValueStrChange,
id,
InputProps,
inputProps,
autoFocus,
valueType,
ownerState: ownerStateProp,
endAdornment,
startAdornment,
...other
} = props;

const inputRef = React.useRef<HTMLDivElement>(null);
const handleInputRef = useForkRef(ref, inputRef);

const muiFormControl = useFormControl();
const fcs = formControlState({
props,
muiFormControl,
states: [
'color',
'disabled',
'error',
'focused',
'size',
'required',
'fullWidth',
'adornedStart',
],
});

React.useEffect(() => {
if (muiFormControl) {
muiFormControl.setAdornedStart(Boolean(startAdornment));
}
}, [muiFormControl, startAdornment]);

const ownerState = {
...props,
...ownerStateProp,
color: fcs.color || 'primary',
disabled: fcs.disabled,
error: fcs.error,
focused: fcs.focused,
fullWidth: fcs.fullWidth,
size: fcs.size,
};
const classes = useUtilityClasses(ownerState);

return (
<SectionsWrapper
ref={handleInputRef}
{...other}
className={classes.root}
onClick={onWrapperClick}
ownerState={ownerState}
aria-invalid={fcs.error}
>
{startAdornment}
<SectionsContainer ownerState={ownerState} className={classes.input}>
<InputContent {...{ elements, contentEditable: other.contentEditable, ownerState }} />
noraleonte marked this conversation as resolved.
Show resolved Hide resolved
<FakeHiddenInput
value={valueStr}
onChange={onValueStrChange}
id={id}
aria-hidden="true"
tabIndex={-1}
/>
</SectionsContainer>
{endAdornment}
<NotchedOutlineRoot
shrink={fcs.adornedStart || fcs.focused}
notched={fcs.adornedStart || fcs.focused}
className={classes.notchedOutline}
label={
label != null && label !== '' && fcs.required ? (
<React.Fragment>
{label}
&thinsp;{'*'}
</React.Fragment>
) : (
label
)
}
{...{ ownerState }}
noraleonte marked this conversation as resolved.
Show resolved Hide resolved
/>
</SectionsWrapper>
);
});

export default FakeInput;
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { FieldsTextFieldProps } from '../../models';

export interface FakeInputElement {
container: React.HTMLAttributes<HTMLSpanElement>;
content: React.HTMLAttributes<HTMLSpanElement>;
before: React.HTMLAttributes<HTMLSpanElement>;
after: React.HTMLAttributes<HTMLSpanElement>;
}

export interface FakeInputProps extends FieldsTextFieldProps {
elements: FakeInputElement[];
areAllSectionsEmpty?: boolean;
valueStr: string;
onValueStrChange: React.ChangeEventHandler<HTMLInputElement>;
id?: string;
InputProps: any;
noraleonte marked this conversation as resolved.
Show resolved Hide resolved
inputProps: any;
autoFocus?: boolean;
ownerState?: any;
valueType: 'value' | 'placeholder';
onWrapperClick: () => void;
defaultValue: string;
label?: string;
endAdornment?: React.ReactNode;
startAdornment?: React.ReactNode;
onBlur?: React.FocusEventHandler;
onChange?: React.FormEventHandler;
onFocus?: React.FocusEventHandler;
onKeyDown?: React.KeyboardEventHandler;
onKeyUp?: React.KeyboardEventHandler;
}
Loading