From 195cc862ea8fc4b5abfac04bca250337314262a1 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Thu, 1 Sep 2022 08:18:08 +0700 Subject: [PATCH 001/192] init Autocomplete --- .../mui-joy/src/Autocomplete/Autocomplete.tsx | 685 ++++++++++++++++++ .../src/Autocomplete/AutocompleteProps.ts | 230 ++++++ .../src/Autocomplete/autocompleteClasses.ts | 90 +++ packages/mui-joy/src/Autocomplete/index.ts | 4 + .../mui-joy/src/IconButton/IconButton.tsx | 2 +- .../src/ListItemButton/ListItemButton.tsx | 2 +- .../src/internal/svg-icons/ArrowDropDown.tsx | 7 + 7 files changed, 1018 insertions(+), 2 deletions(-) create mode 100644 packages/mui-joy/src/Autocomplete/Autocomplete.tsx create mode 100644 packages/mui-joy/src/Autocomplete/AutocompleteProps.ts create mode 100644 packages/mui-joy/src/Autocomplete/autocompleteClasses.ts create mode 100644 packages/mui-joy/src/Autocomplete/index.ts create mode 100644 packages/mui-joy/src/internal/svg-icons/ArrowDropDown.tsx diff --git a/packages/mui-joy/src/Autocomplete/Autocomplete.tsx b/packages/mui-joy/src/Autocomplete/Autocomplete.tsx new file mode 100644 index 00000000000000..bc0880fd6f3b74 --- /dev/null +++ b/packages/mui-joy/src/Autocomplete/Autocomplete.tsx @@ -0,0 +1,685 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import PropTypes from 'prop-types'; +import { OverridableComponent } from '@mui/types'; +import { unstable_capitalize as capitalize } from '@mui/utils'; +import composeClasses from '@mui/base/composeClasses'; +import { useAutocomplete, createFilterOptions } from '@mui/base/AutocompleteUnstyled'; +import PopperUnstyled from '@mui/base/PopperUnstyled'; +import { useThemeProps } from '../styles'; +import ClearIcon from '../internal/svg-icons/Close'; +import ArrowDropDownIcon from '../internal/svg-icons/ArrowDropDown'; +import styled from '../styles/styled'; +import Chip from '../Chip'; +import { IconButtonRoot } from '../IconButton/IconButton'; +import ListProvider, { scopedVariables } from '../List/ListProvider'; +import { ListRoot } from '../List/List'; +import { ListItemButtonRoot } from '../ListItemButton/ListItemButton'; +import autocompleteClasses, { getAutocompleteUtilityClass } from './autocompleteClasses'; + +const useUtilityClasses = (ownerState) => { + const { + disablePortal, + focused, + fullWidth, + hasClearIcon, + hasPopupIcon, + inputFocused, + popupOpen, + size, + } = ownerState; + + const slots = { + root: [ + 'root', + focused && 'focused', + fullWidth && 'fullWidth', + hasClearIcon && 'hasClearIcon', + hasPopupIcon && 'hasPopupIcon', + ], + inputRoot: ['inputRoot'], + input: ['input', inputFocused && 'inputFocused'], + tag: ['tag', `tagSize${capitalize(size)}`], + endAdornment: ['endAdornment'], + clearIndicator: ['clearIndicator'], + popupIndicator: ['popupIndicator', popupOpen && 'popupIndicatorOpen'], + popper: ['popper', disablePortal && 'popperDisablePortal'], + paper: ['paper'], + listbox: ['listbox'], + loading: ['loading'], + noOptions: ['noOptions'], + option: ['option'], + groupLabel: ['groupLabel'], + groupUl: ['groupUl'], + }; + + return composeClasses(slots, getAutocompleteUtilityClass, {}); +}; + +const AutocompleteRoot = styled('div', { + name: 'JoyAutocomplete', + slot: 'Root', + overridesResolver: (props, styles) => styles.root, +})(({ ownerState }) => ({ + [`&.${autocompleteClasses.focused} .${autocompleteClasses.clearIndicator}`]: { + visibility: 'visible', + }, + /* Avoid double tap issue on iOS */ + '@media (pointer: fine)': { + [`&:hover .${autocompleteClasses.clearIndicator}`]: { + visibility: 'visible', + }, + }, + ...(ownerState.fullWidth && { + width: '100%', + }), + [`& .${autocompleteClasses.tag}`]: { + margin: 3, + maxWidth: 'calc(100% - 6px)', + ...(ownerState.size === 'small' && { + margin: 2, + maxWidth: 'calc(100% - 4px)', + }), + }, + [`& .${autocompleteClasses.inputRoot}`]: { + flexWrap: 'wrap', + [`.${autocompleteClasses.hasPopupIcon}&, .${autocompleteClasses.hasClearIcon}&`]: { + paddingRight: 26 + 4, + }, + [`.${autocompleteClasses.hasPopupIcon}.${autocompleteClasses.hasClearIcon}&`]: { + paddingRight: 52 + 4, + }, + [`& .${autocompleteClasses.input}`]: { + width: 0, + minWidth: 30, + }, + }, + // [`& .${inputClasses.root}`]: { + // paddingBottom: 1, + // '& .MuiInput-input': { + // padding: '4px 4px 4px 0px', + // }, + // }, + // [`& .${inputClasses.root}.${inputBaseClasses.sizeSmall}`]: { + // [`& .${inputClasses.input}`]: { + // padding: '2px 4px 3px 0', + // }, + // }, + // [`& .${outlinedInputClasses.root}`]: { + // padding: 9, + // [`.${autocompleteClasses.hasPopupIcon}&, .${autocompleteClasses.hasClearIcon}&`]: { + // paddingRight: 26 + 4 + 9, + // }, + // [`.${autocompleteClasses.hasPopupIcon}.${autocompleteClasses.hasClearIcon}&`]: { + // paddingRight: 52 + 4 + 9, + // }, + // [`& .${autocompleteClasses.input}`]: { + // padding: '7.5px 4px 7.5px 6px', + // }, + // [`& .${autocompleteClasses.endAdornment}`]: { + // right: 9, + // }, + // }, + // [`& .${outlinedInputClasses.root}.${inputBaseClasses.sizeSmall}`]: { + // // Don't specify paddingRight, as it overrides the default value set when there is only + // // one of the popup or clear icon as the specificity is equal so the latter one wins + // paddingTop: 6, + // paddingBottom: 6, + // paddingLeft: 6, + // [`& .${autocompleteClasses.input}`]: { + // padding: '2.5px 4px 2.5px 6px', + // }, + // }, + // [`& .${filledInputClasses.root}`]: { + // paddingTop: 19, + // paddingLeft: 8, + // [`.${autocompleteClasses.hasPopupIcon}&, .${autocompleteClasses.hasClearIcon}&`]: { + // paddingRight: 26 + 4 + 9, + // }, + // [`.${autocompleteClasses.hasPopupIcon}.${autocompleteClasses.hasClearIcon}&`]: { + // paddingRight: 52 + 4 + 9, + // }, + // [`& .${filledInputClasses.input}`]: { + // padding: '7px 4px', + // }, + // [`& .${autocompleteClasses.endAdornment}`]: { + // right: 9, + // }, + // }, + // [`& .${filledInputClasses.root}.${inputBaseClasses.sizeSmall}`]: { + // paddingBottom: 1, + // [`& .${filledInputClasses.input}`]: { + // padding: '2.5px 4px', + // }, + // }, + // [`& .${inputBaseClasses.hiddenLabel}`]: { + // paddingTop: 8, + // }, + [`& .${autocompleteClasses.input}`]: { + flexGrow: 1, + textOverflow: 'ellipsis', + opacity: 0, + ...(ownerState.inputFocused && { + opacity: 1, + }), + }, +})); + +const AutocompleteEndAdornment = styled('div', { + name: 'JoyAutocomplete', + slot: 'EndAdornment', + overridesResolver: (props, styles) => styles.endAdornment, +})({ + // We use a position absolute to support wrapping tags. + position: 'absolute', + right: 0, + top: 'calc(50% - 14px)', // Center vertically +}); + +const AutocompleteClearIndicator = styled(IconButtonRoot, { + name: 'JoyAutocomplete', + slot: 'ClearIndicator', + overridesResolver: (props, styles) => styles.clearIndicator, +})({ + marginRight: -2, + padding: 4, + visibility: 'hidden', +}); + +const AutocompletePopupIndicator = styled(IconButtonRoot, { + name: 'JoyAutocomplete', + slot: 'PopupIndicator', + overridesResolver: (props, styles) => styles.popupIndicator, +})(({ ownerState }) => ({ + padding: 2, + marginRight: -2, + ...(ownerState.popupOpen && { + transform: 'rotate(180deg)', + }), +})); + +const AutocompletePopper = styled(ListRoot, { + name: 'JoyAutocomplete', + slot: 'Popper', + overridesResolver: (props, styles) => styles.popper, +})(({ theme, ownerState }) => ({ + '--List-radius': theme.vars.radius.sm, + '--List-item-stickyBackground': theme.vars.palette.background.surface, // for sticky List + '--List-item-stickyTop': 'calc(var(--List-padding, var(--List-divider-gap)) * -1)', // negative amount of the List's padding block + zIndex: 1200, + ...scopedVariables, + boxShadow: theme.vars.shadow.md, + overflow: 'auto', + maxHeight: '40vh', +})); + +const AutocompleteLoading = styled('div', { + name: 'JoyAutocomplete', + slot: 'Loading', + overridesResolver: (props, styles) => styles.loading, +})(({ theme }) => ({ + color: (theme.vars || theme).palette.text.secondary, + padding: '14px 16px', +})); + +const AutocompleteNoOptions = styled('div', { + name: 'JoyAutocomplete', + slot: 'NoOptions', + overridesResolver: (props, styles) => styles.noOptions, +})(({ theme }) => ({ + color: (theme.vars || theme).palette.text.secondary, + padding: '14px 16px', +})); + +const AutocompleteListbox = styled('div', { + name: 'JoyAutocomplete', + slot: 'Listbox', + overridesResolver: (props, styles) => styles.listbox, +})(({ theme }) => ({ + listStyle: 'none', + margin: 0, + padding: '8px 0', + maxHeight: '40vh', + overflow: 'auto', + // [`& .${autocompleteClasses.option}`]: { + // minHeight: 48, + // display: 'flex', + // overflow: 'hidden', + // justifyContent: 'flex-start', + // alignItems: 'center', + // cursor: 'pointer', + // paddingTop: 6, + // boxSizing: 'border-box', + // outline: '0', + // WebkitTapHighlightColor: 'transparent', + // paddingBottom: 6, + // paddingLeft: 16, + // paddingRight: 16, + // [theme.breakpoints.up('sm')]: { + // minHeight: 'auto', + // }, + // [`&.${autocompleteClasses.focused}`]: { + // // backgroundColor: (theme.vars || theme).palette.action.hover, + // // Reset on touch devices, it doesn't add specificity + // '@media (hover: none)': { + // backgroundColor: 'transparent', + // }, + // }, + // '&[aria-disabled="true"]': { + // // opacity: (theme.vars || theme).palette.action.disabledOpacity, + // pointerEvents: 'none', + // }, + // [`&.${autocompleteClasses.focusVisible}`]: { + // // backgroundColor: (theme.vars || theme).palette.action.focus, + // }, + // // '&[aria-selected="true"]': { + // // backgroundColor: theme.vars + // // ? `rgba(${theme.vars.palette.primary.mainChannel} / ${theme.vars.palette.action.selectedOpacity})` + // // : alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity), + // // [`&.${autocompleteClasses.focused}`]: { + // // backgroundColor: theme.vars + // // ? `rgba(${theme.vars.palette.primary.mainChannel} / calc(${theme.vars.palette.action.selectedOpacity} + ${theme.vars.palette.action.hoverOpacity}))` + // // : alpha( + // // theme.palette.primary.main, + // // theme.palette.action.selectedOpacity + theme.palette.action.hoverOpacity, + // // ), + // // // Reset on touch devices, it doesn't add specificity + // // '@media (hover: none)': { + // // backgroundColor: (theme.vars || theme).palette.action.selected, + // // }, + // // }, + // // [`&.${autocompleteClasses.focusVisible}`]: { + // // backgroundColor: theme.vars + // // ? `rgba(${theme.vars.palette.primary.mainChannel} / calc(${theme.vars.palette.action.selectedOpacity} + ${theme.vars.palette.action.focusOpacity}))` + // // : alpha( + // // theme.palette.primary.main, + // // theme.palette.action.selectedOpacity + theme.palette.action.focusOpacity, + // // ), + // // }, + // // }, + // }, +})); + +const AutocompleteGroupLabel = styled('li', { + name: 'JoyAutocomplete', + slot: 'GroupLabel', + overridesResolver: (props, styles) => styles.groupLabel, +})(({ theme }) => ({ + backgroundColor: theme.vars.palette.background.surface, + top: -8, +})); + +const AutocompleteGroupUl = styled('ul', { + name: 'JoyAutocomplete', + slot: 'GroupUl', + overridesResolver: (props, styles) => styles.groupUl, +})({ + padding: 0, + [`& .${autocompleteClasses.option}`]: { + paddingLeft: 24, + }, +}); + +const Autocomplete = React.forwardRef(function Autocomplete(inProps, ref) { + const props = useThemeProps({ + props: inProps, + name: 'JoyAutocomplete', + }); + + const { + autoComplete = false, + autoHighlight = false, + autoSelect = false, + blurOnSelect = false, + ChipProps, + className, + clearIcon = , + clearOnBlur = !props.freeSolo, + clearOnEscape = false, + clearText = 'Clear', + closeText = 'Close', + componentsProps = {}, + defaultValue = props.multiple ? [] : null, + disableClearable = false, + disableCloseOnSelect = false, + disabled = false, + disabledItemsFocusable = false, + disableListWrap = false, + disablePortal = false, + filterOptions, + filterSelectedOptions = false, + forcePopupIcon = 'auto', + freeSolo = false, + fullWidth = false, + getLimitTagsText = (more) => `+${more}`, + getOptionDisabled, + getOptionLabel = (option) => option.label ?? option, + isOptionEqualToValue, + groupBy, + handleHomeEndKeys = !props.freeSolo, + id: idProp, + includeInputInList = false, + inputValue: inputValueProp, + limitTags = -1, + ListboxComponent = 'ul', + ListboxProps, + loading = false, + loadingText = 'Loading…', + multiple = false, + noOptionsText = 'No options', + onChange, + onClose, + onHighlightChange, + onInputChange, + onOpen, + open, + openOnFocus = false, + openText = 'Open', + options, + popupIcon = , + readOnly = false, + renderGroup: renderGroupProp, + renderInput, + renderOption: renderOptionProp, + renderTags, + selectOnFocus = !props.freeSolo, + size = 'md', + value: valueProp, + ...other + } = props; + + const { + getRootProps, + getInputProps, + getInputLabelProps, + getPopupIndicatorProps, + getClearProps, + getTagProps, + getListboxProps, + getOptionProps, + value, + dirty, + id, + popupOpen, + focused, + focusedTag, + anchorEl, + setAnchorEl, + inputValue, + groupedOptions, + } = useAutocomplete({ ...props, componentName: 'Autocomplete' }); + + const hasClearIcon = !disableClearable && !disabled && dirty && !readOnly; + const hasPopupIcon = (!freeSolo || forcePopupIcon === true) && forcePopupIcon !== false; + + // If you modify this, make sure to keep the `AutocompleteOwnerState` type in sync. + const ownerState = { + ...props, + disablePortal, + focused, + fullWidth, + hasClearIcon, + hasPopupIcon, + inputFocused: focusedTag === -1, + popupOpen, + size, + }; + + const classes = useUtilityClasses(ownerState); + + let startDecorator; + + if (multiple && value.length > 0) { + const getCustomizedTagProps = (params) => ({ + className: classes.tag, + disabled, + ...getTagProps(params), + }); + + if (renderTags) { + startDecorator = renderTags(value, getCustomizedTagProps, ownerState); + } else { + startDecorator = value.map((option, index) => ( + + {getOptionLabel(option)} + + )); + } + } + + if (limitTags > -1 && Array.isArray(startDecorator)) { + const more = startDecorator.length - limitTags; + if (!focused && more > 0) { + startDecorator = startDecorator.splice(0, limitTags); + startDecorator.push( + + {getLimitTagsText(more)} + , + ); + } + } + + const defaultRenderGroup = (params) => ( + + + {params.group} + + + {params.children} + + + ); + + const renderGroup = renderGroupProp || defaultRenderGroup; + const defaultRenderOption = (props2, option) => ( + + {getOptionLabel(option)} + + ); + const renderOption = renderOptionProp || defaultRenderOption; + + const renderListOption = (option, index) => { + const optionProps = getOptionProps({ option, index }); + + return renderOption({ ...optionProps, className: classes.option }, option, { + selected: optionProps['aria-selected'], + inputValue, + }); + }; + + return ( + + + {renderInput({ + id, + disabled, + fullWidth: true, + size: size === 'sm' ? 'sm' : undefined, + InputLabelProps: getInputLabelProps(), + InputProps: { + ref: setAnchorEl, + className: classes.inputRoot, + startDecorator, + ...((hasClearIcon || hasPopupIcon) && { + endDecorator: ( + + {hasClearIcon ? ( + + {clearIcon} + + ) : null} + + {hasPopupIcon ? ( + + {popupIcon} + + ) : null} + + ), + }), + }, + inputProps: { + className: classes.input, + disabled, + readOnly, + ...getInputProps(), + }, + })} + + {anchorEl ? ( + + + {loading && groupedOptions.length === 0 ? ( + + {loadingText} + + ) : null} + {groupedOptions.length === 0 && !freeSolo && !loading ? ( + { + // Prevent input blur when interacting with the "no options" content + event.preventDefault(); + }} + > + {noOptionsText} + + ) : null} + {groupedOptions.map((option, index) => { + if (groupBy) { + return renderGroup({ + key: option.key, + group: option.group, + children: option.options.map((option2, index2) => + renderListOption(option2, option.index + index2), + ), + }); + } + return renderListOption(option, index); + })} + {/* {groupedOptions.length > 0 ? ( + + {groupedOptions.map((option, index) => { + if (groupBy) { + return renderGroup({ + key: option.key, + group: option.group, + children: option.options.map((option2, index2) => + renderListOption(option2, option.index + index2), + ), + }); + } + return renderListOption(option, index); + })} + + ) : null} */} + + + ) : null} + + ); +}); + +Autocomplete.propTypes /* remove-proptypes */ = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * The content of the component. + */ + children: PropTypes.node, + /** + * @ignore + */ + className: PropTypes.string, + /** + * The color of the component. It supports those theme colors that make sense for this component. + * @default 'neutral' + */ + color: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([ + PropTypes.oneOf(['danger', 'info', 'neutral', 'primary', 'success', 'warning']), + PropTypes.string, + ]), + /** + * The component used for the root node. + * Either a string to use a HTML element or a component. + */ + component: PropTypes.elementType, + /** + * The system prop that allows defining system overrides as well as additional CSS styles. + */ + sx: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), + PropTypes.func, + PropTypes.object, + ]), + /** + * The variant to use. + * @default 'plain' + */ + variant: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([ + PropTypes.oneOf(['outlined', 'plain', 'soft', 'solid']), + PropTypes.string, + ]), +} as any; + +export default Autocomplete; diff --git a/packages/mui-joy/src/Autocomplete/AutocompleteProps.ts b/packages/mui-joy/src/Autocomplete/AutocompleteProps.ts new file mode 100644 index 00000000000000..942b934d337dfc --- /dev/null +++ b/packages/mui-joy/src/Autocomplete/AutocompleteProps.ts @@ -0,0 +1,230 @@ +import * as React from 'react'; +import { OverridableStringUnion } from '@mui/types'; +import { + useAutocomplete, + AutocompleteChangeDetails, + AutocompleteChangeReason, + AutocompleteCloseReason, + AutocompleteInputChangeReason, + createFilterOptions, + UseAutocompleteProps, +} from '@mui/base'; +import { SxProps } from '../styles/types'; + +export { + AutocompleteChangeDetails, + AutocompleteChangeReason, + AutocompleteCloseReason, + AutocompleteInputChangeReason, + createFilterOptions, +}; + +export type AutocompleteOwnerState< + T, + Multiple extends boolean | undefined, + DisableClearable extends boolean | undefined, + FreeSolo extends boolean | undefined, +> = AutocompleteProps & { + disablePortal: boolean; + focused: boolean; + fullWidth: boolean; + hasClearIcon: boolean; + hasPopupIcon: boolean; + inputFocused: boolean; + popupOpen: boolean; + size: OverridableStringUnion<'small' | 'medium', AutocompletePropsSizeOverrides>; +}; + +export type AutocompleteRenderGetTagProps = ({ index }: { index: number }) => { + key: number; + className: string; + disabled: boolean; + 'data-tag-index': number; + tabIndex: -1; + onDelete: (event: any) => void; +}; + +export interface AutocompleteRenderOptionState { + inputValue: string; + selected: boolean; +} + +export interface AutocompleteRenderGroupParams { + key: string; + group: string; + children?: React.ReactNode; +} + +export interface AutocompleteRenderInputParams { + id: string; + disabled: boolean; + fullWidth: boolean; + size: 'small' | undefined; + InputLabelProps: ReturnType['getInputLabelProps']>; + InputProps: { + ref: React.Ref; + className: string; + startAdornment: React.ReactNode; + endAdornment: React.ReactNode; + }; + inputProps: ReturnType['getInputProps']>; +} + +export interface AutocompletePropsSizeOverrides {} + +export interface AutocompleteProps< + T, + Multiple extends boolean | undefined, + DisableClearable extends boolean | undefined, + FreeSolo extends boolean | undefined, +> extends UseAutocompleteProps { + /** + * The icon to display in place of the default clear icon. + * @default + */ + clearIcon?: React.ReactNode; + /** + * Override the default text for the *clear* icon button. + * + * For localization purposes, you can use the provided [translations](/material-ui/guides/localization/). + * @default 'Clear' + */ + clearText?: string; + /** + * Override the default text for the *close popup* icon button. + * + * For localization purposes, you can use the provided [translations](/material-ui/guides/localization/). + * @default 'Close' + */ + closeText?: string; + /** + * The props used for each slot inside. + * @default {} + */ + componentsProps?: { + // clearIndicator?: Partial; + // paper?: PaperProps; + // popper?: Partial; + // popupIndicator?: Partial; + }; + /** + * If `true`, the component is disabled. + * @default false + */ + disabled?: boolean; + /** + * If `true`, the `Popper` content will be under the DOM hierarchy of the parent component. + * @default false + */ + disablePortal?: boolean; + /** + * Force the visibility display of the popup icon. + * @default 'auto' + */ + forcePopupIcon?: true | false | 'auto'; + /** + * If `true`, the input will take up the full width of its container. + * @default false + */ + fullWidth?: boolean; + /** + * The label to display when the tags are truncated (`limitTags`). + * + * @param {number} more The number of truncated tags. + * @returns {ReactNode} + * @default (more) => `+${more}` + */ + getLimitTagsText?: (more: number) => React.ReactNode; + /** + * If `true`, the component is in a loading state. + * This shows the `loadingText` in place of suggestions (only if there are no suggestions to show, e.g. `options` are empty). + * @default false + */ + loading?: boolean; + /** + * Text to display when in a loading state. + * + * For localization purposes, you can use the provided [translations](/material-ui/guides/localization/). + * @default 'Loading…' + */ + loadingText?: React.ReactNode; + /** + * The maximum number of tags that will be visible when not focused. + * Set `-1` to disable the limit. + * @default -1 + */ + limitTags?: number; + /** + * Text to display when there are no options. + * + * For localization purposes, you can use the provided [translations](/material-ui/guides/localization/). + * @default 'No options' + */ + noOptionsText?: React.ReactNode; + /** + * Override the default text for the *open popup* icon button. + * + * For localization purposes, you can use the provided [translations](/material-ui/guides/localization/). + * @default 'Open' + */ + openText?: string; + /** + * The icon to display in place of the default popup icon. + * @default + */ + popupIcon?: React.ReactNode; + /** + * If `true`, the component becomes readonly. It is also supported for multiple tags where the tag cannot be deleted. + * @default false + */ + readOnly?: boolean; + /** + * Render the group. + * + * @param {AutocompleteRenderGroupParams} params The group to render. + * @returns {ReactNode} + */ + renderGroup?: (params: AutocompleteRenderGroupParams) => React.ReactNode; + /** + * Render the input. + * + * @param {object} params + * @returns {ReactNode} + */ + renderInput: (params: AutocompleteRenderInputParams) => React.ReactNode; + /** + * Render the option, use `getOptionLabel` by default. + * + * @param {object} props The props to apply on the li element. + * @param {T} option The option to render. + * @param {object} state The state of the component. + * @returns {ReactNode} + */ + renderOption?: ( + props: React.HTMLAttributes, + option: T, + state: AutocompleteRenderOptionState, + ) => React.ReactNode; + /** + * Render the selected value. + * + * @param {T[]} value The `value` provided to the component. + * @param {function} getTagProps A tag props getter. + * @param {object} ownerState The state of the Autocomplete component. + * @returns {ReactNode} + */ + renderTags?: ( + value: T[], + getTagProps: AutocompleteRenderGetTagProps, + ownerState: AutocompleteOwnerState, + ) => React.ReactNode; + /** + * The size of the component. + * @default 'medium' + */ + size?: OverridableStringUnion<'small' | 'medium', AutocompletePropsSizeOverrides>; + /** + * The system prop that allows defining system overrides as well as additional CSS styles. + */ + sx?: SxProps; +} diff --git a/packages/mui-joy/src/Autocomplete/autocompleteClasses.ts b/packages/mui-joy/src/Autocomplete/autocompleteClasses.ts new file mode 100644 index 00000000000000..d73bafc2e1de58 --- /dev/null +++ b/packages/mui-joy/src/Autocomplete/autocompleteClasses.ts @@ -0,0 +1,90 @@ +import { generateUtilityClass, generateUtilityClasses } from '../className'; + +export interface AutocompleteClasses { + /** Styles applied to the root element. */ + root: string; + /** Styles applied to the root element if `fullWidth={true}`. */ + fullWidth: string; + /** State class applied to the root element if focused. */ + focused: string; + /** State class applied to the ButtonBase root element if the button is keyboard focused. */ + focusVisible: string; + /** Styles applied to the tag elements, e.g. the chips. */ + tag: string; + /** Styles applied to the tag elements, e.g. the chips if `size="small"`. */ + tagSizeSmall: string; + /** Styles applied to the tag elements, e.g. the chips if `size="medium"`. */ + tagSizeMedium: string; + /** Styles applied when the popup icon is rendered. */ + hasPopupIcon: string; + /** Styles applied when the clear icon is rendered. */ + hasClearIcon: string; + /** Styles applied to the Input element. */ + inputRoot: string; + /** Styles applied to the input element. */ + input: string; + /** Styles applied to the input element if the input is focused. */ + inputFocused: string; + /** Styles applied to the endAdornment element. */ + endAdornment: string; + /** Styles applied to the clear indicator. */ + clearIndicator: string; + /** Styles applied to the popup indicator. */ + popupIndicator: string; + /** Styles applied to the popup indicator if the popup is open. */ + popupIndicatorOpen: string; + /** Styles applied to the popper element. */ + popper: string; + /** Styles applied to the popper element if `disablePortal={true}`. */ + popperDisablePortal: string; + /** Styles applied to the Paper component. */ + paper: string; + /** Styles applied to the listbox component. */ + listbox: string; + /** Styles applied to the loading wrapper. */ + loading: string; + /** Styles applied to the no option wrapper. */ + noOptions: string; + /** Styles applied to the option elements. */ + option: string; + /** Styles applied to the group's label elements. */ + groupLabel: string; + /** Styles applied to the group's ul elements. */ + groupUl: string; +} + +export type AutocompleteClassKey = keyof AutocompleteClasses; + +export function getAutocompleteUtilityClass(slot: string): string { + return generateUtilityClass('JoyAutocomplete', slot); +} + +const autocompleteClasses: AutocompleteClasses = generateUtilityClasses('JoyAutocomplete', [ + 'root', + 'fullWidth', + 'focused', + 'focusVisible', + 'tag', + 'tagSizeSmall', + 'tagSizeMedium', + 'hasPopupIcon', + 'hasClearIcon', + 'inputRoot', + 'input', + 'inputFocused', + 'endAdornment', + 'clearIndicator', + 'popupIndicator', + 'popupIndicatorOpen', + 'popper', + 'popperDisablePortal', + 'paper', + 'listbox', + 'loading', + 'noOptions', + 'option', + 'groupLabel', + 'groupUl', +]); + +export default autocompleteClasses; diff --git a/packages/mui-joy/src/Autocomplete/index.ts b/packages/mui-joy/src/Autocomplete/index.ts new file mode 100644 index 00000000000000..8e59e3978e0683 --- /dev/null +++ b/packages/mui-joy/src/Autocomplete/index.ts @@ -0,0 +1,4 @@ +export { default } from './Autocomplete'; +export * from './autocompleteClasses'; +export { default as autocompleteClasses } from './autocompleteClasses'; +export * from './AutocompleteProps'; diff --git a/packages/mui-joy/src/IconButton/IconButton.tsx b/packages/mui-joy/src/IconButton/IconButton.tsx index 723aaf3a69cb37..68ebfbc7b7cd6c 100644 --- a/packages/mui-joy/src/IconButton/IconButton.tsx +++ b/packages/mui-joy/src/IconButton/IconButton.tsx @@ -31,7 +31,7 @@ const useUtilityClasses = (ownerState: IconButtonOwnerState) => { return composedClasses; }; -const IconButtonRoot = styled('button', { +export const IconButtonRoot = styled('button', { name: 'JoyIconButton', slot: 'Root', overridesResolver: (props, styles) => styles.root, diff --git a/packages/mui-joy/src/ListItemButton/ListItemButton.tsx b/packages/mui-joy/src/ListItemButton/ListItemButton.tsx index 5cc4df776293bc..f1befaadbeabdf 100644 --- a/packages/mui-joy/src/ListItemButton/ListItemButton.tsx +++ b/packages/mui-joy/src/ListItemButton/ListItemButton.tsx @@ -72,7 +72,7 @@ export const ListItemButtonRoot = styled('div', { minBlockSize: 'var(--List-item-minHeight)', border: 'none', borderRadius: 'var(--List-item-radius)', - flex: ownerState.row ? 'none' : 1, + flex: ownerState.row ? 'none' : '1 0 0%', minInlineSize: 0, // TODO: discuss the transition approach in a separate PR. This value is copied from mui-material Button. transition: diff --git a/packages/mui-joy/src/internal/svg-icons/ArrowDropDown.tsx b/packages/mui-joy/src/internal/svg-icons/ArrowDropDown.tsx new file mode 100644 index 00000000000000..2c6a702fc7ccf8 --- /dev/null +++ b/packages/mui-joy/src/internal/svg-icons/ArrowDropDown.tsx @@ -0,0 +1,7 @@ +import * as React from 'react'; +import createSvgIcon from '../../utils/createSvgIcon'; + +/** + * @ignore - internal component. + */ +export default createSvgIcon(, 'ArrowDropDown'); From df7f7499d7b233238c39d42ef77083379102382d Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Thu, 1 Sep 2022 14:33:56 +0700 Subject: [PATCH 002/192] fix Select component slot --- packages/mui-joy/src/Select/Select.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/mui-joy/src/Select/Select.tsx b/packages/mui-joy/src/Select/Select.tsx index b2db93d19e6962..13902aae5a569d 100644 --- a/packages/mui-joy/src/Select/Select.tsx +++ b/packages/mui-joy/src/Select/Select.tsx @@ -453,14 +453,19 @@ const Select = React.forwardRef(function Select( const listboxProps = useSlotProps({ elementType: SelectListbox, getSlotProps: getListboxProps, - externalSlotProps: componentsProps.listbox, + externalSlotProps: { + ...componentsProps.listbox, + // TODO: find a better way + // @ts-expect-error + as: componentsProps.listbox?.component, + component: SelectListbox, + }, additionalProps: { ref: listboxRef, anchorEl, disablePortal: true, open: listboxOpen, placement: 'bottom' as const, - component: SelectListbox, modifiers: cachedModifiers, }, ownerState: { From b394bf2183288e8f5c5feb693038dba247475b75 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Thu, 1 Sep 2022 14:45:08 +0700 Subject: [PATCH 003/192] add flex none to prevent children shrink --- packages/mui-joy/src/ListItem/ListItem.tsx | 1 + packages/mui-joy/src/ListItemButton/ListItemButton.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/mui-joy/src/ListItem/ListItem.tsx b/packages/mui-joy/src/ListItem/ListItem.tsx index a6369bdbb8fed7..d63800e55e1eb4 100644 --- a/packages/mui-joy/src/ListItem/ListItem.tsx +++ b/packages/mui-joy/src/ListItem/ListItem.tsx @@ -72,6 +72,7 @@ const ListItemRoot = styled('li', { boxSizing: 'border-box', borderRadius: 'var(--List-item-radius)', display: 'flex', + flex: 'none', // prevent children from shrinking when the List's height is limited. position: 'relative', paddingBlockStart: ownerState.nested ? 0 : 'var(--List-item-paddingY)', paddingBlockEnd: ownerState.nested ? 0 : 'var(--List-item-paddingY)', diff --git a/packages/mui-joy/src/ListItemButton/ListItemButton.tsx b/packages/mui-joy/src/ListItemButton/ListItemButton.tsx index f1befaadbeabdf..ab33d7aa1a57d1 100644 --- a/packages/mui-joy/src/ListItemButton/ListItemButton.tsx +++ b/packages/mui-joy/src/ListItemButton/ListItemButton.tsx @@ -72,7 +72,7 @@ export const ListItemButtonRoot = styled('div', { minBlockSize: 'var(--List-item-minHeight)', border: 'none', borderRadius: 'var(--List-item-radius)', - flex: ownerState.row ? 'none' : '1 0 0%', + flex: 'none', // prevent children from shrinking. minInlineSize: 0, // TODO: discuss the transition approach in a separate PR. This value is copied from mui-material Button. transition: From 5988f3a3e41bd12db7a8268c135a00efd65c1c56 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Thu, 1 Sep 2022 14:45:23 +0700 Subject: [PATCH 004/192] add internal outline-inside --- packages/mui-joy/src/Menu/Menu.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/mui-joy/src/Menu/Menu.tsx b/packages/mui-joy/src/Menu/Menu.tsx index 965af4ce4790c7..0392803eb1d6c7 100644 --- a/packages/mui-joy/src/Menu/Menu.tsx +++ b/packages/mui-joy/src/Menu/Menu.tsx @@ -34,6 +34,7 @@ const MenuRoot = styled(ListRoot, { })<{ ownerState: MenuOwnerState }>(({ theme, ownerState }) => { const variantStyle = theme.variants[ownerState.variant!]?.[ownerState.color!]; return { + '--_outline-inside': '1', // to prevent the focus outline from being cut by overflow '--List-radius': theme.vars.radius.sm, '--List-item-stickyBackground': variantStyle?.backgroundColor || From d4a35c0dc236eeeb9b6b9aacc620ac942c909bbb Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Thu, 1 Sep 2022 14:45:50 +0700 Subject: [PATCH 005/192] add focus thickness variable --- .../src/styles/CssVarsProvider.test.tsx | 32 ++++++++++++++++--- .../mui-joy/src/styles/CssVarsProvider.tsx | 3 +- packages/mui-joy/src/styles/extendTheme.ts | 5 +-- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/packages/mui-joy/src/styles/CssVarsProvider.test.tsx b/packages/mui-joy/src/styles/CssVarsProvider.test.tsx index b824207a92813e..04a5b7bd8d55b5 100644 --- a/packages/mui-joy/src/styles/CssVarsProvider.test.tsx +++ b/packages/mui-joy/src/styles/CssVarsProvider.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { expect } from 'chai'; import { createRenderer, screen } from 'test/utils'; -import { CssVarsProvider, useTheme } from '@mui/joy/styles'; +import { CssVarsProvider, useTheme, shouldSkipGeneratingVar } from '@mui/joy/styles'; describe('[Joy] CssVarsProvider', () => { let originalMatchmedia: typeof window.matchMedia; @@ -29,6 +29,26 @@ describe('[Joy] CssVarsProvider', () => { window.matchMedia = originalMatchmedia; }); + describe('shouldSkipGeneratingVar', () => { + it('skip typography', () => { + expect(shouldSkipGeneratingVar(['typography'])).to.equal(true); + }); + + it('skip variants', () => { + expect(shouldSkipGeneratingVar(['variants'])).to.equal(true); + }); + + it('skip breakpoints', () => { + expect(shouldSkipGeneratingVar(['breakpoints'])).to.equal(true); + }); + + it('skip focus', () => { + expect(shouldSkipGeneratingVar(['focus'])).to.equal(true); + expect(shouldSkipGeneratingVar(['focus', 'selector'])).to.equal(true); + expect(shouldSkipGeneratingVar(['focus', 'thickness'])).to.equal(false); + }); + }); + describe('All CSS vars', () => { it('palette', () => { const Vars = () => { @@ -475,7 +495,7 @@ describe('[Joy] CssVarsProvider', () => { , ); - expect(container.firstChild?.textContent).to.equal('selector,default'); + expect(container.firstChild?.textContent).to.equal('thickness,selector,default'); }); }); @@ -605,11 +625,11 @@ describe('[Joy] CssVarsProvider', () => { expect(container.firstChild?.textContent).not.to.equal('typography'); }); - it('should not contain `focus` in theme.vars', () => { + it('should contain only `focus.thickness` in theme.vars', () => { const Consumer = () => { const theme = useTheme(); // @ts-expect-error - return
{theme.vars.focus ? 'focus' : ''}
; + return
{JSON.stringify(theme.vars.focus)}
; }; const { container } = render( @@ -618,7 +638,9 @@ describe('[Joy] CssVarsProvider', () => { , ); - expect(container.firstChild?.textContent).not.to.equal('focus'); + expect(container.firstChild?.textContent).not.to.equal( + JSON.stringify({ focus: { thickness: '4px' } }), + ); }); }); }); diff --git a/packages/mui-joy/src/styles/CssVarsProvider.tsx b/packages/mui-joy/src/styles/CssVarsProvider.tsx index 48d405cd6bbc41..55c61211c4e093 100644 --- a/packages/mui-joy/src/styles/CssVarsProvider.tsx +++ b/packages/mui-joy/src/styles/CssVarsProvider.tsx @@ -3,7 +3,8 @@ import extendTheme from './extendTheme'; import type { DefaultColorScheme, ExtendedColorScheme } from './types'; const shouldSkipGeneratingVar = (keys: string[]) => - !!keys[0].match(/(typography|variants|focus|breakpoints)/); + !!keys[0].match(/(typography|variants|breakpoints)/) || + (keys[0] === 'focus' && keys[1] !== 'thickness'); const { CssVarsProvider, useColorScheme, getInitColorSchemeScript } = createCssVarsProvider< DefaultColorScheme | ExtendedColorScheme diff --git a/packages/mui-joy/src/styles/extendTheme.ts b/packages/mui-joy/src/styles/extendTheme.ts index ae59ff82a9dc13..15dc5fe5bc343b 100644 --- a/packages/mui-joy/src/styles/extendTheme.ts +++ b/packages/mui-joy/src/styles/extendTheme.ts @@ -367,10 +367,11 @@ export default function extendTheme(themeOptions?: CssVarsThemeOptions): Theme { xl3: 900, }, focus: { + thickness: '4px', selector: `&.${generateUtilityClass('', 'focusVisible')}, &:focus-visible`, default: { - outlineOffset: getCssVar('focus-outlineOffset', '0px'), // reset user agent stylesheet - outline: `4px solid ${getCssVar('palette-focusVisible')}`, + outlineOffset: `calc(${getCssVar('focus-thickness')} * -1 * var(--_outline-inside, 0))`, // reset user agent stylesheet + outline: `${getCssVar('focus-thickness')} solid ${getCssVar('palette-focusVisible')}`, }, }, lineHeight: { From d361523e0b188be21766dc77e95ef5a8a4fc0517 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Thu, 1 Sep 2022 19:26:35 +0700 Subject: [PATCH 006/192] temp --- .../AutocompleteUnstyled/useAutocomplete.d.ts | 5 + .../AutocompleteUnstyled/useAutocomplete.js | 11 +- .../mui-joy/src/Autocomplete/Autocomplete.tsx | 392 +++++++----------- .../src/Autocomplete/autocompleteClasses.ts | 3 - packages/mui-joy/src/Input/Input.tsx | 5 +- 5 files changed, 156 insertions(+), 260 deletions(-) diff --git a/packages/mui-base/src/AutocompleteUnstyled/useAutocomplete.d.ts b/packages/mui-base/src/AutocompleteUnstyled/useAutocomplete.d.ts index b00651b4c13e33..0f73f1cba68c94 100644 --- a/packages/mui-base/src/AutocompleteUnstyled/useAutocomplete.d.ts +++ b/packages/mui-base/src/AutocompleteUnstyled/useAutocomplete.d.ts @@ -87,6 +87,11 @@ export interface UseAutocompleteProps< * The component name that is using this hook. Used for warnings. */ componentName?: string; + /** + * The prefix of the state class name + * @default 'Mui' + */ + classNamePrefix?: string; /** * The default value. Use when the component is not controlled. * @default props.multiple ? [] : null diff --git a/packages/mui-base/src/AutocompleteUnstyled/useAutocomplete.js b/packages/mui-base/src/AutocompleteUnstyled/useAutocomplete.js index fefcf1174add6e..765428ea60917c 100644 --- a/packages/mui-base/src/AutocompleteUnstyled/useAutocomplete.js +++ b/packages/mui-base/src/AutocompleteUnstyled/useAutocomplete.js @@ -75,6 +75,7 @@ export default function useAutocomplete(props) { clearOnBlur = !props.freeSolo, clearOnEscape = false, componentName = 'useAutocomplete', + classNamePrefix = 'Mui', defaultValue = props.multiple ? [] : null, disableClearable = false, disableCloseOnSelect = false, @@ -326,10 +327,10 @@ export default function useAutocomplete(props) { return; } - const prev = listboxRef.current.querySelector('[role="option"].Mui-focused'); + const prev = listboxRef.current.querySelector(`[role="option"].${classNamePrefix}-focused`); if (prev) { - prev.classList.remove('Mui-focused'); - prev.classList.remove('Mui-focusVisible'); + prev.classList.remove(`${classNamePrefix}-focused`); + prev.classList.remove(`${classNamePrefix}-focusVisible`); } const listboxNode = listboxRef.current.parentElement.querySelector('[role="listbox"]'); @@ -350,9 +351,9 @@ export default function useAutocomplete(props) { return; } - option.classList.add('Mui-focused'); + option.classList.add(`${classNamePrefix}-focused`); if (reason === 'keyboard') { - option.classList.add('Mui-focusVisible'); + option.classList.add(`${classNamePrefix}-focusVisible`); } // Scroll active descendant into view. diff --git a/packages/mui-joy/src/Autocomplete/Autocomplete.tsx b/packages/mui-joy/src/Autocomplete/Autocomplete.tsx index bc0880fd6f3b74..f933724fac6967 100644 --- a/packages/mui-joy/src/Autocomplete/Autocomplete.tsx +++ b/packages/mui-joy/src/Autocomplete/Autocomplete.tsx @@ -11,9 +11,11 @@ import ClearIcon from '../internal/svg-icons/Close'; import ArrowDropDownIcon from '../internal/svg-icons/ArrowDropDown'; import styled from '../styles/styled'; import Chip from '../Chip'; +import ChipDelete from '../ChipDelete'; import { IconButtonRoot } from '../IconButton/IconButton'; import ListProvider, { scopedVariables } from '../List/ListProvider'; -import { ListRoot } from '../List/List'; +import ListItem from '../ListItem'; +import List, { ListRoot } from '../List/List'; import { ListItemButtonRoot } from '../ListItemButton/ListItemButton'; import autocompleteClasses, { getAutocompleteUtilityClass } from './autocompleteClasses'; @@ -61,157 +63,66 @@ const AutocompleteRoot = styled('div', { slot: 'Root', overridesResolver: (props, styles) => styles.root, })(({ ownerState }) => ({ - [`&.${autocompleteClasses.focused} .${autocompleteClasses.clearIndicator}`]: { - visibility: 'visible', - }, + ...(ownerState.fullWidth && { + width: '100%', + }), /* Avoid double tap issue on iOS */ '@media (pointer: fine)': { [`&:hover .${autocompleteClasses.clearIndicator}`]: { visibility: 'visible', }, }, - ...(ownerState.fullWidth && { - width: '100%', - }), - [`& .${autocompleteClasses.tag}`]: { - margin: 3, - maxWidth: 'calc(100% - 6px)', - ...(ownerState.size === 'small' && { - margin: 2, - maxWidth: 'calc(100% - 4px)', - }), - }, - [`& .${autocompleteClasses.inputRoot}`]: { - flexWrap: 'wrap', - [`.${autocompleteClasses.hasPopupIcon}&, .${autocompleteClasses.hasClearIcon}&`]: { - paddingRight: 26 + 4, - }, - [`.${autocompleteClasses.hasPopupIcon}.${autocompleteClasses.hasClearIcon}&`]: { - paddingRight: 52 + 4, - }, - [`& .${autocompleteClasses.input}`]: { - width: 0, - minWidth: 30, - }, - }, - // [`& .${inputClasses.root}`]: { - // paddingBottom: 1, - // '& .MuiInput-input': { - // padding: '4px 4px 4px 0px', - // }, - // }, - // [`& .${inputClasses.root}.${inputBaseClasses.sizeSmall}`]: { - // [`& .${inputClasses.input}`]: { - // padding: '2px 4px 3px 0', - // }, - // }, - // [`& .${outlinedInputClasses.root}`]: { - // padding: 9, - // [`.${autocompleteClasses.hasPopupIcon}&, .${autocompleteClasses.hasClearIcon}&`]: { - // paddingRight: 26 + 4 + 9, - // }, - // [`.${autocompleteClasses.hasPopupIcon}.${autocompleteClasses.hasClearIcon}&`]: { - // paddingRight: 52 + 4 + 9, - // }, - // [`& .${autocompleteClasses.input}`]: { - // padding: '7.5px 4px 7.5px 6px', - // }, - // [`& .${autocompleteClasses.endAdornment}`]: { - // right: 9, - // }, - // }, - // [`& .${outlinedInputClasses.root}.${inputBaseClasses.sizeSmall}`]: { - // // Don't specify paddingRight, as it overrides the default value set when there is only - // // one of the popup or clear icon as the specificity is equal so the latter one wins - // paddingTop: 6, - // paddingBottom: 6, - // paddingLeft: 6, - // [`& .${autocompleteClasses.input}`]: { - // padding: '2.5px 4px 2.5px 6px', - // }, - // }, - // [`& .${filledInputClasses.root}`]: { - // paddingTop: 19, - // paddingLeft: 8, - // [`.${autocompleteClasses.hasPopupIcon}&, .${autocompleteClasses.hasClearIcon}&`]: { - // paddingRight: 26 + 4 + 9, - // }, - // [`.${autocompleteClasses.hasPopupIcon}.${autocompleteClasses.hasClearIcon}&`]: { - // paddingRight: 52 + 4 + 9, - // }, - // [`& .${filledInputClasses.input}`]: { - // padding: '7px 4px', - // }, - // [`& .${autocompleteClasses.endAdornment}`]: { - // right: 9, - // }, - // }, - // [`& .${filledInputClasses.root}.${inputBaseClasses.sizeSmall}`]: { - // paddingBottom: 1, - // [`& .${filledInputClasses.input}`]: { - // padding: '2.5px 4px', - // }, - // }, - // [`& .${inputBaseClasses.hiddenLabel}`]: { - // paddingTop: 8, - // }, [`& .${autocompleteClasses.input}`]: { - flexGrow: 1, - textOverflow: 'ellipsis', - opacity: 0, - ...(ownerState.inputFocused && { - opacity: 1, - }), + minWidth: 30, + }, + [`& .${autocompleteClasses}`]: { + minWidth: 30, }, })); -const AutocompleteEndAdornment = styled('div', { - name: 'JoyAutocomplete', - slot: 'EndAdornment', - overridesResolver: (props, styles) => styles.endAdornment, -})({ - // We use a position absolute to support wrapping tags. - position: 'absolute', - right: 0, - top: 'calc(50% - 14px)', // Center vertically -}); - const AutocompleteClearIndicator = styled(IconButtonRoot, { name: 'JoyAutocomplete', slot: 'ClearIndicator', overridesResolver: (props, styles) => styles.clearIndicator, -})({ - marginRight: -2, - padding: 4, - visibility: 'hidden', -}); +})(({ ownerState }) => ({ + marginInlineEnd: 0, // prevent the automatic adjustment between Input and IconButtonRoot + visibility: ownerState.focused ? 'visible' : 'hidden', +})); const AutocompletePopupIndicator = styled(IconButtonRoot, { name: 'JoyAutocomplete', slot: 'PopupIndicator', overridesResolver: (props, styles) => styles.popupIndicator, })(({ ownerState }) => ({ - padding: 2, - marginRight: -2, ...(ownerState.popupOpen && { transform: 'rotate(180deg)', }), })); -const AutocompletePopper = styled(ListRoot, { +const AutocompleteListbox = styled(ListRoot, { name: 'JoyAutocomplete', - slot: 'Popper', - overridesResolver: (props, styles) => styles.popper, -})(({ theme, ownerState }) => ({ - '--List-radius': theme.vars.radius.sm, - '--List-item-stickyBackground': theme.vars.palette.background.surface, // for sticky List - '--List-item-stickyTop': 'calc(var(--List-padding, var(--List-divider-gap)) * -1)', // negative amount of the List's padding block - zIndex: 1200, - ...scopedVariables, - boxShadow: theme.vars.shadow.md, - overflow: 'auto', - maxHeight: '40vh', -})); + slot: 'Listbox', + overridesResolver: (props, styles) => styles.listbox, +})(({ theme, ownerState }) => { + const variantStyle = theme.variants[ownerState.variant!]?.[ownerState.color!]; + return { + '--_outline-inside': '1', // to prevent the focus outline from being cut by overflow + '--List-radius': theme.vars.radius.sm, + '--List-item-stickyBackground': + variantStyle?.backgroundColor || + variantStyle?.background || + theme.vars.palette.background.surface, + '--List-item-stickyTop': 'calc(var(--List-padding, var(--List-divider-gap)) * -1)', + ...scopedVariables, + boxShadow: theme.vars.shadow.md, + ...(!variantStyle?.backgroundColor && { + backgroundColor: theme.vars.palette.background.surface, + }), + zIndex: 1200, + overflow: 'auto', + maxHeight: '40vh', + }; +}); const AutocompleteLoading = styled('div', { name: 'JoyAutocomplete', @@ -231,82 +142,95 @@ const AutocompleteNoOptions = styled('div', { padding: '14px 16px', })); -const AutocompleteListbox = styled('div', { +const AutocompleteOption = styled(ListItemButtonRoot, { name: 'JoyAutocomplete', - slot: 'Listbox', - overridesResolver: (props, styles) => styles.listbox, -})(({ theme }) => ({ - listStyle: 'none', - margin: 0, - padding: '8px 0', - maxHeight: '40vh', - overflow: 'auto', - // [`& .${autocompleteClasses.option}`]: { - // minHeight: 48, - // display: 'flex', - // overflow: 'hidden', - // justifyContent: 'flex-start', - // alignItems: 'center', - // cursor: 'pointer', - // paddingTop: 6, - // boxSizing: 'border-box', - // outline: '0', - // WebkitTapHighlightColor: 'transparent', - // paddingBottom: 6, - // paddingLeft: 16, - // paddingRight: 16, - // [theme.breakpoints.up('sm')]: { - // minHeight: 'auto', - // }, - // [`&.${autocompleteClasses.focused}`]: { - // // backgroundColor: (theme.vars || theme).palette.action.hover, - // // Reset on touch devices, it doesn't add specificity - // '@media (hover: none)': { - // backgroundColor: 'transparent', - // }, - // }, - // '&[aria-disabled="true"]': { - // // opacity: (theme.vars || theme).palette.action.disabledOpacity, - // pointerEvents: 'none', - // }, - // [`&.${autocompleteClasses.focusVisible}`]: { - // // backgroundColor: (theme.vars || theme).palette.action.focus, - // }, - // // '&[aria-selected="true"]': { - // // backgroundColor: theme.vars - // // ? `rgba(${theme.vars.palette.primary.mainChannel} / ${theme.vars.palette.action.selectedOpacity})` - // // : alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity), - // // [`&.${autocompleteClasses.focused}`]: { - // // backgroundColor: theme.vars - // // ? `rgba(${theme.vars.palette.primary.mainChannel} / calc(${theme.vars.palette.action.selectedOpacity} + ${theme.vars.palette.action.hoverOpacity}))` - // // : alpha( - // // theme.palette.primary.main, - // // theme.palette.action.selectedOpacity + theme.palette.action.hoverOpacity, - // // ), - // // // Reset on touch devices, it doesn't add specificity - // // '@media (hover: none)': { - // // backgroundColor: (theme.vars || theme).palette.action.selected, - // // }, - // // }, - // // [`&.${autocompleteClasses.focusVisible}`]: { - // // backgroundColor: theme.vars - // // ? `rgba(${theme.vars.palette.primary.mainChannel} / calc(${theme.vars.palette.action.selectedOpacity} + ${theme.vars.palette.action.focusOpacity}))` - // // : alpha( - // // theme.palette.primary.main, - // // theme.palette.action.selectedOpacity + theme.palette.action.focusOpacity, - // // ), - // // }, - // // }, - // }, + slot: 'Option', + overridesResolver: (props, styles) => styles.option, +})(({ theme, ownerState }) => ({ + '&[aria-disabled="true"]': theme.variants[`${ownerState.variant!}Disabled`]?.[ownerState.color!], + '&[aria-selected="true"]': { + ...theme.variants[`${ownerState.variant!}Active`]?.[ownerState.color!], + fontWeight: theme.vars.fontWeight.md, + }, })); -const AutocompleteGroupLabel = styled('li', { +// const AutocompleteListbox = styled('div', { +// name: 'JoyAutocomplete', +// slot: 'Listbox', +// overridesResolver: (props, styles) => styles.listbox, +// })(({ theme }) => ({ +// listStyle: 'none', +// margin: 0, +// padding: '8px 0', +// maxHeight: '40vh', +// overflow: 'auto', +// [`& .${autocompleteClasses.option}`]: { +// minHeight: 48, +// display: 'flex', +// overflow: 'hidden', +// justifyContent: 'flex-start', +// alignItems: 'center', +// cursor: 'pointer', +// paddingTop: 6, +// boxSizing: 'border-box', +// outline: '0', +// WebkitTapHighlightColor: 'transparent', +// paddingBottom: 6, +// paddingLeft: 16, +// paddingRight: 16, +// [theme.breakpoints.up('sm')]: { +// minHeight: 'auto', +// }, +// [`&.${autocompleteClasses.focused}`]: { +// // backgroundColor: (theme.vars || theme).palette.action.hover, +// // Reset on touch devices, it doesn't add specificity +// '@media (hover: none)': { +// backgroundColor: 'transparent', +// }, +// }, +// '&[aria-disabled="true"]': { +// // opacity: (theme.vars || theme).palette.action.disabledOpacity, +// pointerEvents: 'none', +// }, +// [`&.${autocompleteClasses.focusVisible}`]: { +// // backgroundColor: (theme.vars || theme).palette.action.focus, +// }, +// // '&[aria-selected="true"]': { +// // backgroundColor: theme.vars +// // ? `rgba(${theme.vars.palette.primary.mainChannel} / ${theme.vars.palette.action.selectedOpacity})` +// // : alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity), +// // [`&.${autocompleteClasses.focused}`]: { +// // backgroundColor: theme.vars +// // ? `rgba(${theme.vars.palette.primary.mainChannel} / calc(${theme.vars.palette.action.selectedOpacity} + ${theme.vars.palette.action.hoverOpacity}))` +// // : alpha( +// // theme.palette.primary.main, +// // theme.palette.action.selectedOpacity + theme.palette.action.hoverOpacity, +// // ), +// // // Reset on touch devices, it doesn't add specificity +// // '@media (hover: none)': { +// // backgroundColor: (theme.vars || theme).palette.action.selected, +// // }, +// // }, +// // [`&.${autocompleteClasses.focusVisible}`]: { +// // backgroundColor: theme.vars +// // ? `rgba(${theme.vars.palette.primary.mainChannel} / calc(${theme.vars.palette.action.selectedOpacity} + ${theme.vars.palette.action.focusOpacity}))` +// // : alpha( +// // theme.palette.primary.main, +// // theme.palette.action.selectedOpacity + theme.palette.action.focusOpacity, +// // ), +// // }, +// // }, +// }, +// })); + +const AutocompleteGroupLabel = styled(ListItem, { name: 'JoyAutocomplete', slot: 'GroupLabel', overridesResolver: (props, styles) => styles.groupLabel, })(({ theme }) => ({ - backgroundColor: theme.vars.palette.background.surface, - top: -8, + color: theme.vars.palette.text.secondary, + fontSize: theme.vars.fontSize.sm, + letterSpacing: theme.vars.letterSpacing.md, })); const AutocompleteGroupUl = styled('ul', { @@ -331,7 +255,6 @@ const Autocomplete = React.forwardRef(function Autocomplete(inProps, ref) { autoHighlight = false, autoSelect = false, blurOnSelect = false, - ChipProps, className, clearIcon = , clearOnBlur = !props.freeSolo, @@ -361,8 +284,6 @@ const Autocomplete = React.forwardRef(function Autocomplete(inProps, ref) { includeInputInList = false, inputValue: inputValueProp, limitTags = -1, - ListboxComponent = 'ul', - ListboxProps, loading = false, loadingText = 'Loading…', multiple = false, @@ -407,7 +328,7 @@ const Autocomplete = React.forwardRef(function Autocomplete(inProps, ref) { setAnchorEl, inputValue, groupedOptions, - } = useAutocomplete({ ...props, componentName: 'Autocomplete' }); + } = useAutocomplete({ ...props, componentName: 'Autocomplete', classNamePrefix: 'Joy' }); const hasClearIcon = !disableClearable && !disabled && dirty && !readOnly; const hasPopupIcon = (!freeSolo || forcePopupIcon === true) && forcePopupIcon !== false; @@ -439,11 +360,14 @@ const Autocomplete = React.forwardRef(function Autocomplete(inProps, ref) { if (renderTags) { startDecorator = renderTags(value, getCustomizedTagProps, ownerState); } else { - startDecorator = value.map((option, index) => ( - - {getOptionLabel(option)} - - )); + startDecorator = value.map((option, index) => { + const { onDelete, ...tagProps } = getCustomizedTagProps({ index }); + return ( + }> + {getOptionLabel(option)} + + ); + }); } } @@ -460,33 +384,21 @@ const Autocomplete = React.forwardRef(function Autocomplete(inProps, ref) { } const defaultRenderGroup = (params) => ( - - - {params.group} - - - {params.children} - - + + {params.group} + {params.children} + ); const renderGroup = renderGroupProp || defaultRenderGroup; const defaultRenderOption = (props2, option) => ( - {getOptionLabel(option)} - + ); const renderOption = renderOptionProp || defaultRenderOption; @@ -511,7 +423,7 @@ const Autocomplete = React.forwardRef(function Autocomplete(inProps, ref) { id, disabled, fullWidth: true, - size: size === 'sm' ? 'sm' : undefined, + size, InputLabelProps: getInputLabelProps(), InputProps: { ref: setAnchorEl, @@ -519,13 +431,13 @@ const Autocomplete = React.forwardRef(function Autocomplete(inProps, ref) { startDecorator, ...((hasClearIcon || hasPopupIcon) && { endDecorator: ( - + {hasClearIcon ? ( ) : null} - + ), }), }, @@ -566,12 +478,12 @@ const Autocomplete = React.forwardRef(function Autocomplete(inProps, ref) { {anchorEl ? ( 0 ? ( - - {groupedOptions.map((option, index) => { - if (groupBy) { - return renderGroup({ - key: option.key, - group: option.group, - children: option.options.map((option2, index2) => - renderListOption(option2, option.index + index2), - ), - }); - } - return renderListOption(option, index); - })} - - ) : null} */} ) : null} diff --git a/packages/mui-joy/src/Autocomplete/autocompleteClasses.ts b/packages/mui-joy/src/Autocomplete/autocompleteClasses.ts index d73bafc2e1de58..0b0286622f5218 100644 --- a/packages/mui-joy/src/Autocomplete/autocompleteClasses.ts +++ b/packages/mui-joy/src/Autocomplete/autocompleteClasses.ts @@ -25,8 +25,6 @@ export interface AutocompleteClasses { input: string; /** Styles applied to the input element if the input is focused. */ inputFocused: string; - /** Styles applied to the endAdornment element. */ - endAdornment: string; /** Styles applied to the clear indicator. */ clearIndicator: string; /** Styles applied to the popup indicator. */ @@ -72,7 +70,6 @@ const autocompleteClasses: AutocompleteClasses = generateUtilityClasses('JoyAuto 'inputRoot', 'input', 'inputFocused', - 'endAdornment', 'clearIndicator', 'popupIndicator', 'popupIndicatorOpen', diff --git a/packages/mui-joy/src/Input/Input.tsx b/packages/mui-joy/src/Input/Input.tsx index 0a5079e4bdd8cc..c5114e2257ff2a 100644 --- a/packages/mui-joy/src/Input/Input.tsx +++ b/packages/mui-joy/src/Input/Input.tsx @@ -169,9 +169,11 @@ const InputStartDecorator = styled('span', { '--Button-margin': '0 0 0 calc(var(--Input-decorator-childOffset) * -1)', '--IconButton-margin': '0 0 0 calc(var(--Input-decorator-childOffset) * -1)', '--Icon-margin': '0 0 0 calc(var(--Input-paddingInline) / -4)', - pointerEvents: 'none', // to make the input focused when click on the element because start element usually is an icon + cursor: 'initial', display: 'inherit', alignItems: 'center', + paddingBlock: 'var(--internal-paddingBlock)', // for wrapping Autocomplete's tags + flexWrap: 'wrap', // for wrapping Autocomplete's tags marginInlineEnd: 'var(--Input-gap)', color: theme.vars.palette.text.tertiary, ...(ownerState.focused && { @@ -187,6 +189,7 @@ const InputEndDecorator = styled('span', { '--Button-margin': '0 calc(var(--Input-decorator-childOffset) * -1) 0 0', '--IconButton-margin': '0 calc(var(--Input-decorator-childOffset) * -1) 0 0', '--Icon-margin': '0 calc(var(--Input-paddingInline) / -4) 0 0', + cursor: 'initial', display: 'inherit', alignItems: 'center', marginInlineStart: 'var(--Input-gap)', From 1e198a0817dbae5067395e462985bb80ae433f60 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Thu, 1 Sep 2022 20:31:58 +0700 Subject: [PATCH 007/192] add types --- .../AutocompleteUnstyled/useAutocomplete.d.ts | 2 +- .../mui-joy/src/Autocomplete/Autocomplete.tsx | 65 ++++++++++++++----- .../src/Autocomplete/AutocompleteProps.ts | 5 +- 3 files changed, 52 insertions(+), 20 deletions(-) diff --git a/packages/mui-base/src/AutocompleteUnstyled/useAutocomplete.d.ts b/packages/mui-base/src/AutocompleteUnstyled/useAutocomplete.d.ts index 0f73f1cba68c94..18797e45662cec 100644 --- a/packages/mui-base/src/AutocompleteUnstyled/useAutocomplete.d.ts +++ b/packages/mui-base/src/AutocompleteUnstyled/useAutocomplete.d.ts @@ -323,7 +323,7 @@ export default function useAutocomplete< >( props: UseAutocompleteProps, ): { - getRootProps: () => React.HTMLAttributes; + getRootProps: (externalProps?: any) => React.HTMLAttributes; getInputProps: () => React.InputHTMLAttributes; // We pass `getInputLabelProps()` to `@mui/material/InputLabel` which does not implement HTMLLabelElement#color. getInputLabelProps: () => Omit, 'color'>; diff --git a/packages/mui-joy/src/Autocomplete/Autocomplete.tsx b/packages/mui-joy/src/Autocomplete/Autocomplete.tsx index f933724fac6967..5115344f35fc05 100644 --- a/packages/mui-joy/src/Autocomplete/Autocomplete.tsx +++ b/packages/mui-joy/src/Autocomplete/Autocomplete.tsx @@ -4,7 +4,11 @@ import PropTypes from 'prop-types'; import { OverridableComponent } from '@mui/types'; import { unstable_capitalize as capitalize } from '@mui/utils'; import composeClasses from '@mui/base/composeClasses'; -import { useAutocomplete, createFilterOptions } from '@mui/base/AutocompleteUnstyled'; +import { + useAutocomplete, + createFilterOptions, + AutocompleteGroupedOption, +} from '@mui/base/AutocompleteUnstyled'; import PopperUnstyled from '@mui/base/PopperUnstyled'; import { useThemeProps } from '../styles'; import ClearIcon from '../internal/svg-icons/Close'; @@ -18,6 +22,15 @@ import ListItem from '../ListItem'; import List, { ListRoot } from '../List/List'; import { ListItemButtonRoot } from '../ListItemButton/ListItemButton'; import autocompleteClasses, { getAutocompleteUtilityClass } from './autocompleteClasses'; +import { + AutocompleteProps, + AutocompleteRenderGroupParams, + AutocompleteRenderGetTagProps, +} from './AutocompleteProps'; + +const defaultGetOptionLabel = (option: T) => + (option as { label: string }).label ?? option; +const defaultLimitTagsText = (more: string | number) => `+${more}`; const useUtilityClasses = (ownerState) => { const { @@ -244,12 +257,16 @@ const AutocompleteGroupUl = styled('ul', { }, }); -const Autocomplete = React.forwardRef(function Autocomplete(inProps, ref) { +const Autocomplete = React.forwardRef(function Autocomplete( + inProps, + ref: React.ForwardedRef, +) { const props = useThemeProps({ props: inProps, name: 'JoyAutocomplete', }); + /* eslint-disable @typescript-eslint/no-unused-vars */ const { autoComplete = false, autoHighlight = false, @@ -274,9 +291,9 @@ const Autocomplete = React.forwardRef(function Autocomplete(inProps, ref) { forcePopupIcon = 'auto', freeSolo = false, fullWidth = false, - getLimitTagsText = (more) => `+${more}`, + getLimitTagsText = defaultLimitTagsText, getOptionDisabled, - getOptionLabel = (option) => option.label ?? option, + getOptionLabel = defaultGetOptionLabel, isOptionEqualToValue, groupBy, handleHomeEndKeys = !props.freeSolo, @@ -350,17 +367,17 @@ const Autocomplete = React.forwardRef(function Autocomplete(inProps, ref) { let startDecorator; - if (multiple && value.length > 0) { - const getCustomizedTagProps = (params) => ({ + if (multiple && (value as Array).length > 0) { + const getCustomizedTagProps: AutocompleteRenderGetTagProps = (params) => ({ className: classes.tag, disabled, ...getTagProps(params), }); if (renderTags) { - startDecorator = renderTags(value, getCustomizedTagProps, ownerState); + startDecorator = renderTags(value as Array, getCustomizedTagProps, ownerState); } else { - startDecorator = value.map((option, index) => { + startDecorator = (value as Array).map((option, index) => { const { onDelete, ...tagProps } = getCustomizedTagProps({ index }); return ( }> @@ -383,7 +400,7 @@ const Autocomplete = React.forwardRef(function Autocomplete(inProps, ref) { } } - const defaultRenderGroup = (params) => ( + const defaultRenderGroup = (params: AutocompleteRenderGroupParams) => ( {params.group} {params.children} @@ -391,7 +408,7 @@ const Autocomplete = React.forwardRef(function Autocomplete(inProps, ref) { ); const renderGroup = renderGroupProp || defaultRenderGroup; - const defaultRenderOption = (props2, option) => ( + const defaultRenderOption = (props2: React.HTMLAttributes, option: unknown) => ( { + const renderListOption = (option: unknown, index: number) => { const optionProps = getOptionProps({ option, index }); return renderOption({ ...optionProps, className: classes.option }, option, { - selected: optionProps['aria-selected'], + // `aria-selected` prop will always by boolean, see useAutocomplete hook. + selected: !!optionProps['aria-selected'], inputValue, }); }; @@ -511,11 +529,12 @@ const Autocomplete = React.forwardRef(function Autocomplete(inProps, ref) { ) : null} {groupedOptions.map((option, index) => { if (groupBy) { + const typedOption = option as AutocompleteGroupedOption; return renderGroup({ - key: option.key, - group: option.group, - children: option.options.map((option2, index2) => - renderListOption(option2, option.index + index2), + key: String(typedOption.key), + group: typedOption.group, + children: typedOption.options.map((option2, index2) => + renderListOption(option2, typedOption.index + index2), ), }); } @@ -526,7 +545,19 @@ const Autocomplete = React.forwardRef(function Autocomplete(inProps, ref) { ) : null} ); -}); +}) as AutocompleteComponent; + +interface AutocompleteComponent { + < + T, + Multiple extends boolean | undefined = undefined, + DisableClearable extends boolean | undefined = undefined, + FreeSolo extends boolean | undefined = undefined, + >( + props: AutocompleteProps, + ): JSX.Element; + propTypes?: any; +} Autocomplete.propTypes /* remove-proptypes */ = { // ----------------------------- Warning -------------------------------- diff --git a/packages/mui-joy/src/Autocomplete/AutocompleteProps.ts b/packages/mui-joy/src/Autocomplete/AutocompleteProps.ts index 942b934d337dfc..8e9b116ffe689e 100644 --- a/packages/mui-joy/src/Autocomplete/AutocompleteProps.ts +++ b/packages/mui-joy/src/Autocomplete/AutocompleteProps.ts @@ -78,6 +78,7 @@ export interface AutocompleteProps< DisableClearable extends boolean | undefined, FreeSolo extends boolean | undefined, > extends UseAutocompleteProps { + className: string; /** * The icon to display in place of the default clear icon. * @default @@ -220,9 +221,9 @@ export interface AutocompleteProps< ) => React.ReactNode; /** * The size of the component. - * @default 'medium' + * @default 'md' */ - size?: OverridableStringUnion<'small' | 'medium', AutocompletePropsSizeOverrides>; + size?: OverridableStringUnion<'sm' | 'md' | 'lg', AutocompletePropsSizeOverrides>; /** * The system prop that allows defining system overrides as well as additional CSS styles. */ From 864a70859e8814e8fa8e13229d8fd0cb5ecae99e Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Thu, 1 Sep 2022 22:13:28 +0700 Subject: [PATCH 008/192] update styles --- .../mui-joy/src/Autocomplete/Autocomplete.tsx | 185 ++++++++---------- .../src/Autocomplete/AutocompleteProps.ts | 1 - packages/mui-joy/src/Chip/Chip.tsx | 6 +- packages/mui-joy/src/Input/Input.tsx | 7 +- 4 files changed, 86 insertions(+), 113 deletions(-) diff --git a/packages/mui-joy/src/Autocomplete/Autocomplete.tsx b/packages/mui-joy/src/Autocomplete/Autocomplete.tsx index 5115344f35fc05..d8d75d68aa4d1f 100644 --- a/packages/mui-joy/src/Autocomplete/Autocomplete.tsx +++ b/packages/mui-joy/src/Autocomplete/Autocomplete.tsx @@ -14,25 +14,35 @@ import { useThemeProps } from '../styles'; import ClearIcon from '../internal/svg-icons/Close'; import ArrowDropDownIcon from '../internal/svg-icons/ArrowDropDown'; import styled from '../styles/styled'; -import Chip from '../Chip'; +import Chip, { chipClasses } from '../Chip'; import ChipDelete from '../ChipDelete'; import { IconButtonRoot } from '../IconButton/IconButton'; import ListProvider, { scopedVariables } from '../List/ListProvider'; import ListItem from '../ListItem'; import List, { ListRoot } from '../List/List'; +import { ListItemButtonOwnerState } from '../ListItemButton'; import { ListItemButtonRoot } from '../ListItemButton/ListItemButton'; +import { inputClasses } from '../Input'; import autocompleteClasses, { getAutocompleteUtilityClass } from './autocompleteClasses'; import { AutocompleteProps, AutocompleteRenderGroupParams, AutocompleteRenderGetTagProps, + AutocompleteOwnerState, } from './AutocompleteProps'; +type OwnerState = AutocompleteOwnerState< + unknown, + boolean | undefined, + boolean | undefined, + boolean | undefined +>; + const defaultGetOptionLabel = (option: T) => (option as { label: string }).label ?? option; const defaultLimitTagsText = (more: string | number) => `+${more}`; -const useUtilityClasses = (ownerState) => { +const useUtilityClasses = (ownerState: OwnerState) => { const { disablePortal, focused, @@ -54,7 +64,7 @@ const useUtilityClasses = (ownerState) => { ], inputRoot: ['inputRoot'], input: ['input', inputFocused && 'inputFocused'], - tag: ['tag', `tagSize${capitalize(size)}`], + tag: ['tag', size && `tagSize${capitalize(size)}`], endAdornment: ['endAdornment'], clearIndicator: ['clearIndicator'], popupIndicator: ['popupIndicator', popupOpen && 'popupIndicatorOpen'], @@ -75,29 +85,66 @@ const AutocompleteRoot = styled('div', { name: 'JoyAutocomplete', slot: 'Root', overridesResolver: (props, styles) => styles.root, -})(({ ownerState }) => ({ - ...(ownerState.fullWidth && { - width: '100%', - }), - /* Avoid double tap issue on iOS */ - '@media (pointer: fine)': { - [`&:hover .${autocompleteClasses.clearIndicator}`]: { - visibility: 'visible', +})<{ ownerState: OwnerState }>(({ ownerState }) => { + let endDecoratorCount = 0; + if (ownerState.hasClearIcon) { + endDecoratorCount += 1; + } + if (ownerState.hasPopupIcon) { + endDecoratorCount += 1; + } + return [ + { + ...(ownerState.fullWidth && { + width: '100%', + }), + /* Avoid double tap issue on iOS */ + '@media (pointer: fine)': { + [`&:hover .${autocompleteClasses.clearIndicator}`]: { + visibility: 'visible', + }, + }, + [`& .${autocompleteClasses.input}`]: { + minWidth: 30, + minHeight: 'calc(var(--Input-minHeight) - 2 * var(--variant-borderWidth, 0px))', + }, + [`& .${inputClasses.root}`]: { + paddingInlineEnd: `calc(${endDecoratorCount} * var(--Input-decorator-childHeight) + 2 * var(--_Input-paddingBlock))`, + }, + [`& .${inputClasses.endDecorator}`]: { + position: 'absolute', + top: '50%', + transform: 'translateY(-50%)', + right: 'var(--Input-paddingInline)', + }, }, - }, - [`& .${autocompleteClasses.input}`]: { - minWidth: 30, - }, - [`& .${autocompleteClasses}`]: { - minWidth: 30, - }, -})); + ownerState.multiple && { + [`& .${inputClasses.root}`]: { + flexWrap: 'wrap', + paddingInlineStart: 0, + paddingBlockEnd: 'var(--_Input-paddingBlock)', + }, + [`& .${inputClasses.startDecorator}`]: { + display: 'contents', + }, + [`& .${autocompleteClasses.input}`]: { + marginInlineStart: 'var(--Input-paddingInline)', + marginBlockEnd: 'calc(-1 * var(--_Input-paddingBlock))', + }, + [`& .${chipClasses.root}`]: { + // TODO: move to flexbox `gap` later. + marginInlineStart: 'var(--_Input-paddingBlock)', + marginBlockStart: 'var(--_Input-paddingBlock)', + }, + }, + ]; +}); const AutocompleteClearIndicator = styled(IconButtonRoot, { name: 'JoyAutocomplete', slot: 'ClearIndicator', overridesResolver: (props, styles) => styles.clearIndicator, -})(({ ownerState }) => ({ +})<{ ownerState: OwnerState }>(({ ownerState }) => ({ marginInlineEnd: 0, // prevent the automatic adjustment between Input and IconButtonRoot visibility: ownerState.focused ? 'visible' : 'hidden', })); @@ -106,7 +153,7 @@ const AutocompletePopupIndicator = styled(IconButtonRoot, { name: 'JoyAutocomplete', slot: 'PopupIndicator', overridesResolver: (props, styles) => styles.popupIndicator, -})(({ ownerState }) => ({ +})<{ ownerState: OwnerState }>(({ ownerState }) => ({ ...(ownerState.popupOpen && { transform: 'rotate(180deg)', }), @@ -116,7 +163,7 @@ const AutocompleteListbox = styled(ListRoot, { name: 'JoyAutocomplete', slot: 'Listbox', overridesResolver: (props, styles) => styles.listbox, -})(({ theme, ownerState }) => { +})<{ ownerState: OwnerState }>(({ theme, ownerState }) => { const variantStyle = theme.variants[ownerState.variant!]?.[ownerState.color!]; return { '--_outline-inside': '1', // to prevent the focus outline from being cut by overflow @@ -141,7 +188,7 @@ const AutocompleteLoading = styled('div', { name: 'JoyAutocomplete', slot: 'Loading', overridesResolver: (props, styles) => styles.loading, -})(({ theme }) => ({ +})<{ ownerState: OwnerState }>(({ theme }) => ({ color: (theme.vars || theme).palette.text.secondary, padding: '14px 16px', })); @@ -150,7 +197,7 @@ const AutocompleteNoOptions = styled('div', { name: 'JoyAutocomplete', slot: 'NoOptions', overridesResolver: (props, styles) => styles.noOptions, -})(({ theme }) => ({ +})<{ ownerState: OwnerState }>(({ theme }) => ({ color: (theme.vars || theme).palette.text.secondary, padding: '14px 16px', })); @@ -159,7 +206,7 @@ const AutocompleteOption = styled(ListItemButtonRoot, { name: 'JoyAutocomplete', slot: 'Option', overridesResolver: (props, styles) => styles.option, -})(({ theme, ownerState }) => ({ +})<{ ownerState: ListItemButtonOwnerState }>(({ theme, ownerState }) => ({ '&[aria-disabled="true"]': theme.variants[`${ownerState.variant!}Disabled`]?.[ownerState.color!], '&[aria-selected="true"]': { ...theme.variants[`${ownerState.variant!}Active`]?.[ownerState.color!], @@ -167,75 +214,6 @@ const AutocompleteOption = styled(ListItemButtonRoot, { }, })); -// const AutocompleteListbox = styled('div', { -// name: 'JoyAutocomplete', -// slot: 'Listbox', -// overridesResolver: (props, styles) => styles.listbox, -// })(({ theme }) => ({ -// listStyle: 'none', -// margin: 0, -// padding: '8px 0', -// maxHeight: '40vh', -// overflow: 'auto', -// [`& .${autocompleteClasses.option}`]: { -// minHeight: 48, -// display: 'flex', -// overflow: 'hidden', -// justifyContent: 'flex-start', -// alignItems: 'center', -// cursor: 'pointer', -// paddingTop: 6, -// boxSizing: 'border-box', -// outline: '0', -// WebkitTapHighlightColor: 'transparent', -// paddingBottom: 6, -// paddingLeft: 16, -// paddingRight: 16, -// [theme.breakpoints.up('sm')]: { -// minHeight: 'auto', -// }, -// [`&.${autocompleteClasses.focused}`]: { -// // backgroundColor: (theme.vars || theme).palette.action.hover, -// // Reset on touch devices, it doesn't add specificity -// '@media (hover: none)': { -// backgroundColor: 'transparent', -// }, -// }, -// '&[aria-disabled="true"]': { -// // opacity: (theme.vars || theme).palette.action.disabledOpacity, -// pointerEvents: 'none', -// }, -// [`&.${autocompleteClasses.focusVisible}`]: { -// // backgroundColor: (theme.vars || theme).palette.action.focus, -// }, -// // '&[aria-selected="true"]': { -// // backgroundColor: theme.vars -// // ? `rgba(${theme.vars.palette.primary.mainChannel} / ${theme.vars.palette.action.selectedOpacity})` -// // : alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity), -// // [`&.${autocompleteClasses.focused}`]: { -// // backgroundColor: theme.vars -// // ? `rgba(${theme.vars.palette.primary.mainChannel} / calc(${theme.vars.palette.action.selectedOpacity} + ${theme.vars.palette.action.hoverOpacity}))` -// // : alpha( -// // theme.palette.primary.main, -// // theme.palette.action.selectedOpacity + theme.palette.action.hoverOpacity, -// // ), -// // // Reset on touch devices, it doesn't add specificity -// // '@media (hover: none)': { -// // backgroundColor: (theme.vars || theme).palette.action.selected, -// // }, -// // }, -// // [`&.${autocompleteClasses.focusVisible}`]: { -// // backgroundColor: theme.vars -// // ? `rgba(${theme.vars.palette.primary.mainChannel} / calc(${theme.vars.palette.action.selectedOpacity} + ${theme.vars.palette.action.focusOpacity}))` -// // : alpha( -// // theme.palette.primary.main, -// // theme.palette.action.selectedOpacity + theme.palette.action.focusOpacity, -// // ), -// // }, -// // }, -// }, -// })); - const AutocompleteGroupLabel = styled(ListItem, { name: 'JoyAutocomplete', slot: 'GroupLabel', @@ -246,17 +224,6 @@ const AutocompleteGroupLabel = styled(ListItem, { letterSpacing: theme.vars.letterSpacing.md, })); -const AutocompleteGroupUl = styled('ul', { - name: 'JoyAutocomplete', - slot: 'GroupUl', - overridesResolver: (props, styles) => styles.groupUl, -})({ - padding: 0, - [`& .${autocompleteClasses.option}`]: { - paddingLeft: 24, - }, -}); - const Autocomplete = React.forwardRef(function Autocomplete( inProps, ref: React.ForwardedRef, @@ -380,7 +347,13 @@ const Autocomplete = React.forwardRef(function Autocomplete( startDecorator = (value as Array).map((option, index) => { const { onDelete, ...tagProps } = getCustomizedTagProps({ index }); return ( - }> + } + > {getOptionLabel(option)} ); diff --git a/packages/mui-joy/src/Autocomplete/AutocompleteProps.ts b/packages/mui-joy/src/Autocomplete/AutocompleteProps.ts index 8e9b116ffe689e..dfe3f5fec4260b 100644 --- a/packages/mui-joy/src/Autocomplete/AutocompleteProps.ts +++ b/packages/mui-joy/src/Autocomplete/AutocompleteProps.ts @@ -32,7 +32,6 @@ export type AutocompleteOwnerState< hasPopupIcon: boolean; inputFocused: boolean; popupOpen: boolean; - size: OverridableStringUnion<'small' | 'medium', AutocompletePropsSizeOverrides>; }; export type AutocompleteRenderGetTagProps = ({ index }: { index: number }) => { diff --git a/packages/mui-joy/src/Chip/Chip.tsx b/packages/mui-joy/src/Chip/Chip.tsx index f49f5cb1acb220..fc53543dc4089d 100644 --- a/packages/mui-joy/src/Chip/Chip.tsx +++ b/packages/mui-joy/src/Chip/Chip.tsx @@ -58,7 +58,7 @@ const ChipRoot = styled('div', { '--Chip-paddingInline': '0.5rem', '--Chip-decorator-childHeight': 'calc(min(1.5rem, var(--Chip-minHeight)) - 2 * var(--variant-borderWidth))', - '--Icon-fontSize': '0.875rem', + '--Icon-fontSize': 'calc(var(--Chip-minHeight, 1.5rem) / 1.714)', // 0.875rem by default '--Chip-minHeight': '1.5rem', fontSize: theme.vars.fontSize.xs, }), @@ -66,7 +66,7 @@ const ChipRoot = styled('div', { '--Chip-gap': '0.375rem', '--Chip-paddingInline': '0.75rem', '--Chip-decorator-childHeight': 'min(1.5rem, var(--Chip-minHeight))', - '--Icon-fontSize': '1.125rem', + '--Icon-fontSize': 'calc(var(--Chip-minHeight, 2rem) / 1.778)', // 1.125rem by default '--Chip-minHeight': '2rem', fontSize: theme.vars.fontSize.sm, }), @@ -74,7 +74,7 @@ const ChipRoot = styled('div', { '--Chip-gap': '0.5rem', '--Chip-paddingInline': '1rem', '--Chip-decorator-childHeight': 'min(2rem, var(--Chip-minHeight))', - '--Icon-fontSize': '1.25rem', + '--Icon-fontSize': 'calc(var(--Chip-minHeight, 2.5rem) / 2)', // 1.25rem by default '--Chip-minHeight': '2.5rem', fontSize: theme.vars.fontSize.md, }), diff --git a/packages/mui-joy/src/Input/Input.tsx b/packages/mui-joy/src/Input/Input.tsx index c5114e2257ff2a..728aac2a164ff2 100644 --- a/packages/mui-joy/src/Input/Input.tsx +++ b/packages/mui-joy/src/Input/Input.tsx @@ -65,10 +65,10 @@ const InputRoot = styled('div', { // variables for controlling child components '--Input-decorator-childOffset': 'min(calc(var(--Input-paddingInline) - (var(--Input-minHeight) - 2 * var(--variant-borderWidth) - var(--Input-decorator-childHeight)) / 2), var(--Input-paddingInline))', - '--internal-paddingBlock': + '--_Input-paddingBlock': 'max((var(--Input-minHeight) - 2 * var(--variant-borderWidth) - var(--Input-decorator-childHeight)) / 2, 0px)', '--Input-decorator-childRadius': - 'max((var(--Input-radius) - var(--variant-borderWidth)) - var(--internal-paddingBlock), min(var(--internal-paddingBlock) / 2, (var(--Input-radius) - var(--variant-borderWidth)) / 2))', + 'max((var(--Input-radius) - var(--variant-borderWidth)) - var(--_Input-paddingBlock), min(var(--_Input-paddingBlock) / 2, (var(--Input-radius) - var(--variant-borderWidth)) / 2))', '--Button-minHeight': 'var(--Input-decorator-childHeight)', '--IconButton-size': 'var(--Input-decorator-childHeight)', '--Button-radius': 'var(--Input-decorator-childRadius)', @@ -151,6 +151,7 @@ const InputInput = styled('input', { fontStyle: 'inherit', fontWeight: 'inherit', lineHeight: 'inherit', + textOverflow: 'ellipsis', '&:-webkit-autofill': { WebkitBackgroundClip: 'text', // remove autofill background WebkitTextFillColor: theme.vars.palette[ownerState.color!]?.overrideTextPrimary, @@ -172,7 +173,7 @@ const InputStartDecorator = styled('span', { cursor: 'initial', display: 'inherit', alignItems: 'center', - paddingBlock: 'var(--internal-paddingBlock)', // for wrapping Autocomplete's tags + paddingBlock: 'var(--_Input-paddingBlock)', // for wrapping Autocomplete's tags flexWrap: 'wrap', // for wrapping Autocomplete's tags marginInlineEnd: 'var(--Input-gap)', color: theme.vars.palette.text.tertiary, From 2be4a821ff8623dc57541a716c1845b83167c81c Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Fri, 2 Sep 2022 09:30:06 +0700 Subject: [PATCH 009/192] update chip label --- packages/mui-joy/src/Chip/Chip.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/mui-joy/src/Chip/Chip.tsx b/packages/mui-joy/src/Chip/Chip.tsx index fc53543dc4089d..ce6a1b1f628378 100644 --- a/packages/mui-joy/src/Chip/Chip.tsx +++ b/packages/mui-joy/src/Chip/Chip.tsx @@ -119,9 +119,11 @@ const ChipLabel = styled('span', { slot: 'Label', overridesResolver: (props, styles) => styles.label, })<{ ownerState: ChipOwnerState }>(({ ownerState }) => ({ - display: 'inherit', - alignItems: 'center', + display: 'inline-block', + overflow: 'hidden', + textOverflow: 'ellipsis', order: 1, + minInlineSize: 0, flexGrow: 1, ...(ownerState.clickable && { zIndex: 1, From a8aa5b4d02a702fd4436735e13f05550e75b212b Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Sat, 3 Sep 2022 14:30:10 +0700 Subject: [PATCH 010/192] bind controls id --- .../mui-joy/src/FormControl/FormControl.tsx | 148 ++++++++++++++++++ .../src/FormControl/FormControlContext.ts | 22 +++ .../src/FormControl/FormControlProps.ts | 33 ++++ .../src/FormControl/formControlClasses.ts | 22 +++ packages/mui-joy/src/FormControl/index.ts | 4 + .../src/FormHelperText/FormHelperText.tsx | 18 ++- packages/mui-joy/src/FormLabel/FormLabel.tsx | 9 +- packages/mui-joy/src/Input/Input.tsx | 6 + 8 files changed, 257 insertions(+), 5 deletions(-) create mode 100644 packages/mui-joy/src/FormControl/FormControl.tsx create mode 100644 packages/mui-joy/src/FormControl/FormControlContext.ts create mode 100644 packages/mui-joy/src/FormControl/FormControlProps.ts create mode 100644 packages/mui-joy/src/FormControl/formControlClasses.ts create mode 100644 packages/mui-joy/src/FormControl/index.ts diff --git a/packages/mui-joy/src/FormControl/FormControl.tsx b/packages/mui-joy/src/FormControl/FormControl.tsx new file mode 100644 index 00000000000000..ce5d47fdae2d51 --- /dev/null +++ b/packages/mui-joy/src/FormControl/FormControl.tsx @@ -0,0 +1,148 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import clsx from 'clsx'; +import { OverridableComponent } from '@mui/types'; +import { unstable_useId as useId } from '@mui/utils'; +import composeClasses from '@mui/base/composeClasses'; +import { useThemeProps } from '../styles'; +import styled from '../styles/styled'; +import FormControlContext from './FormControlContext'; +import formControlClasses, { getFormControlUtilityClass } from './formControlClasses'; +import { FormControlProps, FormControlOwnerState, FormControlTypeMap } from './FormControlProps'; + +const useUtilityClasses = () => { + const slots = { + root: ['root'], + }; + + return composeClasses(slots, getFormControlUtilityClass, {}); +}; + +export const FormControlRoot = styled('div', { + name: 'JoyFormControl', + slot: 'Root', + overridesResolver: (props, styles) => styles.root, +})<{ ownerState: FormControlOwnerState }>(({ theme, ownerState }) => ({ + '--FormLabel-margin': '0 0 0.25rem 0', + '--FormHelperText-margin': '0.25rem 0 0 0', + '--FormLabel-asterisk-color': theme.vars.palette.danger[500], + '--FormHelperText-color': theme.vars.palette[ownerState.color!]?.[500], + ...(ownerState.size === 'sm' && { + '--FormHelperText-fontSize': theme.vars.fontSize.xs, + '--FormLabel-fontSize': theme.vars.fontSize.xs, + }), + [`&.${formControlClasses.error}`]: { + '--FormHelperText-color': theme.vars.palette.danger[500], + }, + [`&.${formControlClasses.disabled}`]: { + '--FormLabel-color': theme.vars.palette[ownerState.color || 'neutral']?.plainDisabledColor, + '--FormHelperText-color': theme.vars.palette[ownerState.color || 'neutral']?.plainDisabledColor, + }, + display: 'flex', + flexDirection: 'column', +})); + +const FormControl = React.forwardRef(function FormControl(inProps, ref) { + const props = useThemeProps({ + props: inProps, + name: 'JoyFormControl', + }); + + const { + id: idOverride, + className, + component = 'div', + disabled = false, + required = false, + error = false, + variant, + color = 'neutral', + size = 'md', + ...other + } = props; + + const id = useId(idOverride); + const helperTextId = `${id}-helper-text`; + const [helperText, setHelperText] = React.useState(null); + + const ownerState = { + ...props, + id, + component, + color, + size, + variant, + }; + + const childContext = { + disabled, + required, + error, + variant, + color, + size, + htmlFor: id, + 'aria-describedby': helperText ? helperTextId : undefined, + setHelperText, + }; + + const classes = useUtilityClasses(); + + return ( + + + + ); +}) as OverridableComponent; + +FormControl.propTypes /* remove-proptypes */ = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * The content of the component. + */ + children: PropTypes.node, + /** + * @ignore + */ + className: PropTypes.string, + /** + * The color of the component. It supports those theme colors that make sense for this component. + * @default 'neutral' + */ + color: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([ + PropTypes.oneOf(['danger', 'info', 'neutral', 'primary', 'success', 'warning']), + PropTypes.string, + ]), + /** + * The component used for the root node. + * Either a string to use a HTML element or a component. + */ + component: PropTypes.elementType, + /** + * The system prop that allows defining system overrides as well as additional CSS styles. + */ + sx: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), + PropTypes.func, + PropTypes.object, + ]), + /** + * The variant to use. + * @default 'plain' + */ + variant: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([ + PropTypes.oneOf(['outlined', 'plain', 'soft', 'solid']), + PropTypes.string, + ]), +} as any; + +export default FormControl; diff --git a/packages/mui-joy/src/FormControl/FormControlContext.ts b/packages/mui-joy/src/FormControl/FormControlContext.ts new file mode 100644 index 00000000000000..c34176d085d568 --- /dev/null +++ b/packages/mui-joy/src/FormControl/FormControlContext.ts @@ -0,0 +1,22 @@ +import * as React from 'react'; +import { FormControlProps } from './FormControlProps'; + +type RequiredExcept = { + [P in keyof T as P extends K ? never : P]-?: T[P]; +} & { + [P in K]: T[K]; +}; + +const FormControlContext = React.createContext< + | undefined + | (RequiredExcept< + Pick, + 'variant' + > & { + htmlFor: string | undefined; + 'aria-describedby': string | undefined; + setHelperText: (node: null | HTMLElement) => void; + }) +>(undefined); + +export default FormControlContext; diff --git a/packages/mui-joy/src/FormControl/FormControlProps.ts b/packages/mui-joy/src/FormControl/FormControlProps.ts new file mode 100644 index 00000000000000..5054ee886b99a1 --- /dev/null +++ b/packages/mui-joy/src/FormControl/FormControlProps.ts @@ -0,0 +1,33 @@ +import * as React from 'react'; +import { OverrideProps } from '@mui/types'; +import { InputProps } from '../Input/InputProps'; +import { SxProps } from '../styles/types'; + +export type FormControlSlot = 'root'; + +export interface FormControlPropsColorOverrides {} +export interface FormControlPropsVariantOverrides {} + +type InputRootKeys = 'disabled' | 'error' | 'required' | 'variant' | 'color' | 'size'; + +export interface FormControlTypeMap

{ + props: P & + Pick & { + /** + * The content of the component. + */ + children?: React.ReactNode; + /** + * The system prop that allows defining system overrides as well as additional CSS styles. + */ + sx?: SxProps; + }; + defaultComponent: D; +} + +export type FormControlProps< + D extends React.ElementType = FormControlTypeMap['defaultComponent'], + P = { component?: React.ElementType }, +> = OverrideProps, D>; + +export interface FormControlOwnerState extends FormControlProps {} diff --git a/packages/mui-joy/src/FormControl/formControlClasses.ts b/packages/mui-joy/src/FormControl/formControlClasses.ts new file mode 100644 index 00000000000000..6f84ae74080f5a --- /dev/null +++ b/packages/mui-joy/src/FormControl/formControlClasses.ts @@ -0,0 +1,22 @@ +import { generateUtilityClass, generateUtilityClasses } from '../className'; + +export interface FormControlClasses { + /** Styles applied to the root element. */ + root: string; + error: string; + disabled: string; +} + +export type FormControlClassKey = keyof FormControlClasses; + +export function getFormControlUtilityClass(slot: string): string { + return generateUtilityClass('JoyFormControl', slot); +} + +const formControlClasses: FormControlClasses = generateUtilityClasses('JoyFormControl', [ + 'root', + 'error', + 'disabled', +]); + +export default formControlClasses; diff --git a/packages/mui-joy/src/FormControl/index.ts b/packages/mui-joy/src/FormControl/index.ts new file mode 100644 index 00000000000000..0a506dbac76ac0 --- /dev/null +++ b/packages/mui-joy/src/FormControl/index.ts @@ -0,0 +1,4 @@ +export { default } from './FormControl'; +export * from './formControlClasses'; +export { default as formControlClasses } from './formControlClasses'; +export * from './FormControlProps'; diff --git a/packages/mui-joy/src/FormHelperText/FormHelperText.tsx b/packages/mui-joy/src/FormHelperText/FormHelperText.tsx index b9a603d2438d98..4cb6ab6fef38eb 100644 --- a/packages/mui-joy/src/FormHelperText/FormHelperText.tsx +++ b/packages/mui-joy/src/FormHelperText/FormHelperText.tsx @@ -1,11 +1,13 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { OverridableComponent } from '@mui/types'; +import { unstable_useForkRef as useForkRef } from '@mui/utils'; import composeClasses from '@mui/base/composeClasses'; import { useSlotProps } from '@mui/base/utils'; import { styled, useThemeProps } from '../styles'; import { FormHelperTextProps, FormHelperTextTypeMap } from './FormHelperTextProps'; import { getFormHelperTextUtilityClass } from './formHelperTextClasses'; +import FormControlContext from '../FormControl/FormControlContext'; const useUtilityClasses = () => { const slots = { @@ -26,7 +28,7 @@ const FormHelperTextRoot = styled('p', { fontSize: `var(--FormHelperText-fontSize, ${theme.vars.fontSize.sm})`, lineHeight: theme.vars.lineHeight.sm, color: `var(--FormHelperText-color, ${theme.vars.palette.text.secondary})`, - margin: 'var(--FormHelperText-margin, 0.25rem 0 0 0)', + margin: 'var(--FormHelperText-margin, 0px)', })); const FormHelperText = React.forwardRef(function FormHelperText(inProps, ref) { @@ -36,6 +38,17 @@ const FormHelperText = React.forwardRef(function FormHelperText(inProps, ref) { }); const { children, component, ...other } = props; + const rootRef = React.useRef(null); + const handleRef = useForkRef(rootRef, ref); + const formControl = React.useContext(FormControlContext); + const setHelperText = formControl?.setHelperText; + + React.useEffect(() => { + setHelperText?.(rootRef.current); + return () => { + setHelperText?.(null); + }; + }, [setHelperText]); const ownerState = { ...props, @@ -49,8 +62,9 @@ const FormHelperText = React.forwardRef(function FormHelperText(inProps, ref) { externalForwardedProps: other, ownerState, additionalProps: { - ref, + ref: handleRef, as: component, + id: formControl?.['aria-describedby'], }, className: classes.root, }); diff --git a/packages/mui-joy/src/FormLabel/FormLabel.tsx b/packages/mui-joy/src/FormLabel/FormLabel.tsx index 52c853758084b8..a48955d354ed0f 100644 --- a/packages/mui-joy/src/FormLabel/FormLabel.tsx +++ b/packages/mui-joy/src/FormLabel/FormLabel.tsx @@ -6,6 +6,7 @@ import { useSlotProps } from '@mui/base/utils'; import { styled, useThemeProps } from '../styles'; import { FormLabelProps, FormLabelTypeMap } from './FormLabelProps'; import { getFormLabelUtilityClass } from './formLabelClasses'; +import FormControlContext from '../FormControl/FormControlContext'; const useUtilityClasses = () => { const slots = { @@ -29,7 +30,7 @@ const FormLabelRoot = styled('label', { fontWeight: theme.vars.fontWeight.md, lineHeight: theme.vars.lineHeight.md, color: `var(--FormLabel-color, ${theme.vars.palette.text.primary})`, - margin: 'var(--FormLabel-margin, 0 0 0.25rem 0)', + margin: 'var(--FormLabel-margin, 0px)', })); const AsteriskComponent = styled('span', { @@ -46,7 +47,9 @@ const FormLabel = React.forwardRef(function FormLabel(inProps, ref) { name: 'JoyFormLabel', }); - const { children, component = 'label', componentsProps = {}, required = false, ...other } = props; + const { children, component = 'label', componentsProps = {}, ...other } = props; + const formControl = React.useContext(FormControlContext); + const required = inProps.required ?? formControl?.required ?? false; const ownerState = { ...props, @@ -78,7 +81,7 @@ const FormLabel = React.forwardRef(function FormLabel(inProps, ref) { }); return ( - + {children} {required &&  {'*'}} diff --git a/packages/mui-joy/src/Input/Input.tsx b/packages/mui-joy/src/Input/Input.tsx index eb74edd37e2903..821f3b8389ba26 100644 --- a/packages/mui-joy/src/Input/Input.tsx +++ b/packages/mui-joy/src/Input/Input.tsx @@ -8,6 +8,7 @@ import { styled, useThemeProps } from '../styles'; import { InputTypeMap, InputProps } from './InputProps'; import inputClasses, { getInputUtilityClass } from './inputClasses'; import useForwardedInput from './useForwardedInput'; +import FormControlContext from '../FormControl/FormControlContext'; const useUtilityClasses = (ownerState: InputProps) => { const { disabled, fullWidth, variant, color, size } = ownerState; @@ -219,6 +220,7 @@ const Input = React.forwardRef(function Input(inProps, ref) { endDecorator, ...other } = useForwardedInput(props, inputClasses); + const formControl = React.useContext(FormControlContext); const ownerState = { ...props, @@ -253,6 +255,10 @@ const Input = React.forwardRef(function Input(inProps, ref) { getInputProps({ ...otherHandlers, ...propsToForward }), externalSlotProps: componentsProps.input, ownerState, + additionalProps: { + id: formControl?.htmlFor, + 'aria-describedby': formControl?.['aria-describedby'], + }, className: [classes.input, inputStateClasses], }); From 44e606be6ab3eaff4b2d97d75788c4bb5ef0edd5 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Sat, 3 Sep 2022 14:51:00 +0700 Subject: [PATCH 011/192] adjust Input to use values from form control --- .../mui-joy/src/FormControl/FormControl.tsx | 12 ++++- .../src/FormControl/FormControlContext.ts | 11 +--- .../src/FormControl/FormControlProps.ts | 51 +++++++++++++------ packages/mui-joy/src/Input/Input.tsx | 17 ++++--- packages/mui-joy/src/Input/InputProps.ts | 5 -- 5 files changed, 55 insertions(+), 41 deletions(-) diff --git a/packages/mui-joy/src/FormControl/FormControl.tsx b/packages/mui-joy/src/FormControl/FormControl.tsx index ce5d47fdae2d51..b5be0b06c87c5e 100644 --- a/packages/mui-joy/src/FormControl/FormControl.tsx +++ b/packages/mui-joy/src/FormControl/FormControl.tsx @@ -28,8 +28,16 @@ export const FormControlRoot = styled('div', { '--FormLabel-asterisk-color': theme.vars.palette.danger[500], '--FormHelperText-color': theme.vars.palette[ownerState.color!]?.[500], ...(ownerState.size === 'sm' && { - '--FormHelperText-fontSize': theme.vars.fontSize.xs, '--FormLabel-fontSize': theme.vars.fontSize.xs, + '--FormHelperText-fontSize': theme.vars.fontSize.xs, + }), + ...(ownerState.size === 'md' && { + '--FormLabel-fontSize': theme.vars.fontSize.sm, + '--FormHelperText-fontSize': theme.vars.fontSize.sm, + }), + ...(ownerState.size === 'lg' && { + '--FormLabel-fontSize': theme.vars.fontSize.md, + '--FormHelperText-fontSize': theme.vars.fontSize.md, }), [`&.${formControlClasses.error}`]: { '--FormHelperText-color': theme.vars.palette.danger[500], @@ -55,7 +63,7 @@ const FormControl = React.forwardRef(function FormControl(inProps, ref) { disabled = false, required = false, error = false, - variant, + variant = 'outlined', color = 'neutral', size = 'md', ...other diff --git a/packages/mui-joy/src/FormControl/FormControlContext.ts b/packages/mui-joy/src/FormControl/FormControlContext.ts index c34176d085d568..850c2e02a75643 100644 --- a/packages/mui-joy/src/FormControl/FormControlContext.ts +++ b/packages/mui-joy/src/FormControl/FormControlContext.ts @@ -1,18 +1,9 @@ import * as React from 'react'; import { FormControlProps } from './FormControlProps'; -type RequiredExcept = { - [P in keyof T as P extends K ? never : P]-?: T[P]; -} & { - [P in K]: T[K]; -}; - const FormControlContext = React.createContext< | undefined - | (RequiredExcept< - Pick, - 'variant' - > & { + | (Pick & { htmlFor: string | undefined; 'aria-describedby': string | undefined; setHelperText: (node: null | HTMLElement) => void; diff --git a/packages/mui-joy/src/FormControl/FormControlProps.ts b/packages/mui-joy/src/FormControl/FormControlProps.ts index 5054ee886b99a1..b14d0f9bb1d400 100644 --- a/packages/mui-joy/src/FormControl/FormControlProps.ts +++ b/packages/mui-joy/src/FormControl/FormControlProps.ts @@ -1,27 +1,46 @@ import * as React from 'react'; -import { OverrideProps } from '@mui/types'; -import { InputProps } from '../Input/InputProps'; -import { SxProps } from '../styles/types'; +import { OverrideProps, OverridableStringUnion } from '@mui/types'; +import { ColorPaletteProp, VariantProp, SxProps } from '../styles/types'; export type FormControlSlot = 'root'; export interface FormControlPropsColorOverrides {} export interface FormControlPropsVariantOverrides {} - -type InputRootKeys = 'disabled' | 'error' | 'required' | 'variant' | 'color' | 'size'; +export interface FormControlPropsSizeOverrides {} export interface FormControlTypeMap

{ - props: P & - Pick & { - /** - * The content of the component. - */ - children?: React.ReactNode; - /** - * The system prop that allows defining system overrides as well as additional CSS styles. - */ - sx?: SxProps; - }; + props: P & { + /** + * The content of the component. + */ + children?: React.ReactNode; + /** + * The color of the component. It supports those theme colors that make sense for this component. + * @default 'neutral' + */ + color?: OverridableStringUnion; + disabled?: boolean; + /** + * If `true`, the children will indicate an error. + * @default false + */ + error?: boolean; + required?: boolean; + /** + * The size of the component. + * @default 'md' + */ + size?: OverridableStringUnion<'sm' | 'md' | 'lg', FormControlPropsSizeOverrides>; + /** + * The system prop that allows defining system overrides as well as additional CSS styles. + */ + sx?: SxProps; + /** + * The variant to use. + * @default 'outlined' + */ + variant?: OverridableStringUnion; + }; defaultComponent: D; } diff --git a/packages/mui-joy/src/Input/Input.tsx b/packages/mui-joy/src/Input/Input.tsx index 821f3b8389ba26..b2dee00fc4da9d 100644 --- a/packages/mui-joy/src/Input/Input.tsx +++ b/packages/mui-joy/src/Input/Input.tsx @@ -209,11 +209,10 @@ const Input = React.forwardRef(function Input(inProps, ref) { component, componentsProps = {}, focused, - formControlContext, - error: errorState, - disabled: disabledState, + error: errorProp = false, + disabled: disableProp = false, fullWidth = false, - size = 'md', + size: sizeProp = 'md', color = 'neutral', variant = 'outlined', startDecorator, @@ -221,15 +220,17 @@ const Input = React.forwardRef(function Input(inProps, ref) { ...other } = useForwardedInput(props, inputClasses); const formControl = React.useContext(FormControlContext); + const disabled = inProps.disabled ?? formControl?.disabled ?? disableProp; + const error = inProps.error ?? formControl?.error ?? errorProp; + const size = inProps.size ?? formControl?.size ?? sizeProp; const ownerState = { ...props, fullWidth, - color: errorState ? 'danger' : color, - disabled: disabledState, - error: errorState, + color: error ? 'danger' : color, + disabled, + error, focused, - formControlContext: formControlContext!, size, variant, }; diff --git a/packages/mui-joy/src/Input/InputProps.ts b/packages/mui-joy/src/Input/InputProps.ts index cf30e1cc870018..c4f3061fb2562d 100644 --- a/packages/mui-joy/src/Input/InputProps.ts +++ b/packages/mui-joy/src/Input/InputProps.ts @@ -1,7 +1,6 @@ import React from 'react'; import { OverridableStringUnion, OverrideProps } from '@mui/types'; import { SlotComponentProps } from '@mui/base/utils'; -import { FormControlUnstyledState } from '@mui/base/FormControlUnstyled'; import { ColorPaletteProp, VariantProp, SxProps } from '../styles/types'; export type InputSlot = 'root' | 'input' | 'startDecorator' | 'endDecorator'; @@ -105,8 +104,4 @@ export interface InputOwnerState extends InputProps { * If `true`, the input is focused. */ focused: boolean; - /** - * The data from the parent form control. - */ - formControlContext: FormControlUnstyledState | undefined; } From aeccd764137bcf6aa8aeea2ac3e6e74959c57e91 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Sat, 3 Sep 2022 15:31:12 +0700 Subject: [PATCH 012/192] add classes to form control --- .../mui-joy/src/FormControl/FormControl.tsx | 19 ++++++-- .../src/FormControl/formControlClasses.ts | 43 ++++++++++++++++++- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/packages/mui-joy/src/FormControl/FormControl.tsx b/packages/mui-joy/src/FormControl/FormControl.tsx index b5be0b06c87c5e..10d04cf308d7e8 100644 --- a/packages/mui-joy/src/FormControl/FormControl.tsx +++ b/packages/mui-joy/src/FormControl/FormControl.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; import { OverridableComponent } from '@mui/types'; -import { unstable_useId as useId } from '@mui/utils'; +import { unstable_useId as useId, unstable_capitalize as capitalize } from '@mui/utils'; import composeClasses from '@mui/base/composeClasses'; import { useThemeProps } from '../styles'; import styled from '../styles/styled'; @@ -10,9 +10,17 @@ import FormControlContext from './FormControlContext'; import formControlClasses, { getFormControlUtilityClass } from './formControlClasses'; import { FormControlProps, FormControlOwnerState, FormControlTypeMap } from './FormControlProps'; -const useUtilityClasses = () => { +const useUtilityClasses = (ownerState: FormControlOwnerState) => { + const { disabled, error, size, color, variant } = ownerState; const slots = { - root: ['root'], + root: [ + 'root', + disabled && 'disabled', + error && 'error', + variant && `variant${capitalize(variant)}`, + color && `color${capitalize(color)}`, + size && `size${capitalize(size)}`, + ], }; return composeClasses(slots, getFormControlUtilityClass, {}); @@ -78,6 +86,9 @@ const FormControl = React.forwardRef(function FormControl(inProps, ref) { id, component, color, + disabled, + error, + required, size, variant, }; @@ -94,7 +105,7 @@ const FormControl = React.forwardRef(function FormControl(inProps, ref) { setHelperText, }; - const classes = useUtilityClasses(); + const classes = useUtilityClasses(ownerState); return ( diff --git a/packages/mui-joy/src/FormControl/formControlClasses.ts b/packages/mui-joy/src/FormControl/formControlClasses.ts index 6f84ae74080f5a..eef5cce675f3d8 100644 --- a/packages/mui-joy/src/FormControl/formControlClasses.ts +++ b/packages/mui-joy/src/FormControl/formControlClasses.ts @@ -3,8 +3,36 @@ import { generateUtilityClass, generateUtilityClasses } from '../className'; export interface FormControlClasses { /** Styles applied to the root element. */ root: string; - error: string; + /** Styles applied to the root element if `disabled={true}`. */ disabled: string; + /** State class applied to the root element if `error={true}`. */ + error: string; + /** Styles applied to the root element if `color="primary"`. */ + colorPrimary: string; + /** Styles applied to the root element if `color="neutral"`. */ + colorNeutral: string; + /** Styles applied to the root element if `color="danger"`. */ + colorDanger: string; + /** Styles applied to the root element if `color="info"`. */ + colorInfo: string; + /** Styles applied to the root element if `color="success"`. */ + colorSuccess: string; + /** Styles applied to the root element if `color="warning"`. */ + colorWarning: string; + /** Styles applied to the root element if `size="sm"`. */ + sizeSm: string; + /** Styles applied to the root element if `size="md"`. */ + sizeMd: string; + /** Styles applied to the root element if `size="lg"`. */ + sizeLg: string; + /** Styles applied to the root element if `variant="plain"`. */ + variantPlain: string; + /** Styles applied to the root element if `variant="outlined"`. */ + variantOutlined: string; + /** Styles applied to the root element if `variant="soft"`. */ + variantSoft: string; + /** Styles applied to the root element if `variant="solid"`. */ + variantSolid: string; } export type FormControlClassKey = keyof FormControlClasses; @@ -17,6 +45,19 @@ const formControlClasses: FormControlClasses = generateUtilityClasses('JoyFormCo 'root', 'error', 'disabled', + 'colorPrimary', + 'colorNeutral', + 'colorDanger', + 'colorInfo', + 'colorSuccess', + 'colorWarning', + 'sizeSm', + 'sizeMd', + 'sizeLg', + 'variantPlain', + 'variantOutlined', + 'variantSoft', + 'variantSolid', ]); export default formControlClasses; From c231fcb94ea4870043887ad948a80fcf71fac2fe Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Sat, 3 Sep 2022 15:31:23 +0700 Subject: [PATCH 013/192] use context for input --- packages/mui-joy/src/Input/Input.tsx | 6 ++++-- packages/mui-joy/src/Input/inputClasses.ts | 3 +++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/mui-joy/src/Input/Input.tsx b/packages/mui-joy/src/Input/Input.tsx index b2dee00fc4da9d..9ac0062ac037f6 100644 --- a/packages/mui-joy/src/Input/Input.tsx +++ b/packages/mui-joy/src/Input/Input.tsx @@ -213,8 +213,8 @@ const Input = React.forwardRef(function Input(inProps, ref) { disabled: disableProp = false, fullWidth = false, size: sizeProp = 'md', - color = 'neutral', - variant = 'outlined', + color: colorProp = 'neutral', + variant: variantProp = 'outlined', startDecorator, endDecorator, ...other @@ -223,6 +223,8 @@ const Input = React.forwardRef(function Input(inProps, ref) { const disabled = inProps.disabled ?? formControl?.disabled ?? disableProp; const error = inProps.error ?? formControl?.error ?? errorProp; const size = inProps.size ?? formControl?.size ?? sizeProp; + const color = inProps.color ?? formControl?.color ?? colorProp; + const variant = inProps.variant ?? formControl?.variant ?? variantProp; const ownerState = { ...props, diff --git a/packages/mui-joy/src/Input/inputClasses.ts b/packages/mui-joy/src/Input/inputClasses.ts index 6f853d154ba8ee..8d11b6abbdcec8 100644 --- a/packages/mui-joy/src/Input/inputClasses.ts +++ b/packages/mui-joy/src/Input/inputClasses.ts @@ -37,6 +37,8 @@ export interface InputClasses { variantOutlined: string; /** Styles applied to the root element if `variant="soft"`. */ variantSoft: string; + /** Styles applied to the root element if `variant="solid"`. */ + variantSolid: string; /** Styles applied to the root element if `fullWidth={true}`. */ fullWidth: string; /** Styles applied to the startDecorator element */ @@ -72,6 +74,7 @@ const inputClasses: InputClasses = generateUtilityClasses('JoyInput', [ 'variantPlain', 'variantOutlined', 'variantSoft', + 'variantSolid', 'fullWidth', 'startDecorator', 'endDecorator', From ed7dcc11381926d093e35d96ccb12c94dbd0e545 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Sat, 3 Sep 2022 15:31:31 +0700 Subject: [PATCH 014/192] use context for Textarea --- packages/mui-joy/src/Textarea/Textarea.tsx | 28 +++++++++++++------ .../mui-joy/src/Textarea/TextareaProps.ts | 5 ---- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/packages/mui-joy/src/Textarea/Textarea.tsx b/packages/mui-joy/src/Textarea/Textarea.tsx index 140d98280bce17..a6e3c369d372c4 100644 --- a/packages/mui-joy/src/Textarea/Textarea.tsx +++ b/packages/mui-joy/src/Textarea/Textarea.tsx @@ -9,6 +9,7 @@ import { styled, useThemeProps } from '../styles'; import { TextareaTypeMap, TextareaProps, TextareaOwnerState } from './TextareaProps'; import textareaClasses, { getTextareaUtilityClass } from './textareaClasses'; import useForwardedInput from '../Input/useForwardedInput'; +import FormControlContext from '../FormControl/FormControlContext'; const useUtilityClasses = (ownerState: TextareaOwnerState) => { const { disabled, variant, color, size } = ownerState; @@ -207,25 +208,30 @@ const Textarea = React.forwardRef(function Textarea(inProps, ref) { componentsProps = {}, focused, formControlContext, - error: errorState, - disabled: disabledState, - size = 'md', - color = 'neutral', - variant = 'outlined', + error: errorProp = false, + disabled: disabledProp = false, + size: sizeProp = 'md', + color: colorProp = 'neutral', + variant: variantProp = 'outlined', startDecorator, endDecorator, minRows, maxRows, ...other } = useForwardedInput(props, textareaClasses); + const formControl = React.useContext(FormControlContext); + const disabled = inProps.disabled ?? formControl?.disabled ?? disabledProp; + const error = inProps.error ?? formControl?.error ?? errorProp; + const size = inProps.size ?? formControl?.size ?? sizeProp; + const color = inProps.color ?? formControl?.color ?? colorProp; + const variant = inProps.variant ?? formControl?.variant ?? variantProp; const ownerState = { ...props, - color: errorState ? 'danger' : color, - disabled: disabledState, - error: errorState, + color: error ? 'danger' : color, + disabled, + error, focused, - formControlContext: formControlContext!, size, variant, }; @@ -253,6 +259,10 @@ const Textarea = React.forwardRef(function Textarea(inProps, ref) { minRows, maxRows, }, + additionalProps: { + id: formControl?.htmlFor, + 'aria-describedby': formControl?.['aria-describedby'], + }, ownerState, className: [classes.textarea, inputStateClasses], }); diff --git a/packages/mui-joy/src/Textarea/TextareaProps.ts b/packages/mui-joy/src/Textarea/TextareaProps.ts index 8c87063c6cfbcb..b2cfc4be0d2efe 100644 --- a/packages/mui-joy/src/Textarea/TextareaProps.ts +++ b/packages/mui-joy/src/Textarea/TextareaProps.ts @@ -1,7 +1,6 @@ import React from 'react'; import { OverridableStringUnion, OverrideProps } from '@mui/types'; import { SlotComponentProps } from '@mui/base/utils'; -import { FormControlUnstyledState } from '@mui/base/FormControlUnstyled'; import { ColorPaletteProp, VariantProp, SxProps } from '../styles/types'; export type TextareaSlot = 'root' | 'textarea' | 'startDecorator' | 'endDecorator'; @@ -102,8 +101,4 @@ export interface TextareaOwnerState extends TextareaProps { * If `true`, the input is focused. */ focused: boolean; - /** - * The data from the parent form control. - */ - formControlContext: FormControlUnstyledState | undefined; } From 6d2826e08753440e94594cae43fe425c59399c8e Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Sat, 3 Sep 2022 16:16:45 +0700 Subject: [PATCH 015/192] fix color --- packages/mui-joy/src/Input/Input.tsx | 4 ++-- packages/mui-joy/src/Select/Select.tsx | 19 +++++++++++++------ packages/mui-joy/src/Textarea/Textarea.tsx | 4 ++-- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/mui-joy/src/Input/Input.tsx b/packages/mui-joy/src/Input/Input.tsx index 9ac0062ac037f6..eee27d860facb3 100644 --- a/packages/mui-joy/src/Input/Input.tsx +++ b/packages/mui-joy/src/Input/Input.tsx @@ -223,13 +223,13 @@ const Input = React.forwardRef(function Input(inProps, ref) { const disabled = inProps.disabled ?? formControl?.disabled ?? disableProp; const error = inProps.error ?? formControl?.error ?? errorProp; const size = inProps.size ?? formControl?.size ?? sizeProp; - const color = inProps.color ?? formControl?.color ?? colorProp; + const color = error ? 'danger' : inProps.color ?? formControl?.color ?? colorProp; const variant = inProps.variant ?? formControl?.variant ?? variantProp; const ownerState = { ...props, fullWidth, - color: error ? 'danger' : color, + color, disabled, error, focused, diff --git a/packages/mui-joy/src/Select/Select.tsx b/packages/mui-joy/src/Select/Select.tsx index f56d22a258e332..258c82c522d50b 100644 --- a/packages/mui-joy/src/Select/Select.tsx +++ b/packages/mui-joy/src/Select/Select.tsx @@ -23,6 +23,7 @@ import { styled, useThemeProps } from '../styles'; import { SelectOwnProps, SelectStaticProps, SelectOwnerState, SelectTypeMap } from './SelectProps'; import selectClasses, { getSelectUtilityClass } from './selectClasses'; import { ListOwnerState } from '../List'; +import FormControlContext from '../FormControl/FormControlContext'; function defaultRenderSingleValue(selectedOption: SelectOption | null) { return selectedOption?.label ?? ''; @@ -264,7 +265,7 @@ const Select = React.forwardRef(function Select( componentsProps = {}, defaultValue, defaultListboxOpen = false, - disabled: disabledProp, + disabled: disabledExternalProp, placeholder, listboxId, listboxOpen: listboxOpenProp, @@ -273,9 +274,9 @@ const Select = React.forwardRef(function Select( onClose, renderValue: renderValueProp, value: valueProp, - size = 'md', - variant = 'outlined', - color = 'neutral', + size: sizeProp = 'md', + variant: variantProp = 'outlined', + color: colorProp = 'neutral', startDecorator, endDecorator, indicator = , @@ -295,6 +296,12 @@ const Select = React.forwardRef(function Select( name?: string; }; + const formControl = React.useContext(FormControlContext); + const disabledProp = inProps.disabled ?? formControl?.disabled ?? disabledExternalProp; + const size = inProps.size ?? formControl?.size ?? sizeProp; + const color = formControl?.error ? 'danger' : inProps.color ?? formControl?.color ?? colorProp; + const variant = inProps.variant ?? formControl?.variant ?? variantProp; + const renderValue = renderValueProp ?? defaultRenderSingleValue; const [anchorEl, setAnchorEl] = React.useState(null); @@ -413,10 +420,10 @@ const Select = React.forwardRef(function Select( getSlotProps: getButtonProps, externalSlotProps: componentsProps.button, additionalProps: { - 'aria-describedby': ariaDescribedby, + 'aria-describedby': ariaDescribedby ?? formControl?.['aria-describedby'], 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, - id, + id: id ?? formControl?.htmlFor, name, }, ownerState, diff --git a/packages/mui-joy/src/Textarea/Textarea.tsx b/packages/mui-joy/src/Textarea/Textarea.tsx index a6e3c369d372c4..0d837ea46b7b52 100644 --- a/packages/mui-joy/src/Textarea/Textarea.tsx +++ b/packages/mui-joy/src/Textarea/Textarea.tsx @@ -223,12 +223,12 @@ const Textarea = React.forwardRef(function Textarea(inProps, ref) { const disabled = inProps.disabled ?? formControl?.disabled ?? disabledProp; const error = inProps.error ?? formControl?.error ?? errorProp; const size = inProps.size ?? formControl?.size ?? sizeProp; - const color = inProps.color ?? formControl?.color ?? colorProp; + const color = error ? 'danger' : inProps.color ?? formControl?.color ?? colorProp; const variant = inProps.variant ?? formControl?.variant ?? variantProp; const ownerState = { ...props, - color: error ? 'danger' : color, + color, disabled, error, focused, From d1d61cc918e5a96417f747976dfcc971f2ba79c6 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Sat, 3 Sep 2022 16:24:35 +0700 Subject: [PATCH 016/192] add composition demo to textfield --- .../components/text-field/TextFieldComposition.js | 15 +++++++++++++++ .../text-field/TextFieldComposition.tsx | 15 +++++++++++++++ docs/data/joy/components/text-field/text-field.md | 6 ++++++ 3 files changed, 36 insertions(+) create mode 100644 docs/data/joy/components/text-field/TextFieldComposition.js create mode 100644 docs/data/joy/components/text-field/TextFieldComposition.tsx diff --git a/docs/data/joy/components/text-field/TextFieldComposition.js b/docs/data/joy/components/text-field/TextFieldComposition.js new file mode 100644 index 00000000000000..112e731c471acb --- /dev/null +++ b/docs/data/joy/components/text-field/TextFieldComposition.js @@ -0,0 +1,15 @@ +import * as React from 'react'; +import FormControl from '@mui/joy/FormControl'; +import FormLabel from '@mui/joy/FormLabel'; +import FormHelperText from '@mui/joy/FormHelperText'; +import Input from '@mui/joy/Input'; + +export default function TextFieldComposition() { + return ( + + Label + + This is a helper text. + + ); +} diff --git a/docs/data/joy/components/text-field/TextFieldComposition.tsx b/docs/data/joy/components/text-field/TextFieldComposition.tsx new file mode 100644 index 00000000000000..112e731c471acb --- /dev/null +++ b/docs/data/joy/components/text-field/TextFieldComposition.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; +import FormControl from '@mui/joy/FormControl'; +import FormLabel from '@mui/joy/FormLabel'; +import FormHelperText from '@mui/joy/FormHelperText'; +import Input from '@mui/joy/Input'; + +export default function TextFieldComposition() { + return ( + + Label + + This is a helper text. + + ); +} diff --git a/docs/data/joy/components/text-field/text-field.md b/docs/data/joy/components/text-field/text-field.md index c90daa8ec2414f..78b6fd12e8dc4b 100644 --- a/docs/data/joy/components/text-field/text-field.md +++ b/docs/data/joy/components/text-field/text-field.md @@ -72,3 +72,9 @@ Use the `startDecorator` and/or `endDecorator` props to add supporting icons or To make the text field take up the full width of its container, use the `fullWidth` prop. {{"demo": "TextFieldFullwidth.js"}} + +### Composition + +`TextField` is composed of smaller components (`FormControl`, `FormLabel`, `Input`, and `FormHelperText`) that you can leverage directly to customize your form inputs. + +{{"demo": "TextFieldComposition.js"}} From 60cd8e5b7c27076e1c34d6047e9382e6de02982f Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Sat, 3 Sep 2022 16:29:42 +0700 Subject: [PATCH 017/192] use FormControl --- .../joy/components/select/SelectFieldDemo.js | 31 ++++++------------- docs/data/joy/components/select/select.md | 3 +- 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/docs/data/joy/components/select/SelectFieldDemo.js b/docs/data/joy/components/select/SelectFieldDemo.js index 5775c88bc12c65..737a6a915f8e35 100644 --- a/docs/data/joy/components/select/SelectFieldDemo.js +++ b/docs/data/joy/components/select/SelectFieldDemo.js @@ -1,5 +1,5 @@ import * as React from 'react'; -import Box from '@mui/joy/Box'; +import FormControl from '@mui/joy/FormControl'; import FormLabel from '@mui/joy/FormLabel'; import FormHelperText from '@mui/joy/FormHelperText'; import Select from '@mui/joy/Select'; @@ -8,31 +8,20 @@ import Option from '@mui/joy/Option'; export default function SelectFieldDemo() { const [value, setValue] = React.useState('dog'); return ( - - Favorite pet - - - This is a helper text. - - + This is a helper text. + ); } diff --git a/docs/data/joy/components/select/select.md b/docs/data/joy/components/select/select.md index c55b37a90514dd..4c2ee5442a1e6d 100644 --- a/docs/data/joy/components/select/select.md +++ b/docs/data/joy/components/select/select.md @@ -43,8 +43,7 @@ The `Select` component is similar to the native HTML's `` and `