diff --git a/docs/data/base/components/select/UnstyledSelectIntroduction.js b/docs/data/base/components/select/UnstyledSelectIntroduction.js index 63d77cc40e58a1..a2523d6b2d540d 100644 --- a/docs/data/base/components/select/UnstyledSelectIntroduction.js +++ b/docs/data/base/components/select/UnstyledSelectIntroduction.js @@ -33,7 +33,7 @@ const Button = React.forwardRef(function Button(props, ref) { return ( ); }); diff --git a/docs/data/base/components/select/UnstyledSelectIntroduction.tsx b/docs/data/base/components/select/UnstyledSelectIntroduction.tsx index a6aed20058c968..ea57e3bc07f25e 100644 --- a/docs/data/base/components/select/UnstyledSelectIntroduction.tsx +++ b/docs/data/base/components/select/UnstyledSelectIntroduction.tsx @@ -39,7 +39,7 @@ const Button = React.forwardRef(function Button( return ( ); }); diff --git a/docs/data/base/components/select/UseSelect.js b/docs/data/base/components/select/UseSelect.js index 7e4f2e46eb6309..a3709c24a474b9 100644 --- a/docs/data/base/components/select/UseSelect.js +++ b/docs/data/base/components/select/UseSelect.js @@ -2,6 +2,16 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { useSelect } from '@mui/base'; import { styled } from '@mui/system'; +import UnfoldMoreRoundedIcon from '@mui/icons-material/UnfoldMoreRounded'; + +const blue = { + 100: '#DAECFF', + 200: '#99CCF3', + 400: '#3399FF', + 500: '#007FFF', + 600: '#0072E5', + 900: '#003A75', +}; const grey = { 50: '#f6f8fa', @@ -20,29 +30,44 @@ const Root = styled('div')` position: relative; `; -const Toggle = styled('div')( +const Toggle = styled('button')( ({ theme }) => ` font-family: IBM Plex Sans, sans-serif; font-size: 0.875rem; box-sizing: border-box; min-height: calc(1.5em + 22px); min-width: 320px; + padding: 12px; border-radius: 12px; text-align: left; line-height: 1.5; + background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'}; border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; - background: var(--color, ${theme.palette.mode === 'dark' ? grey[900] : '#fff'}); - display: inline-flex; - align-items: center; - justify-content: center; - cursor: default; + position: relative; + transition-property: all; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 120ms; - & .placeholder { - opacity: 0.8; + box-shadow: 0 0 0 2px var(--color) inset; + + &:hover { + background: ${theme.palette.mode === 'dark' ? grey[800] : grey[50]}; + border-color: ${theme.palette.mode === 'dark' ? grey[600] : grey[300]}; + } + + &:focus-visible { + border-color: ${blue[400]}; + outline: 3px solid ${theme.palette.mode === 'dark' ? grey[600] : grey[200]}; + } + + & > svg { + font-size: 1rem; + position: absolute; + height: 100%; + top: 0; + right: 10px; } `, ); @@ -74,32 +99,62 @@ const Listbox = styled('ul')( &.hidden { opacity: 0; visibility: hidden; - transition: opacity 0.4s 0.5s ease, visibility 0.4s 0.5s step-end; + transition: opacity 0.4s ease, visibility 0.4s step-end; + } + `, +); + +const Option = styled('li')( + ({ theme }) => ` + padding: 8px; + border-radius: 0.45em; + + &[aria-selected='true'] { + background-color: ${theme.palette.mode === 'dark' ? blue[900] : blue[100]}; + color: ${theme.palette.mode === 'dark' ? blue[100] : blue[900]}; } - & > li { - padding: 8px; - border-radius: 0.45em; + &.highlighted, + &:hover { + background-color: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]}; + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; + } - &:hover { - background: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]}; - } + &[aria-selected='true'].highlighted { + background-color: ${theme.palette.mode === 'dark' ? blue[900] : blue[100]}; + color: ${theme.palette.mode === 'dark' ? blue[100] : blue[900]}; + } - &[aria-selected='true'] { - background: ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; - } + &:before { + content: ''; + width: 1ex; + height: 1ex; + margin-right: 1ex; + background-color: var(--color); + display: inline-block; + border-radius: 50%; + vertical-align: middle; } `, ); +function renderSelectedValue(value, options) { + const selectedOption = options.find((option) => option.value === value); + + return selectedOption ? `${selectedOption.label} (${value})` : null; +} + function CustomSelect({ options, placeholder }) { const listboxRef = React.useRef(null); const [listboxVisible, setListboxVisible] = React.useState(false); - const { getButtonProps, getListboxProps, getOptionProps, value } = useSelect({ - listboxRef, - options, - }); + const { getButtonProps, getListboxProps, getOptionProps, getOptionState, value } = + useSelect({ + listboxRef, + onOpenChange: setListboxVisible, + open: listboxVisible, + options, + }); React.useEffect(() => { if (listboxVisible) { @@ -108,21 +163,32 @@ function CustomSelect({ options, placeholder }) { }, [listboxVisible]); return ( - setListboxVisible(true)} - onMouseOut={() => setListboxVisible(false)} - onFocus={() => setListboxVisible(true)} - onBlur={() => setListboxVisible(false)} - > + - {value ?? {placeholder ?? ' '}} + {renderSelectedValue(value, options) || ( + {placeholder ?? ' '} + )} + + - - {options.map((option) => ( -
  • - {option.label} -
  • - ))} + + {options.map((option) => { + const optionState = getOptionState(option); + return ( + + ); + })}
    ); diff --git a/docs/data/base/components/select/UseSelect.tsx b/docs/data/base/components/select/UseSelect.tsx index 5ea4417d2002a2..68b2cec36fd3be 100644 --- a/docs/data/base/components/select/UseSelect.tsx +++ b/docs/data/base/components/select/UseSelect.tsx @@ -1,6 +1,16 @@ import * as React from 'react'; import { useSelect, SelectOption } from '@mui/base'; import { styled } from '@mui/system'; +import UnfoldMoreRoundedIcon from '@mui/icons-material/UnfoldMoreRounded'; + +const blue = { + 100: '#DAECFF', + 200: '#99CCF3', + 400: '#3399FF', + 500: '#007FFF', + 600: '#0072E5', + 900: '#003A75', +}; const grey = { 50: '#f6f8fa', @@ -19,29 +29,44 @@ const Root = styled('div')` position: relative; `; -const Toggle = styled('div')( +const Toggle = styled('button')( ({ theme }) => ` font-family: IBM Plex Sans, sans-serif; font-size: 0.875rem; box-sizing: border-box; min-height: calc(1.5em + 22px); min-width: 320px; + padding: 12px; border-radius: 12px; text-align: left; line-height: 1.5; + background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'}; border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; - background: var(--color, ${theme.palette.mode === 'dark' ? grey[900] : '#fff'}); - display: inline-flex; - align-items: center; - justify-content: center; - cursor: default; + position: relative; + transition-property: all; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 120ms; - & .placeholder { - opacity: 0.8; + box-shadow: 0 0 0 2px var(--color) inset; + + &:hover { + background: ${theme.palette.mode === 'dark' ? grey[800] : grey[50]}; + border-color: ${theme.palette.mode === 'dark' ? grey[600] : grey[300]}; + } + + &:focus-visible { + border-color: ${blue[400]}; + outline: 3px solid ${theme.palette.mode === 'dark' ? grey[600] : grey[200]}; + } + + & > svg { + font-size: 1rem; + position: absolute; + height: 100%; + top: 0; + right: 10px; } `, ); @@ -73,20 +98,41 @@ const Listbox = styled('ul')( &.hidden { opacity: 0; visibility: hidden; - transition: opacity 0.4s 0.5s ease, visibility 0.4s 0.5s step-end; + transition: opacity 0.4s ease, visibility 0.4s step-end; + } + `, +); + +const Option = styled('li')( + ({ theme }) => ` + padding: 8px; + border-radius: 0.45em; + + &[aria-selected='true'] { + background-color: ${theme.palette.mode === 'dark' ? blue[900] : blue[100]}; + color: ${theme.palette.mode === 'dark' ? blue[100] : blue[900]}; } - & > li { - padding: 8px; - border-radius: 0.45em; + &.highlighted, + &:hover { + background-color: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]}; + color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]}; + } - &:hover { - background: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]}; - } + &[aria-selected='true'].highlighted { + background-color: ${theme.palette.mode === 'dark' ? blue[900] : blue[100]}; + color: ${theme.palette.mode === 'dark' ? blue[100] : blue[900]}; + } - &[aria-selected='true'] { - background: ${theme.palette.mode === 'dark' ? grey[700] : grey[200]}; - } + &:before { + content: ''; + width: 1ex; + height: 1ex; + margin-right: 1ex; + background-color: var(--color); + display: inline-block; + border-radius: 50%; + vertical-align: middle; } `, ); @@ -96,14 +142,23 @@ interface Props { placeholder?: string; } +function renderSelectedValue(value: string | null, options: SelectOption[]) { + const selectedOption = options.find((option) => option.value === value); + + return selectedOption ? `${selectedOption.label} (${value})` : null; +} + function CustomSelect({ options, placeholder }: Props) { const listboxRef = React.useRef(null); const [listboxVisible, setListboxVisible] = React.useState(false); - const { getButtonProps, getListboxProps, getOptionProps, value } = useSelect({ - listboxRef, - options, - }); + const { getButtonProps, getListboxProps, getOptionProps, getOptionState, value } = + useSelect({ + listboxRef, + onOpenChange: setListboxVisible, + open: listboxVisible, + options, + }); React.useEffect(() => { if (listboxVisible) { @@ -112,21 +167,32 @@ function CustomSelect({ options, placeholder }: Props) { }, [listboxVisible]); return ( - setListboxVisible(true)} - onMouseOut={() => setListboxVisible(false)} - onFocus={() => setListboxVisible(true)} - onBlur={() => setListboxVisible(false)} - > + - {value ?? {placeholder ?? ' '}} + {renderSelectedValue(value, options) || ( + {placeholder ?? ' '} + )} + + - - {options.map((option) => ( -
  • - {option.label} -
  • - ))} + + {options.map((option) => { + const optionState = getOptionState(option); + return ( + + ); + })}
    ); diff --git a/docs/data/base/components/select/select.md b/docs/data/base/components/select/select.md index fe8eb4a4597b2c..049fb639b453e4 100644 --- a/docs/data/base/components/select/select.md +++ b/docs/data/base/components/select/select.md @@ -161,8 +161,9 @@ With hooks, you can take full control over how your component is rendered, and d You may not need to use hooks unless you find that you're limited by the customization options of their component counterparts—for instance, if your component requires significantly different [structure](#anatomy). ::: -The following example shows a select that opens when hovered over or focused. -It can be controlled by a mouse/touch or a keyboard. +The following example shows a select built with a hook. +Note how this component does not include any built-in classes. +The resulting HTML is much smaller compared to the unstyled component version, as the class names are not applied. {{"demo": "UseSelect.js", "defaultCodeOpen": false}}